Exemplo n.º 1
0
 def start(self):
     #gui communication
     self.msg = MessageQueue()
     self.msg_event = THD.Event()
     self.msg_thread = THD.Thread(target=self.__handleMessage)
     self.msg_thread.start()
     #data engine
     self.engine = Engine(self)
     self.__startAutoRefresh(True)
     self.engine.qryEtfQuoteFeed()
     self.engine.qryTableDataFeed()
 def __init__(self, gui):
     self.gui = gui
     #original position table
     self.ori_positions = None
     #etf quote
     self.etf = TableHandler()
     self.etf.reset(1, Engine.ETF_QUOTE_HEADERS, -1)
     #marketdata service
     self.md = MarketdataAdaptor()
     #database service
     self.dp = DADAPTOR.DataProxy()
     self.__reloadPositions()
     #flow control
     self.last_sync_time = DT.datetime.now()
     #gui communication
     self.msg = MessageQueue()
     self.msg_event = THD.Event()
     self.msg_thread = THD.Thread(target=self.__handleMessage)
     self.msg_thread.start()
     return
Exemplo n.º 3
0
 def start(self):
     #gui communication
     self.msg = MessageQueue()
     self.msg_event = THD.Event()
     self.msg_thread = THD.Thread(target=self.__handleMessage)
     self.msg_thread.start()
     #data engine
     self.engine = Engine(self)
     self.__startAutoRefresh(True)
     self.engine.qryEtfQuoteFeed()
     self.engine.qryTableDataFeed()
 def __init__(self, gui):
     self.gui = gui
     #original position table
     self.ori_positions = None
     #etf quote
     self.etf = TableHandler()
     self.etf.reset(1, Engine.ETF_QUOTE_HEADERS, -1)
     #marketdata service
     self.md  = MarketdataAdaptor()
     #database service
     self.dp = DADAPTOR.DataProxy()
     self.__reloadPositions()
     #flow control
     self.last_sync_time = DT.datetime.now()
     #gui communication
     self.msg = MessageQueue()
     self.msg_event = THD.Event()
     self.msg_thread = THD.Thread(target=self.__handleMessage)
     self.msg_thread.start()
     return
class Engine:
    etf_code = '510050.SH'
    ETF_QUOTE_HEADERS = ('last_price', 'open_price', 'high_price', 'low_price',
                         'update_time')

    STATISTICS_HEADERS = ('implied_vol', 'delta', 'gamma', 'vega', 'theta',
                          'intrnic', 'time_value')

    #-------------------------------------------------------------
    def __init__(self, gui):
        self.gui = gui
        #original position table
        self.ori_positions = None
        #etf quote
        self.etf = TableHandler()
        self.etf.reset(1, Engine.ETF_QUOTE_HEADERS, -1)
        #marketdata service
        self.md = MarketdataAdaptor()
        #database service
        self.dp = DADAPTOR.DataProxy()
        self.__reloadPositions()
        #flow control
        self.last_sync_time = DT.datetime.now()
        #gui communication
        self.msg = MessageQueue()
        self.msg_event = THD.Event()
        self.msg_thread = THD.Thread(target=self.__handleMessage)
        self.msg_thread.start()
        return

    def quit(self):
        self.__pushMsg(MessageTypes.QUIT)
        self.msg_thread.join()

    #-------------------------------------------------------------
    def qryUpdateData(self):
        self.__pushMsg(MessageTypes.UPDATE_QUOTE_DATA)

    def qryEtfQuoteFeed(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_ETF_QUOTE_FEED)

    def qryTableDataFeed(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_TABLE_FEED)

    def qryPositionBasedata(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_POSITION_BASEDATA_FEED)

    def qryCalGreeksSensibilityByGroup(self, option_group_id, stock_group_id,
                                       x_axis_type):
        self.__pushMsg(MessageTypes.GUI_QUERY_CAL_SENSI,
                       (option_group_id, stock_group_id, PassedIndexType.GROUP,
                        x_axis_type))

    def qryCalGreeksSensibilityByPosition(self, option_rows, stock_rows,
                                          x_axis_type):
        self.__pushMsg(
            MessageTypes.GUI_QUERY_CAL_SENSI,
            (option_rows, stock_rows, PassedIndexType.ROW, x_axis_type))

    def qryExerciseCurveByGroup(self, option_group_id, stock_group_id):
        self.__pushMsg(
            MessageTypes.GUI_QUERY_EXERCISE_CURVE,
            (option_group_id, stock_group_id, PassedIndexType.GROUP))

    def qryExerciseCurveByPosition(self, option_rows, stock_rows):
        self.__pushMsg(MessageTypes.GUI_QUERY_EXERCISE_CURVE,
                       (option_rows, stock_rows, PassedIndexType.ROW))

    def qryReloadPositions(self, positions_data=None):
        self.__pushMsg(MessageTypes.GUI_QUERY_RELOAD_POSITIONS, positions_data)

    def qrySavePositionCsv(self):
        self.__pushMsg(MessageTypes.SAVE_POSITION_CSV)

    def __pushMsg(self, msg_type, content=None):
        self.msg.pushMsg(msg_type, content)
        self.msg_event.set()

    def __handleMessage(self):
        try:
            while True:
                msg = self.msg.getMsg()
                if msg is None:
                    self.msg_event.wait()
                    self.msg_event.clear()
                #update marketdata order by user
                elif msg.type is MessageTypes.UPDATE_QUOTE_DATA:
                    self.__updateData()
                #qry engine provide table data
                elif msg.type is MessageTypes.GUI_QUERY_TABLE_FEED:
                    self.__feedDataTable()
                #qry etf data
                elif msg.type is MessageTypes.GUI_QUERY_ETF_QUOTE_FEED:
                    self.__feedEtfQuote()
                #qry position base data for editor
                elif msg.type is MessageTypes.GUI_QUERY_POSITION_BASEDATA_FEED:
                    self.__feedPositionBaseData()
                #cal greeks sensibility
                elif msg.type is MessageTypes.GUI_QUERY_CAL_SENSI:
                    self.__calGreekSensibility(msg.content[0], msg.content[1],
                                               msg.content[2], msg.content[3])

                elif msg.type is MessageTypes.GUI_QUERY_EXERCISE_CURVE:
                    self.__calOptionExerciseProfitCurve(
                        msg.content[0], msg.content[1], msg.content[2])

                elif msg.type is MessageTypes.GUI_QUERY_RELOAD_POSITIONS:
                    self.__reloadPositions(msg.content)

                elif msg.type is MessageTypes.SAVE_POSITION_CSV:
                    self.__savePosition2Csv()

                elif msg.type is MessageTypes.QUIT:
                    break

        except Exception as err:
            self.gui.onEngineError(err)
        #thread terminate
        return

    #-----------------------------------------------------------
    #positions should be a instance of TableHandler
    def __reloadPositions(self, positions=None):
        if type(positions) is TableHandler:
            pos = positions.toDataFrame()
        else:
            pos, err = DADAPTOR.loadPositionCsv()
            if not err is None:
                raise Exception('load position csv failed ...')
        #save pos
        self.ori_positions = pos
        #separate data
        option_rows = list()
        stock_rows = list()
        for r in range(0, pos.shape[0]):
            code = pos['code'].iat[r]
            contract_type = self.md.getContractType(code)
            if contract_type in ['call', 'put']:
                option_rows.append(r)
            else:
                stock_rows.append(r)
        option_df = pos.iloc[option_rows, :]
        stock_df = pos.iloc[stock_rows, :]
        self.dp.initialize(option_df, stock_df)
        self.__updateData(True)
        return

    def __savePosition2Csv(self):
        DADAPTOR.savePositionCsv(self.ori_positions)
        return

    def __updateData(self, update_baseinfo=False):
        self.last_sync_time = DT.datetime.now()
        #stock
        self.__updateEtfData()
        stk = self.dp.getStockData()
        for r in range(0, stk.rows()):
            self.__updateStockRow(r)
        #option
        opt = self.dp.getOptionData()
        for r in range(0, opt.rows()):
            if update_baseinfo:
                self.__updateRowBaseInfos(r)
            self.__updateOptionRow(r)
        #update database
        self.dp.updateData()
        return

    #update etf price data
    def __updateEtfData(self):
        etf_last_price = self.md.getLastprice(Engine.etf_code)
        self.etf.setByHeader(0, 'last_price', etf_last_price)
        self.etf.setByHeader(0, 'update_time',
                             self.md.getLastUpdateTime(Engine.etf_code))
        if not self.etf.getByHeader(0, 'open_price') < 0:
            self.etf.setByHeader(
                0, 'high_price',
                max(etf_last_price, self.etf.getByHeader(0, 'high_price')))
            self.etf.setByHeader(
                0, 'low_price',
                min(etf_last_price, self.etf.getByHeader(0, 'low_price')))
        else:
            O = self.md.getDailyOpen(Engine.etf_code)
            H = self.md.getDailyHigh(Engine.etf_code)
            L = self.md.getDailyLow(Engine.etf_code)
            if O and H and L:
                self.etf.setByHeader(0, 'open_price', O)
                self.etf.setByHeader(0, 'high_price', H)
                self.etf.setByHeader(0, 'low_price', L)
        return

    def __updateStockRow(self, irow):
        pos = self.dp.getStockData()
        last_price = self.etf.getByHeader(0, 'last_price')
        float_profit = ANALYSER.getFloatProfit(
            pos.getByHeader(irow, 'dir'), pos.getByHeader(irow, 'lots'),
            pos.getByHeader(irow, 'open_price'), last_price,
            self.md.getStockMultiplier())
        pos.setByHeader(irow, 'last_price', last_price)
        pos.setByHeader(irow, 'float_profit', float_profit)
        return

    #update basic_infos like expiry, strike_price etc.
    def __updateRowBaseInfos(self, irow):
        pos = self.dp.getOptionData()
        code = pos.getByHeader(irow, 'code')
        pos.setByHeader(irow, 'type', self.md.getContractType(code))
        pos.setByHeader(irow, 'strike', self.md.getStrikePrice(code))
        pos.setByHeader(irow, 'expiry', self.md.getExerciseDate(code))
        pos.setByHeader(irow, 'left_days', self.md.getDaysBeforeExercise(code))
        return

    #update
    def __updateOptionRow(self, irow):
        pos = self.dp.getOptionData()
        code = pos.getByHeader(irow, 'code')
        last_price = self.md.getLastprice(code)
        pos.setByHeader(irow, 'last_price', last_price)
        ###################################
        S = self.etf.getByHeader(0, 'last_price')
        K = pos.getByHeader(irow, 'strike')
        T = pos.getByHeader(irow, 'left_days')
        opt_type = pos.getByHeader(irow, 'type')
        #greeks
        stat = None
        if opt_type.lower() == 'call':
            stat = ANALYSER.getStatistics(S, K, T, last_price, True)
        elif opt_type.lower() == 'put':
            stat = ANALYSER.getStatistics(S, K, T, last_price, False)
        if stat:
            for header in Engine.STATISTICS_HEADERS:
                pos.setByHeader(irow, header, stat[header])
        #trade state
        float_profit = ANALYSER.getFloatProfit(
            pos.getByHeader(irow, 'dir'), pos.getByHeader(irow, 'lots'),
            pos.getByHeader(irow, 'open_price'), last_price,
            self.md.getOptionMultiplier())
        pos.setByHeader(irow, 'float_profit', float_profit)
        return

    def __feedDataTable(self):
        opt_data = TableHandler()
        opt_data.copyDataframe(self.dp.getOptionData().getDataFrame())
        stk_data = TableHandler()
        stk_data.copyDataframe(self.dp.getStockData().getDataFrame())
        ptf_data = TableHandler()
        ptf_data.copyDataframe(self.dp.getPortfolioData().getDataFrame())
        self.gui.onRepTableFeed(opt_data, stk_data, ptf_data)
        return

    def __feedEtfQuote(self):
        snap_etf = TableHandler()
        snap_etf.copy(self.etf)
        self.gui.onRepEtfQuoteFeed(snap_etf)
        return

    def __feedPositionBaseData(self):
        tdata = TableHandler()
        tdata.copyDataframe(self.ori_positions)
        self.gui.onRepPositionBasedataFeed(tdata)
        return

    def __calGreekSensibility(self, option_idx, stock_idx, idx_type,
                              x_axis_type):
        opt = self.dp.getOptionData()
        stk = self.dp.getStockData()
        if idx_type is PassedIndexType.GROUP:
            opt_data = opt.getPositionDataByGroupId(option_idx)
            stk_data = stk.getPositionDataByGroupId(stock_idx)
        elif idx_type is PassedIndexType.ROW:
            opt_data = opt.getPositionDataByRowIdx(option_idx)
            stk_data = stk.getPositionDataByRowIdx(stock_idx)
        else:
            return

        if x_axis_type is XAxisType.PRICE:
            rtn = ANALYSER.getGreeksSensibilityByPrice(
                opt_data, stk_data, self.etf.getByHeader(0, 'last_price'))
        elif x_axis_type is XAxisType.VOLATILITY:
            rtn = ANALYSER.getGreeksSensibilityByVolatility(
                opt_data, stk_data, self.etf.getByHeader(0, 'last_price'))
        elif x_axis_type is XAxisType.TIME:
            rtn = ANALYSER.getGreeksSensibilityByTime(
                opt_data, stk_data, self.etf.getByHeader(0, 'last_price'))
        else:
            return

        self.gui.onRepCalGreeksSensibility(rtn, x_axis_type)
        return

    def __calOptionExerciseProfitCurve(self, option_idx, stock_idx, idx_type):
        opt = self.dp.getOptionData()
        stk = self.dp.getStockData()
        if idx_type is PassedIndexType.GROUP:
            opt_data = opt.getPositionDataByGroupId(option_idx)
            stk_data = stk.getPositionDataByGroupId(stock_idx)
        elif idx_type is PassedIndexType.ROW:
            opt_data = opt.getPositionDataByRowIdx(option_idx)
            stk_data = stk.getPositionDataByRowIdx(stock_idx)
        else:
            return

        rtn = ANALYSER.getExerciseProfitCurve(
            opt_data, stk_data, self.etf.getByHeader(0, 'last_price'))
        self.gui.onRepCalExerciseCurve(rtn)
        return
Exemplo n.º 6
0
class OptionCalculator(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(OptionCalculator, self).__init__(parent)
        self.setupUi(self)
        #show
        self.show()
        #define signal&slot
        self.connect(self.fresh_quotes_button, SIGNAL('clicked()'), self.__onRefreshQuoteBtClicked)
        self.connect(self.edit_position_button, SIGNAL('clicked()'), self.__onEditPosBtClicked)
        self.connect(self.plot_button, SIGNAL('clicked()'), self.__onPlotBtClicked)
        self.connect(self, SIGNAL('PLOT_SENSIBILITY'), self.__plotGreeksSensibility)
        self.connect(self, SIGNAL('PLOT_EXERCISE_CURVE'), self.__plotExerciseCurve)
        self.connect(self, SIGNAL('SET_ETF_DISPLAY'), self.__setEtfDataDisplay)
        self.connect(self, SIGNAL('SET_CENTRAL_DISPLAY'), self.__setCentralTableDisplay)
        self.connect(self, SIGNAL('ENGINE_ERROR'), self.__notifyErrorOccur)
        #init position vtable
        self.option_data = MatrixModel(self)
        self.pos_deleg = AutoFormDelegate(self)
        self.position_vtable.setItemDelegate(self.pos_deleg)
        self.position_vtable.setModel(self.option_data)
        self.option_data.setSize(0, OPTION_DF_HEADERS)
        #init stock_position vtable
        self.stock_data = MatrixModel(self)
        self.stpos_deleg = AutoFormDelegate(self)
        self.stock_position_vtable.setItemDelegate(self.stpos_deleg)
        self.stock_position_vtable.setModel(self.stock_data)
        self.stock_data.setSize(0, STOCK_DF_HEADERS)
        #init portfolio vtable
        self.portfolio_data = MatrixModel(self)
        self.ptf_deleg = AutoFormDelegate(self)
        self.portfolio_vtable.setItemDelegate(self.ptf_deleg)
        self.portfolio_vtable.setModel(self.portfolio_data)
        self.portfolio_data.setSize(0, PORTFOLIO_DF_HEADERS)
        #gui communication
        self.msg = None
        self.msg_event = None
        self.msg_thread = None
        #data engine
        self.engine = None
        #flow control
        self.is_updating = False
        self.auto_refresh_timer = None
        #qt child
        self.edit_dialog = PosEditor(self)
        self.edit_dialog.setControler(self)
        return

    def start(self):
        #gui communication
        self.msg = MessageQueue()
        self.msg_event = THD.Event()
        self.msg_thread = THD.Thread(target=self.__handleMessage)
        self.msg_thread.start()
        #data engine
        self.engine = Engine(self)
        self.__startAutoRefresh(True)
        self.engine.qryEtfQuoteFeed()
        self.engine.qryTableDataFeed()

    def quit(self):
        if not self.auto_refresh_timer is None:
            self.auto_refresh_timer.cancel()
        if not self.engine is None:
            self.engine.quit()
        if not self.msg_thread is None:
            self.__pushMsg(MessageTypes.QUIT)
            self.msg_thread.join()
        return

    def closeEvent(self, event):
        rtn = QMessageBox.question(self, 'Save & exit', 'Save positions to csv?',
                                   QMessageBox.Save, QMessageBox.No, QMessageBox.Cancel)
        if rtn == QMessageBox.Cancel:
            event.ignore()
            return
        if rtn == QMessageBox.Save:
            self.onSavePosition2Csv()
        self.quit()
        #close
        super(OptionCalculator, self).closeEvent(event)
        return

    #-------------------------------------------------------------------------
    def onEngineError(self, err):
        with open(r'./logs.txt', 'a') as fid:
            err_info = '\n>>%s\n%s' % (str(DT.datetime.now()), str(err))
            fid.write(err_info)
        self.emit(SIGNAL('ENGINE_ERROR'))
        return

    def onRepTableFeed(self, option_data, stock_data, ptf_data):
        self.__pushMsg(MessageTypes.REPLY_TABLE_FEED, (option_data, stock_data, ptf_data))

    def onRepEtfQuoteFeed(self, etf_data):
        self.__pushMsg(MessageTypes.REPLY_ETF_QUOTE_FEED, etf_data)

    def onRepCalGreeksSensibility(self, plot_data, x_axis_type):
        self.__pushMsg(MessageTypes.REPLY_CAL_SENSI, (plot_data, x_axis_type))

    def onRepCalExerciseCurve(self, plot_data):
        self.__pushMsg(MessageTypes.REPLY_EXERCISE_CURVE, plot_data)

    def onRepPositionBasedataFeed(self, positions):
        self.__pushMsg(MessageTypes.REPLY_POSITION_BASEDATA_FEED, positions)

    def __handleMessage(self):
        while True:
            msg = self.msg.getMsg()
            if msg is None:
              self.msg_event.wait()
              self.msg_event.clear()
            #received data for display
            elif msg.type is MessageTypes.REPLY_TABLE_FEED:
                self.emit(SIGNAL('SET_CENTRAL_DISPLAY'),
                          msg.content[0], msg.content[1], msg.content[2])

            elif msg.type is MessageTypes.REPLY_ETF_QUOTE_FEED:
                self.emit(SIGNAL('SET_ETF_DISPLAY'), msg.content)

            elif msg.type is MessageTypes.REPLY_CAL_SENSI:
                self.emit(SIGNAL('PLOT_SENSIBILITY'), msg.content[0], msg.content[1])

            elif msg.type is MessageTypes.REPLY_EXERCISE_CURVE:
                self.emit(SIGNAL('PLOT_EXERCISE_CURVE'), msg.content)

            elif msg.type is MessageTypes.REPLY_POSITION_BASEDATA_FEED:
                self.__updatePosEditorData(msg.content)

            elif msg.type is MessageTypes.QUIT:
                break
        return

    def __pushMsg(self, msg_type, content=None):
        self.msg.pushMsg(msg_type, content)
        self.msg_event.set()
    #----------------------------------------------------------------------
    def onEditorClickBtSaveAll(self, position_data):
        self.engine.qryReloadPositions(position_data)
        self.__queryUpdateData()

    def onEditorClickBtReloadPosition(self):
        self.engine.qryReloadPositions()
        self.engine.qryPositionBasedata()
        self.__queryUpdateData()

    def onSavePosition2Csv(self):
        self.engine.qrySavePositionCsv()

    #----------------------------------------------------------------------
    def __onRefreshQuoteBtClicked(self):
        self.__queryUpdateData()

    def __onEditPosBtClicked(self):
        self.edit_dialog.wakeupEditor()
        self.engine.qryPositionBasedata()

    def __onPlotBtClicked(self):
        x_axis_type = self.greeks_x_axis_combobox.currentIndex()
        #pass group id list
        if self.portfolio_checkBox.isChecked():
            #collect group id
            group_ids = list()
            for r in getSelectedRows(self.portfolio_vtable):
                item = self.portfolio_data.getValueByHeader(r, 'group')
                try:
                    group_ids.append(int(item))
                except:
                    pass
            if group_ids:
                if x_axis_type == 0:
                    self.engine.qryCalGreeksSensibilityByGroup(group_ids, group_ids, XAxisType.PRICE)
                elif x_axis_type == 1:
                    self.engine.qryCalGreeksSensibilityByGroup(group_ids, group_ids, XAxisType.VOLATILITY)
                elif x_axis_type == 2:
                    self.engine.qryCalGreeksSensibilityByGroup(group_ids, group_ids, XAxisType.TIME)
                elif x_axis_type == 3:
                    self.engine.qryExerciseCurveByGroup(group_ids, group_ids)
        #pass row numbers list
        else:
            option_idx = getSelectedRows(self.position_vtable)
            stock_idx  = getSelectedRows(self.stock_position_vtable)
            if option_idx or stock_idx:
                if x_axis_type == 0:
                    self.engine.qryCalGreeksSensibilityByPosition(option_idx, stock_idx, XAxisType.PRICE)
                elif x_axis_type == 1:
                    self.engine.qryCalGreeksSensibilityByPosition(option_idx, stock_idx, XAxisType.VOLATILITY)
                elif x_axis_type == 2:
                    self.engine.qryCalGreeksSensibilityByPosition(option_idx, stock_idx, XAxisType.TIME)
                elif x_axis_type == 3:
                    self.engine.qryExerciseCurveByPosition(option_idx, stock_idx)
        return

    #-------------------------------------------------------------------
    def __startAutoRefresh(self, onInit=False):
        self.auto_refresh_timer = THD.Timer(300, self.__startAutoRefresh)
        self.auto_refresh_timer.start()
        if not onInit:
            self.__queryUpdateData()
        return

    def __queryUpdateData(self):
        if not self.is_updating:
            self.is_updating = True
            self.engine.qryUpdateData()
            self.engine.qryEtfQuoteFeed()
            self.engine.qryTableDataFeed()

    def __setEtfDataDisplay(self, etf_data):
        self.update_time_label.setText('%s' % etf_data.getByHeader(0, 'update_time').strftime(r'%H:%M:%S'))
        self.etf_openprice_label.setText('open: %.3f' % etf_data.getByHeader(0, 'open_price'))
        self.etf_highprice_label.setText('high: %.3f' % etf_data.getByHeader(0, 'high_price'))
        self.etf_lowprice_label.setText('low: %.3f' % etf_data.getByHeader(0, 'low_price'))
        self.etf_lastprice_label.setText('last: %.3f' % etf_data.getByHeader(0, 'last_price'))

    def __setCentralTableDisplay(self, option_data, stock_data, portfolio_data):
        self.option_data.setTableContent(option_data)
        self.stock_data.setTableContent(stock_data)
        self.portfolio_data.setTableContent(portfolio_data)
        #notify_updating_completed
        self.is_updating = False
        return

    def __updatePosEditorData(self, pos_table_handler):
        self.edit_dialog.setEditTableContent(pos_table_handler)

    def __plotGreeksSensibility(self, p_data, x_axis_type):
        if x_axis_type == XAxisType.PRICE:
            figure_name = 'by price'
        elif x_axis_type == XAxisType.VOLATILITY:
            figure_name = 'by volatility'
        elif x_axis_type == XAxisType.TIME:
            figure_name = 'by time'
        else:
            figure_name = ''

        fig = PLT.figure()
        fig.suptitle(figure_name)

        sp = fig.add_subplot(2, 2, 1)
        decorateAndPlot(sp, p_data['ax_x'], p_data['delta'],
                        title='delta', central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 2)
        decorateAndPlot(sp, p_data['ax_x'], p_data['gamma'],
                        title='gamma', central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 3)
        decorateAndPlot(sp, p_data['ax_x'], p_data['vega'],
                        title='vega', central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 4)
        decorateAndPlot(sp, p_data['ax_x'], p_data['theta'],
                        title='theta', central_x=p_data['central_x'])

        PLT.show()
        return

    def __plotExerciseCurve(self, plot_data):
        fig = PLT.figure()
        fig.suptitle('Theoretical earnings curve')

        sp = fig.add_subplot(1, 1, 1)
        decorateAndPlot(sp, plot_data['ax_x'], plot_data['exercise_profit'],
                        central_x=plot_data['central_x'])
        plotZeroLine(sp)

        PLT.show()
        return

    def __notifyErrorOccur(self):
        QMessageBox.question(self, 'Error', 'engine error occurs, restart manually ...',
                             QMessageBox.Yes)
Exemplo n.º 7
0
class OptionCalculator(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(OptionCalculator, self).__init__(parent)
        self.setupUi(self)
        #show
        self.show()
        #define signal&slot
        self.connect(self.fresh_quotes_button, SIGNAL('clicked()'),
                     self.__onRefreshQuoteBtClicked)
        self.connect(self.edit_position_button, SIGNAL('clicked()'),
                     self.__onEditPosBtClicked)
        self.connect(self.plot_button, SIGNAL('clicked()'),
                     self.__onPlotBtClicked)
        self.connect(self, SIGNAL('PLOT_SENSIBILITY'),
                     self.__plotGreeksSensibility)
        self.connect(self, SIGNAL('PLOT_EXERCISE_CURVE'),
                     self.__plotExerciseCurve)
        self.connect(self, SIGNAL('SET_ETF_DISPLAY'), self.__setEtfDataDisplay)
        self.connect(self, SIGNAL('SET_CENTRAL_DISPLAY'),
                     self.__setCentralTableDisplay)
        self.connect(self, SIGNAL('ENGINE_ERROR'), self.__notifyErrorOccur)
        #init position vtable
        self.option_data = MatrixModel(self)
        self.pos_deleg = AutoFormDelegate(self)
        self.position_vtable.setItemDelegate(self.pos_deleg)
        self.position_vtable.setModel(self.option_data)
        self.option_data.setSize(0, OPTION_DF_HEADERS)
        #init stock_position vtable
        self.stock_data = MatrixModel(self)
        self.stpos_deleg = AutoFormDelegate(self)
        self.stock_position_vtable.setItemDelegate(self.stpos_deleg)
        self.stock_position_vtable.setModel(self.stock_data)
        self.stock_data.setSize(0, STOCK_DF_HEADERS)
        #init portfolio vtable
        self.portfolio_data = MatrixModel(self)
        self.ptf_deleg = AutoFormDelegate(self)
        self.portfolio_vtable.setItemDelegate(self.ptf_deleg)
        self.portfolio_vtable.setModel(self.portfolio_data)
        self.portfolio_data.setSize(0, PORTFOLIO_DF_HEADERS)
        #gui communication
        self.msg = None
        self.msg_event = None
        self.msg_thread = None
        #data engine
        self.engine = None
        #flow control
        self.is_updating = False
        self.auto_refresh_timer = None
        #qt child
        self.edit_dialog = PosEditor(self)
        self.edit_dialog.setControler(self)
        return

    def start(self):
        #gui communication
        self.msg = MessageQueue()
        self.msg_event = THD.Event()
        self.msg_thread = THD.Thread(target=self.__handleMessage)
        self.msg_thread.start()
        #data engine
        self.engine = Engine(self)
        self.__startAutoRefresh(True)
        self.engine.qryEtfQuoteFeed()
        self.engine.qryTableDataFeed()

    def quit(self):
        if not self.auto_refresh_timer is None:
            self.auto_refresh_timer.cancel()
        if not self.engine is None:
            self.engine.quit()
        if not self.msg_thread is None:
            self.__pushMsg(MessageTypes.QUIT)
            self.msg_thread.join()
        return

    def closeEvent(self, event):
        rtn = QMessageBox.question(self, 'Save & exit',
                                   'Save positions to csv?', QMessageBox.Save,
                                   QMessageBox.No, QMessageBox.Cancel)
        if rtn == QMessageBox.Cancel:
            event.ignore()
            return
        if rtn == QMessageBox.Save:
            self.onSavePosition2Csv()
        self.quit()
        #close
        super(OptionCalculator, self).closeEvent(event)
        return

    #-------------------------------------------------------------------------
    def onEngineError(self, err):
        with open(r'./logs.txt', 'a') as fid:
            err_info = '\n>>%s\n%s' % (str(DT.datetime.now()), str(err))
            fid.write(err_info)
        self.emit(SIGNAL('ENGINE_ERROR'))
        return

    def onRepTableFeed(self, option_data, stock_data, ptf_data):
        self.__pushMsg(MessageTypes.REPLY_TABLE_FEED,
                       (option_data, stock_data, ptf_data))

    def onRepEtfQuoteFeed(self, etf_data):
        self.__pushMsg(MessageTypes.REPLY_ETF_QUOTE_FEED, etf_data)

    def onRepCalGreeksSensibility(self, plot_data, x_axis_type):
        self.__pushMsg(MessageTypes.REPLY_CAL_SENSI, (plot_data, x_axis_type))

    def onRepCalExerciseCurve(self, plot_data):
        self.__pushMsg(MessageTypes.REPLY_EXERCISE_CURVE, plot_data)

    def onRepPositionBasedataFeed(self, positions):
        self.__pushMsg(MessageTypes.REPLY_POSITION_BASEDATA_FEED, positions)

    def __handleMessage(self):
        while True:
            msg = self.msg.getMsg()
            if msg is None:
                self.msg_event.wait()
                self.msg_event.clear()
            #received data for display
            elif msg.type is MessageTypes.REPLY_TABLE_FEED:
                self.emit(SIGNAL('SET_CENTRAL_DISPLAY'), msg.content[0],
                          msg.content[1], msg.content[2])

            elif msg.type is MessageTypes.REPLY_ETF_QUOTE_FEED:
                self.emit(SIGNAL('SET_ETF_DISPLAY'), msg.content)

            elif msg.type is MessageTypes.REPLY_CAL_SENSI:
                self.emit(SIGNAL('PLOT_SENSIBILITY'), msg.content[0],
                          msg.content[1])

            elif msg.type is MessageTypes.REPLY_EXERCISE_CURVE:
                self.emit(SIGNAL('PLOT_EXERCISE_CURVE'), msg.content)

            elif msg.type is MessageTypes.REPLY_POSITION_BASEDATA_FEED:
                self.__updatePosEditorData(msg.content)

            elif msg.type is MessageTypes.QUIT:
                break
        return

    def __pushMsg(self, msg_type, content=None):
        self.msg.pushMsg(msg_type, content)
        self.msg_event.set()

    #----------------------------------------------------------------------
    def onEditorClickBtSaveAll(self, position_data):
        self.engine.qryReloadPositions(position_data)
        self.__queryUpdateData()

    def onEditorClickBtReloadPosition(self):
        self.engine.qryReloadPositions()
        self.engine.qryPositionBasedata()
        self.__queryUpdateData()

    def onSavePosition2Csv(self):
        self.engine.qrySavePositionCsv()

    #----------------------------------------------------------------------
    def __onRefreshQuoteBtClicked(self):
        self.__queryUpdateData()

    def __onEditPosBtClicked(self):
        self.edit_dialog.wakeupEditor()
        self.engine.qryPositionBasedata()

    def __onPlotBtClicked(self):
        x_axis_type = self.greeks_x_axis_combobox.currentIndex()
        #pass group id list
        if self.portfolio_checkBox.isChecked():
            #collect group id
            group_ids = list()
            for r in getSelectedRows(self.portfolio_vtable):
                item = self.portfolio_data.getValueByHeader(r, 'group')
                try:
                    group_ids.append(int(item))
                except:
                    pass
            if group_ids:
                if x_axis_type == 0:
                    self.engine.qryCalGreeksSensibilityByGroup(
                        group_ids, group_ids, XAxisType.PRICE)
                elif x_axis_type == 1:
                    self.engine.qryCalGreeksSensibilityByGroup(
                        group_ids, group_ids, XAxisType.VOLATILITY)
                elif x_axis_type == 2:
                    self.engine.qryCalGreeksSensibilityByGroup(
                        group_ids, group_ids, XAxisType.TIME)
                elif x_axis_type == 3:
                    self.engine.qryExerciseCurveByGroup(group_ids, group_ids)
        #pass row numbers list
        else:
            option_idx = getSelectedRows(self.position_vtable)
            stock_idx = getSelectedRows(self.stock_position_vtable)
            if option_idx or stock_idx:
                if x_axis_type == 0:
                    self.engine.qryCalGreeksSensibilityByPosition(
                        option_idx, stock_idx, XAxisType.PRICE)
                elif x_axis_type == 1:
                    self.engine.qryCalGreeksSensibilityByPosition(
                        option_idx, stock_idx, XAxisType.VOLATILITY)
                elif x_axis_type == 2:
                    self.engine.qryCalGreeksSensibilityByPosition(
                        option_idx, stock_idx, XAxisType.TIME)
                elif x_axis_type == 3:
                    self.engine.qryExerciseCurveByPosition(
                        option_idx, stock_idx)
        return

    #-------------------------------------------------------------------
    def __startAutoRefresh(self, onInit=False):
        self.auto_refresh_timer = THD.Timer(300, self.__startAutoRefresh)
        self.auto_refresh_timer.start()
        if not onInit:
            self.__queryUpdateData()
        return

    def __queryUpdateData(self):
        if not self.is_updating:
            self.is_updating = True
            self.engine.qryUpdateData()
            self.engine.qryEtfQuoteFeed()
            self.engine.qryTableDataFeed()

    def __setEtfDataDisplay(self, etf_data):
        self.update_time_label.setText(
            '%s' %
            etf_data.getByHeader(0, 'update_time').strftime(r'%H:%M:%S'))
        self.etf_openprice_label.setText('open: %.3f' %
                                         etf_data.getByHeader(0, 'open_price'))
        self.etf_highprice_label.setText('high: %.3f' %
                                         etf_data.getByHeader(0, 'high_price'))
        self.etf_lowprice_label.setText('low: %.3f' %
                                        etf_data.getByHeader(0, 'low_price'))
        self.etf_lastprice_label.setText('last: %.3f' %
                                         etf_data.getByHeader(0, 'last_price'))

    def __setCentralTableDisplay(self, option_data, stock_data,
                                 portfolio_data):
        self.option_data.setTableContent(option_data)
        self.stock_data.setTableContent(stock_data)
        self.portfolio_data.setTableContent(portfolio_data)
        #notify_updating_completed
        self.is_updating = False
        return

    def __updatePosEditorData(self, pos_table_handler):
        self.edit_dialog.setEditTableContent(pos_table_handler)

    def __plotGreeksSensibility(self, p_data, x_axis_type):
        if x_axis_type == XAxisType.PRICE:
            figure_name = 'by price'
        elif x_axis_type == XAxisType.VOLATILITY:
            figure_name = 'by volatility'
        elif x_axis_type == XAxisType.TIME:
            figure_name = 'by time'
        else:
            figure_name = ''

        fig = PLT.figure()
        fig.suptitle(figure_name)

        sp = fig.add_subplot(2, 2, 1)
        decorateAndPlot(sp,
                        p_data['ax_x'],
                        p_data['delta'],
                        title='delta',
                        central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 2)
        decorateAndPlot(sp,
                        p_data['ax_x'],
                        p_data['gamma'],
                        title='gamma',
                        central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 3)
        decorateAndPlot(sp,
                        p_data['ax_x'],
                        p_data['vega'],
                        title='vega',
                        central_x=p_data['central_x'])

        sp = fig.add_subplot(2, 2, 4)
        decorateAndPlot(sp,
                        p_data['ax_x'],
                        p_data['theta'],
                        title='theta',
                        central_x=p_data['central_x'])

        PLT.show()
        return

    def __plotExerciseCurve(self, plot_data):
        fig = PLT.figure()
        fig.suptitle('Theoretical earnings curve')

        sp = fig.add_subplot(1, 1, 1)
        decorateAndPlot(sp,
                        plot_data['ax_x'],
                        plot_data['exercise_profit'],
                        central_x=plot_data['central_x'])
        plotZeroLine(sp)

        PLT.show()
        return

    def __notifyErrorOccur(self):
        QMessageBox.question(self, 'Error',
                             'engine error occurs, restart manually ...',
                             QMessageBox.Yes)
class Engine:
    etf_code = '510050.SH'
    ETF_QUOTE_HEADERS = ('last_price', 'open_price', 'high_price',
                         'low_price', 'update_time')

    STATISTICS_HEADERS = ('implied_vol', 'delta', 'gamma', 'vega',
                          'theta', 'intrnic', 'time_value')

    #-------------------------------------------------------------
    def __init__(self, gui):
        self.gui = gui
        #original position table
        self.ori_positions = None
        #etf quote
        self.etf = TableHandler()
        self.etf.reset(1, Engine.ETF_QUOTE_HEADERS, -1)
        #marketdata service
        self.md  = MarketdataAdaptor()
        #database service
        self.dp = DADAPTOR.DataProxy()
        self.__reloadPositions()
        #flow control
        self.last_sync_time = DT.datetime.now()
        #gui communication
        self.msg = MessageQueue()
        self.msg_event = THD.Event()
        self.msg_thread = THD.Thread(target=self.__handleMessage)
        self.msg_thread.start()
        return

    def quit(self):
        self.__pushMsg(MessageTypes.QUIT)
        self.msg_thread.join()

    #-------------------------------------------------------------
    def qryUpdateData(self):
        self.__pushMsg(MessageTypes.UPDATE_QUOTE_DATA)

    def qryEtfQuoteFeed(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_ETF_QUOTE_FEED)

    def qryTableDataFeed(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_TABLE_FEED)

    def qryPositionBasedata(self):
        self.__pushMsg(MessageTypes.GUI_QUERY_POSITION_BASEDATA_FEED)

    def qryCalGreeksSensibilityByGroup(self, option_group_id, stock_group_id, x_axis_type):
        self.__pushMsg(MessageTypes.GUI_QUERY_CAL_SENSI,
                       (option_group_id, stock_group_id,
                       PassedIndexType.GROUP, x_axis_type))

    def qryCalGreeksSensibilityByPosition(self, option_rows, stock_rows, x_axis_type):
        self.__pushMsg(MessageTypes.GUI_QUERY_CAL_SENSI,
                       (option_rows, stock_rows,
                        PassedIndexType.ROW, x_axis_type))

    def qryExerciseCurveByGroup(self, option_group_id, stock_group_id):
        self.__pushMsg(MessageTypes.GUI_QUERY_EXERCISE_CURVE,
                       (option_group_id, stock_group_id, PassedIndexType.GROUP))

    def qryExerciseCurveByPosition(self, option_rows, stock_rows):
        self.__pushMsg(MessageTypes.GUI_QUERY_EXERCISE_CURVE,
                       (option_rows, stock_rows, PassedIndexType.ROW))

    def qryReloadPositions(self, positions_data=None):
        self.__pushMsg(MessageTypes.GUI_QUERY_RELOAD_POSITIONS, positions_data)

    def qrySavePositionCsv(self):
        self.__pushMsg(MessageTypes.SAVE_POSITION_CSV)

    def __pushMsg(self, msg_type, content=None):
        self.msg.pushMsg(msg_type, content)
        self.msg_event.set()

    def __handleMessage(self):
        try:
            while True:
                msg = self.msg.getMsg()
                if msg is None:
                  self.msg_event.wait()
                  self.msg_event.clear()
                #update marketdata order by user
                elif msg.type is MessageTypes.UPDATE_QUOTE_DATA:
                    self.__updateData()
                #qry engine provide table data
                elif msg.type is MessageTypes.GUI_QUERY_TABLE_FEED:
                    self.__feedDataTable()
                #qry etf data
                elif msg.type is MessageTypes.GUI_QUERY_ETF_QUOTE_FEED:
                    self.__feedEtfQuote()
                #qry position base data for editor
                elif msg.type is MessageTypes.GUI_QUERY_POSITION_BASEDATA_FEED:
                    self.__feedPositionBaseData()
                #cal greeks sensibility
                elif msg.type is MessageTypes.GUI_QUERY_CAL_SENSI:
                    self.__calGreekSensibility(msg.content[0], msg.content[1],
                                               msg.content[2], msg.content[3])

                elif msg.type is MessageTypes.GUI_QUERY_EXERCISE_CURVE:
                    self.__calOptionExerciseProfitCurve(msg.content[0], msg.content[1],
                                                        msg.content[2])

                elif msg.type is MessageTypes.GUI_QUERY_RELOAD_POSITIONS:
                    self.__reloadPositions(msg.content)

                elif msg.type is MessageTypes.SAVE_POSITION_CSV:
                    self.__savePosition2Csv()

                elif msg.type is MessageTypes.QUIT:
                    break

        except Exception as err:
            self.gui.onEngineError(err)
        #thread terminate
        return

    #-----------------------------------------------------------
    #positions should be a instance of TableHandler
    def __reloadPositions(self, positions=None):
        if type(positions) is TableHandler:
            pos = positions.toDataFrame()
        else:
            pos, err = DADAPTOR.loadPositionCsv()
            if not err is None:
                raise Exception('load position csv failed ...')
        #save pos
        self.ori_positions = pos
        #separate data
        option_rows = list()
        stock_rows = list()
        for r in range(0, pos.shape[0]):
            code = pos['code'].iat[r]
            contract_type = self.md.getContractType(code)
            if contract_type in ['call', 'put']:
                option_rows.append(r)
            else:
                stock_rows.append(r)
        option_df = pos.iloc[option_rows, :]
        stock_df = pos.iloc[stock_rows, :]
        self.dp.initialize(option_df, stock_df)
        self.__updateData(True)
        return

    def __savePosition2Csv(self):
        DADAPTOR.savePositionCsv(self.ori_positions)
        return

    def __updateData(self, update_baseinfo=False):
        self.last_sync_time = DT.datetime.now()
        #stock
        self.__updateEtfData()
        stk = self.dp.getStockData()
        for r in range(0, stk.rows()):
            self.__updateStockRow(r)
        #option
        opt = self.dp.getOptionData()
        for r in range(0, opt.rows()):
            if update_baseinfo:
                self.__updateRowBaseInfos(r)
            self.__updateOptionRow(r)
        #update database
        self.dp.updateData()
        return

    #update etf price data
    def __updateEtfData(self):
        etf_last_price = self.md.getLastprice(Engine.etf_code)
        self.etf.setByHeader(0, 'last_price', etf_last_price)
        self.etf.setByHeader(0, 'update_time', self.md.getLastUpdateTime(Engine.etf_code))
        if not self.etf.getByHeader(0, 'open_price') < 0:
            self.etf.setByHeader(0, 'high_price', max(etf_last_price, self.etf.getByHeader(0, 'high_price')))
            self.etf.setByHeader(0, 'low_price', min(etf_last_price, self.etf.getByHeader(0, 'low_price')))
        else:
            O = self.md.getDailyOpen(Engine.etf_code)
            H = self.md.getDailyHigh(Engine.etf_code)
            L = self.md.getDailyLow(Engine.etf_code)
            if O and H and L:
                self.etf.setByHeader(0, 'open_price', O)
                self.etf.setByHeader(0, 'high_price', H)
                self.etf.setByHeader(0, 'low_price', L)
        return

    def __updateStockRow(self, irow):
        pos = self.dp.getStockData()
        last_price = self.etf.getByHeader(0, 'last_price')
        float_profit = ANALYSER.getFloatProfit(pos.getByHeader(irow, 'dir'),
                                               pos.getByHeader(irow, 'lots'),
                                               pos.getByHeader(irow, 'open_price'),
                                               last_price, self.md.getStockMultiplier())
        pos.setByHeader(irow, 'last_price', last_price)
        pos.setByHeader(irow, 'float_profit', float_profit)
        return

    #update basic_infos like expiry, strike_price etc.
    def __updateRowBaseInfos(self, irow):
        pos = self.dp.getOptionData()
        code = pos.getByHeader(irow, 'code')
        pos.setByHeader(irow, 'type', self.md.getContractType(code))
        pos.setByHeader(irow, 'strike', self.md.getStrikePrice(code))
        pos.setByHeader(irow, 'expiry', self.md.getExerciseDate(code))
        pos.setByHeader(irow, 'left_days', self.md.getDaysBeforeExercise(code))
        return

    #update
    def __updateOptionRow(self, irow):
        pos = self.dp.getOptionData()
        code = pos.getByHeader(irow, 'code')
        last_price = self.md.getLastprice(code)
        pos.setByHeader(irow, 'last_price', last_price)
        ###################################
        S = self.etf.getByHeader(0, 'last_price')
        K = pos.getByHeader(irow, 'strike')
        T = pos.getByHeader(irow, 'left_days')
        opt_type = pos.getByHeader(irow, 'type')
        #greeks
        stat = None
        if opt_type.lower() == 'call':
            stat = ANALYSER.getStatistics(S, K, T, last_price, True)
        elif opt_type.lower() == 'put':
            stat = ANALYSER.getStatistics(S, K, T, last_price, False)
        if stat:
            for header in Engine.STATISTICS_HEADERS:
                pos.setByHeader(irow, header, stat[header])
        #trade state
        float_profit = ANALYSER.getFloatProfit(pos.getByHeader(irow, 'dir'),
                                               pos.getByHeader(irow, 'lots'),
                                               pos.getByHeader(irow, 'open_price'),
                                               last_price, self.md.getOptionMultiplier())
        pos.setByHeader(irow, 'float_profit', float_profit)
        return

    def __feedDataTable(self):
        opt_data = TableHandler()
        opt_data.copyDataframe(self.dp.getOptionData().getDataFrame())
        stk_data = TableHandler()
        stk_data.copyDataframe(self.dp.getStockData().getDataFrame())
        ptf_data = TableHandler()
        ptf_data.copyDataframe(self.dp.getPortfolioData().getDataFrame())
        self.gui.onRepTableFeed(opt_data, stk_data, ptf_data)
        return

    def __feedEtfQuote(self):
        snap_etf = TableHandler()
        snap_etf.copy(self.etf)
        self.gui.onRepEtfQuoteFeed(snap_etf)
        return

    def __feedPositionBaseData(self):
        tdata = TableHandler()
        tdata.copyDataframe(self.ori_positions)
        self.gui.onRepPositionBasedataFeed(tdata)
        return

    def __calGreekSensibility(self, option_idx, stock_idx, idx_type, x_axis_type):
        opt = self.dp.getOptionData()
        stk = self.dp.getStockData()
        if idx_type is PassedIndexType.GROUP:
            opt_data = opt.getPositionDataByGroupId(option_idx)
            stk_data = stk.getPositionDataByGroupId(stock_idx)
        elif idx_type is PassedIndexType.ROW:
            opt_data = opt.getPositionDataByRowIdx(option_idx)
            stk_data = stk.getPositionDataByRowIdx(stock_idx)
        else:
            return

        if x_axis_type is XAxisType.PRICE:
            rtn = ANALYSER.getGreeksSensibilityByPrice(opt_data, stk_data,
                                                       self.etf.getByHeader(0, 'last_price'))
        elif x_axis_type is XAxisType.VOLATILITY:
            rtn = ANALYSER.getGreeksSensibilityByVolatility(opt_data, stk_data,
                                                            self.etf.getByHeader(0, 'last_price'))
        elif x_axis_type is XAxisType.TIME:
            rtn = ANALYSER.getGreeksSensibilityByTime(opt_data, stk_data,
                                                      self.etf.getByHeader(0, 'last_price'))
        else:
            return

        self.gui.onRepCalGreeksSensibility(rtn, x_axis_type)
        return

    def __calOptionExerciseProfitCurve(self, option_idx, stock_idx, idx_type):
        opt = self.dp.getOptionData()
        stk = self.dp.getStockData()
        if idx_type is PassedIndexType.GROUP:
            opt_data = opt.getPositionDataByGroupId(option_idx)
            stk_data = stk.getPositionDataByGroupId(stock_idx)
        elif idx_type is PassedIndexType.ROW:
            opt_data = opt.getPositionDataByRowIdx(option_idx)
            stk_data = stk.getPositionDataByRowIdx(stock_idx)
        else:
            return

        rtn = ANALYSER.getExerciseProfitCurve(opt_data, stk_data,
                                              self.etf.getByHeader(0, 'last_price'))
        self.gui.onRepCalExerciseCurve(rtn)
        return