Ejemplo n.º 1
0
    def __init__(self,
                 sas: StockAnalysisSystem,
                 memo_record: RecordSet,
                 parent: QWidget = None):
        super(StockMemoEditor, self).__init__(parent)

        self.__sas = sas
        self.__memo_recordset = memo_record

        self.__current_stock = None
        self.__current_index = None
        self.__current_record: Record = None

        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_stock = SecuritiesSelector(data_utility)
        self.__table_memo_index = EasyQTableWidget()

        self.__datetime_time = QDateTimeEdit(QDateTime().currentDateTime())
        self.__line_brief = QLineEdit()
        self.__text_record = QTextEdit()

        self.__button_new = QPushButton('新建')
        self.__button_apply = QPushButton('保存')

        self.init_ui()
        self.config_ui()
Ejemplo n.º 2
0
    def __init__(self, memo_data: StockMemoData):
        super(StockChartUi, self).__init__()

        self.__memo_data = memo_data
        self.__memo_data.add_observer(self)

        self.__sas = memo_data.get_sas()
        self.__memo_record: StockMemoRecord = memo_data.get_memo_record()

        self.__in_edit_mode = True
        self.__paint_securities = ''
        self.__paint_trade_data = None

        # # Record
        # user_path = os.path.expanduser('~')
        # project_path = sas.get_project_path() if sas is not None else os.getcwd()
        #
        # self.__root_path = \
        #     memo_path_from_project_path(project_path) if user_path == '' else \
        #     memo_path_from_user_path(user_path)
        # self.__memo_record = Record(os.path.join(self.__root_path, 'stock_memo.csv'), STOCK_MEMO_COLUMNS)

        # vnpy chart
        self.__vnpy_chart = ChartWidget()

        # Memo editor
        self.__memo_editor: StockMemoEditor = self.__memo_data.get_data(
            'editor')

        # Timer for workaround signal fired twice
        self.__accepted = False
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_name = SecuritiesSelector(data_utility)
        self.__button_ensure = QPushButton('确定')

        self.__check_abs = QCheckBox('固定价格范围')
        self.__check_memo = QCheckBox('笔记')
        self.__check_volume = QCheckBox('成交量')

        self.__radio_adj_tail = QRadioButton('后复权')
        self.__radio_adj_head = QRadioButton('前复权')
        self.__radio_adj_none = QRadioButton('不复权')
        self.__group_adj = QButtonGroup(self)

        self.__radio_log_return = QRadioButton('对数收益')
        self.__radio_simple_return = QRadioButton('算术收益')
        self.__group_return = QButtonGroup(self)

        self.__init_ui()
        self.__config_ui()
Ejemplo n.º 3
0
    def __init__(self, memo_data: StockMemoData):
        super(StockMemoDeck, self).__init__()
        self.__memo_data = memo_data

        if self.__memo_data is not None:
            self.__memo_data.add_observer(self)

            self.__sas = self.__memo_data.get_sas()
            self.__memo_record: StockMemoRecord = self.__memo_data.get_memo_record()
            self.__memo_editor: StockMemoEditor = self.__memo_data.get_data('editor')
            self.__data_utility = self.__sas.get_data_hub_entry().get_data_utility() if self.__sas is not None else None
        else:
            # For layout debug
            self.__sas = None
            self.__memo_record = None
            self.__memo_editor = None
            self.__data_utility = None

        self.__memo_extras = []
        self.__list_securities = []
        self.__show_securities = []

        # ---------------- Page -----------------

        self.__page = 1
        self.__item_per_page = 20

        self.__button_first = QPushButton('|<')
        self.__button_prev = QPushButton('<')
        self.__spin_page = QSpinBox()
        self.__label_total_page = QLabel('/ 1')
        self.__button_jump = QPushButton('GO')
        self.__button_next = QPushButton('>')
        self.__button_last = QPushButton('>|')

        self.__button_reload = QPushButton('Reload')
        self.__check_show_black_list = QCheckBox('Black List')

        # --------------- Widgets ---------------

        self.__memo_table = TableViewEx()
        self.__stock_selector = \
            SecuritiesSelector(self.__data_utility) if self.__data_utility is not None else QComboBox()
        self.__line_path = QLineEdit(self.__memo_data.get_root_path() if self.__memo_data is not None else root_path)
        self.__info_panel = QLabel(NOTE)
        self.__button_new = QPushButton('New')
        self.__button_filter = QPushButton('Filter')
        self.__button_browse = QPushButton('Browse')
        # self.__button_black_list = QPushButton('Black List')

        self.__layout_extra = QHBoxLayout()

        self.init_ui()
        self.config_ui()

        self.show_securities(self.__memo_record.get_all_security() if self.__memo_record is not None else [])
Ejemplo n.º 4
0
        def __init__(self, data_utility: DataUtility):
            self.__data_utility = data_utility
            super(BlackListUi.BlackListEditor, self).__init__()

            self.__stock_selector = \
                SecuritiesSelector(self.__data_utility) if self.__data_utility is not None else QComboBox()
            self.__editor_reason = QTextEdit()

            self.init_ui()
            self.config_ui()
Ejemplo n.º 5
0
    class BlackListEditor(QWidget):
        def __init__(self, data_utility: DataUtility):
            self.__data_utility = data_utility
            super(BlackListUi.BlackListEditor, self).__init__()

            self.__stock_selector = \
                SecuritiesSelector(self.__data_utility) if self.__data_utility is not None else QComboBox()
            self.__editor_reason = QTextEdit()

            self.init_ui()
            self.config_ui()

        def init_ui(self):
            main_layout = QVBoxLayout()
            self.setLayout(main_layout)

            main_layout.addWidget(QLabel('Security: '))
            main_layout.addWidget(self.__stock_selector)

            main_layout.addWidget(QLabel('Reason: '))
            main_layout.addWidget(self.__editor_reason)

        def config_ui(self):
            self.setMinimumSize(QSize(600, 400))

        def get_input(self) -> (str, str):
            return self.__stock_selector.get_select_securities(), \
                   self.__editor_reason.toPlainText()
Ejemplo n.º 6
0
    def __init__(self, sas: StockAnalysisSystem):
        super(StockHistoryUi, self).__init__()

        self.__sas = sas
        self.__paint_securities = ''
        self.__paint_trade_data = None

        # Record set
        project_path = sas.get_project_path(
        ) if sas is not None else os.getcwd()
        self.__root_path = memo_path_from_project_path(project_path)
        self.__memo_recordset = RecordSet(self.__root_path)

        # vnpy chart
        self.__vnpy_chart = ChartWidget()

        # Memo editor
        self.__memo_editor = StockMemoEditor(self.__sas, self.__memo_recordset)

        # Timer for workaround signal fired twice
        self.__accepted = False
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_name = SecuritiesSelector(data_utility)
        self.__button_ensure = QPushButton('确定')

        self.__check_memo = QCheckBox('笔记')
        self.__check_volume = QCheckBox('成交量')

        self.__radio_adj_tail = QRadioButton('后复权')
        self.__radio_adj_head = QRadioButton('前复权')
        self.__radio_adj_none = QRadioButton('不复权')
        self.__group_adj = QButtonGroup(self)

        self.__radio_log_return = QRadioButton('对数收益')
        self.__radio_simple_return = QRadioButton('算术收益')
        self.__group_return = QButtonGroup(self)

        self.__init_ui()
        self.__config_ui()
Ejemplo n.º 7
0
    def __init__(self, memo_data: StockMemoData, parent: QWidget = None):
        self.__memo_data = memo_data
        super(StockMemoEditor, self).__init__(parent)

        # The filter of left list
        # self.__filter_identity: str = ''
        # self.__filter_datetime: datetime.datetime = None

        # The stock that selected by combobox or outer setting
        self.__current_stock = None

        # The current index of editing memo
        # Not None: Update exists
        # None: Create new
        self.__current_index = None

        # The memo list that displaying in the left list
        self.__current_memos: pd.DataFrame = None

        self.__observers = []

        self.__sas = self.__memo_data.get_sas(
        ) if self.__memo_data is not None else None
        self.__memo_record = self.__memo_data.get_memo_record(
        ) if self.__memo_data is not None else None

        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_stock = SecuritiesSelector(data_utility)
        self.__table_memo_index = EasyQTableWidget()

        self.__datetime_time = QDateTimeEdit(QDateTime().currentDateTime())
        self.__line_brief = QLineEdit()
        self.__text_record = QTextEdit()

        self.__button_new = QPushButton('New')
        self.__button_apply = QPushButton('Save')
        self.__button_delete = QPushButton('Delete')

        self.init_ui()
        self.config_ui()
Ejemplo n.º 8
0
class StockHistoryUi(QWidget):
    ADJUST_TAIL = 0
    ADJUST_HEAD = 1
    ADJUST_NONE = 2

    RETURN_LOG = 3
    RETURN_SIMPLE = 4

    def __init__(self, sas: StockAnalysisSystem):
        super(StockHistoryUi, self).__init__()

        self.__sas = sas
        self.__paint_securities = ''
        self.__paint_trade_data = None

        # Record set
        project_path = sas.get_project_path(
        ) if sas is not None else os.getcwd()
        self.__root_path = memo_path_from_project_path(project_path)
        self.__memo_recordset = RecordSet(self.__root_path)

        # vnpy chart
        self.__vnpy_chart = ChartWidget()

        # Memo editor
        self.__memo_editor = StockMemoEditor(self.__sas, self.__memo_recordset)

        # Timer for workaround signal fired twice
        self.__accepted = False
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_name = SecuritiesSelector(data_utility)
        self.__button_ensure = QPushButton('确定')

        self.__check_memo = QCheckBox('笔记')
        self.__check_volume = QCheckBox('成交量')

        self.__radio_adj_tail = QRadioButton('后复权')
        self.__radio_adj_head = QRadioButton('前复权')
        self.__radio_adj_none = QRadioButton('不复权')
        self.__group_adj = QButtonGroup(self)

        self.__radio_log_return = QRadioButton('对数收益')
        self.__radio_simple_return = QRadioButton('算术收益')
        self.__group_return = QButtonGroup(self)

        self.__init_ui()
        self.__config_ui()

    def __init_ui(self):
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        self.__group_adj.addButton(self.__radio_adj_tail)
        self.__group_adj.addButton(self.__radio_adj_head)
        self.__group_adj.addButton(self.__radio_adj_none)
        group_box_adj, group_layout = create_h_group_box('')
        group_layout.addWidget(self.__radio_adj_tail)
        group_layout.addWidget(self.__radio_adj_head)
        group_layout.addWidget(self.__radio_adj_none)

        self.__group_return.addButton(self.__radio_log_return)
        self.__group_return.addButton(self.__radio_simple_return)
        group_box_return, group_layout = create_h_group_box('')
        group_layout.addWidget(self.__radio_log_return)
        group_layout.addWidget(self.__radio_simple_return)

        group_box, group_layout = create_v_group_box('Securities')

        main_layout.addWidget(self.__vnpy_chart, 99)
        main_layout.addWidget(group_box)

        group_layout.addLayout(
            horizon_layout([
                self.__combo_name, group_box_adj, group_box_return,
                self.__button_ensure, self.__check_volume, self.__check_memo
            ]))

    def __config_ui(self):
        data_utility = self.__sas.get_data_hub_entry().get_data_utility()
        index_dict = data_utility.get_support_index()

        self.__combo_name.setEditable(True)
        for key in index_dict:
            self.__combo_name.addItem(key + ' | ' + index_dict.get(key), key)

        self.__radio_adj_none.setChecked(True)
        self.__radio_simple_return.setChecked(True)

        self.__check_memo.clicked.connect(self.on_button_memo)
        self.__check_volume.clicked.connect(self.on_button_volume)
        self.__button_ensure.clicked.connect(self.on_button_ensure)

        self.setMinimumWidth(1280)
        self.setMinimumHeight(800)

        # --------------------- Editor ----------------------

        self.__memo_editor.closeEvent = self.on_editor_closed

        # ---------------------- Chart ----------------------

        self.__vnpy_chart.add_plot("candle", hide_x_axis=True)
        self.__vnpy_chart.add_plot("volume", maximum_height=200)
        self.__vnpy_chart.add_plot("memo", maximum_height=50)

        self.__vnpy_chart.add_item(CandleItem, "candle", "candle")
        self.__vnpy_chart.add_item(VolumeItem, "volume", "volume")
        self.__vnpy_chart.add_item(MemoItem, "memo", "memo")

        self.__vnpy_chart.add_cursor()
        self.__vnpy_chart.scene().sigMouseClicked.connect(
            self.on_chart_clicked)

    @pyqtSlot(MouseClickEvent)
    def on_chart_clicked(self, event: MouseClickEvent):
        if not event.double or self.__accepted:
            return
        self.__accepted = True
        scene_pt = event.scenePos()
        items = self.__vnpy_chart.scene().items(scene_pt)
        for i in items:
            if isinstance(i, pg.PlotItem):
                view = i.getViewBox()
                view_pt = view.mapSceneToView(scene_pt)
                self.popup_memo_editor(view_pt.x())
                break
        # print("Plots:" + str([x for x in items if isinstance(x, pg.PlotItem)]))

    def on_timer(self):
        # Workaround for click event double fire
        self.__accepted = False

    def on_button_memo(self):
        enable = self.__check_memo.isChecked()

    def on_button_volume(self):
        enable = self.on_button_volume.isChecked()

    def on_button_ensure(self):
        input_securities = self.__combo_name.currentText()
        if '|' in input_securities:
            input_securities = input_securities.split('|')[0].strip()

        if self.__radio_adj_tail.isChecked():
            adjust_method = StockHistoryUi.ADJUST_TAIL
        elif self.__radio_adj_head.isChecked():
            adjust_method = StockHistoryUi.ADJUST_HEAD
        elif self.__radio_adj_none.isChecked():
            adjust_method = StockHistoryUi.ADJUST_NONE
        else:
            adjust_method = StockHistoryUi.ADJUST_NONE

        if self.__radio_log_return.isChecked():
            return_style = StockHistoryUi.RETURN_LOG
        elif self.__radio_simple_return.isChecked():
            return_style = StockHistoryUi.RETURN_SIMPLE
        else:
            return_style = StockHistoryUi.RETURN_SIMPLE

        if not self.load_security_data(input_securities, adjust_method,
                                       return_style):
            QMessageBox.information(
                self, QtCore.QCoreApplication.translate('History', '没有数据'),
                QtCore.QCoreApplication.translate('History',
                                                  '没有交易数据,请检查证券编码或更新本地数据'),
                QMessageBox.Ok, QMessageBox.Ok)
        else:
            self.load_security_memo()
            self.__vnpy_chart.refresh_history()

    def on_editor_closed(self, event):
        self.load_security_memo()
        self.__vnpy_chart.refresh_history()

    def popup_memo_editor(self, ix: float):
        bar_data = self.__vnpy_chart.get_bar_manager().get_bar(ix)
        if bar_data is not None:
            _time = bar_data.datetime.to_pydatetime()
            self.popup_memo_editor_by_time(_time)
        else:
            self.popup_memo_editor_by_time(now())

    def popup_memo_editor_by_time(self, _time: datetime):
        self.__memo_editor.select_memo_by_time(_time)
        # self.__memo_editor.show()
        self.__memo_editor.exec()

    def load_security_data(self, securities: str, adjust_method: int,
                           return_style: int) -> bool:
        data_utility = self.__sas.get_data_hub_entry().get_data_utility()
        index_dict = data_utility.get_support_index()

        if securities != self.__paint_securities or self.__paint_trade_data is None:
            if securities in index_dict.keys():
                uri = 'TradeData.Index.Daily'
            else:
                uri = 'TradeData.Stock.Daily'
            trade_data = self.__sas.get_data_hub_entry().get_data_center(
            ).query(uri, securities)

            # base_path = os.path.dirname(os.path.abspath(__file__))
            # history_path = os.path.join(base_path, 'History')
            # depot_path = os.path.join(history_path, 'depot')
            # his_file = os.path.join(depot_path, securities + '.his')

            # self.__memo_file = his_file
            self.__paint_trade_data = trade_data
            self.__paint_securities = securities

        if self.__paint_trade_data is None or len(
                self.__paint_trade_data) == 0:
            return False

        trade_data = pd.DataFrame()
        if adjust_method == StockHistoryUi.ADJUST_TAIL and 'adj_factor' in self.__paint_trade_data.columns:
            trade_data['open'] = self.__paint_trade_data[
                'open'] * self.__paint_trade_data['adj_factor']
            trade_data['close'] = self.__paint_trade_data[
                'close'] * self.__paint_trade_data['adj_factor']
            trade_data['high'] = self.__paint_trade_data[
                'high'] * self.__paint_trade_data['adj_factor']
            trade_data['low'] = self.__paint_trade_data[
                'low'] * self.__paint_trade_data['adj_factor']
        elif adjust_method == StockHistoryUi.ADJUST_HEAD and 'adj_factor' in self.__paint_trade_data.columns:
            trade_data['open'] = self.__paint_trade_data[
                'open'] / self.__paint_trade_data['adj_factor']
            trade_data['close'] = self.__paint_trade_data[
                'close'] / self.__paint_trade_data['adj_factor']
            trade_data['high'] = self.__paint_trade_data[
                'high'] / self.__paint_trade_data['adj_factor']
            trade_data['low'] = self.__paint_trade_data[
                'low'] / self.__paint_trade_data['adj_factor']
        else:
            trade_data['open'] = self.__paint_trade_data['open']
            trade_data['close'] = self.__paint_trade_data['close']
            trade_data['high'] = self.__paint_trade_data['high']
            trade_data['low'] = self.__paint_trade_data['low']
        trade_data['amount'] = self.__paint_trade_data['amount']
        trade_data['trade_date'] = pd.to_datetime(
            self.__paint_trade_data['trade_date'])

        if return_style == StockHistoryUi.RETURN_LOG:
            trade_data['open'] = np.log(trade_data['open'])
            trade_data['close'] = np.log(trade_data['close'])
            trade_data['high'] = np.log(trade_data['high'])
            trade_data['low'] = np.log(trade_data['low'])

        bars = self.df_to_bar_data(trade_data, securities)
        self.__vnpy_chart.get_bar_manager().update_history(bars)

        return True

    def load_security_memo(self) -> bool:
        self.__memo_editor.select_stock(self.__paint_securities)

        memo_record = self.__memo_recordset.get_record(self.__paint_securities)
        bar_manager = self.__vnpy_chart.get_bar_manager()

        if memo_record is None or bar_manager is None:
            return False

        try:
            memo_df = memo_record.get_records().copy()
            memo_df['normalised_time'] = memo_df['time'].dt.normalize()
            memo_df_grouped = memo_df.groupby('normalised_time')
        except Exception as e:
            return False
        finally:
            pass

        max_memo_count = 0
        for group_time, group_df in memo_df_grouped:
            memo_count = len(group_df)
            max_memo_count = max(memo_count, max_memo_count)
            bar_manager.set_item_data(group_time, 'memo', group_df)
        bar_manager.set_item_data_range('memo', 0.0, max_memo_count)

        # memo_item = self.__vnpy_chart.get_item('memo')
        # if memo_item is not None:
        #     memo_item.refresh_history()
        # self.__vnpy_chart.update_history()

        return True

    # TODO: Move it to common place
    @staticmethod
    def df_to_bar_data(df: pd.DataFrame,
                       securities: str,
                       exchange: Exchange = Exchange.SSE) -> [BarData]:
        # 98 ms
        bars = []
        for trade_date, amount, open, close, high, low in \
                zip(df['trade_date'], df['amount'], df['open'], df['close'], df['high'], df['low']):
            bar = BarData(datetime=trade_date,
                          exchange=exchange,
                          symbol=securities)

            bar.interval = Interval.DAILY
            bar.volume = amount * 10000
            bar.open_interest = 0
            bar.open_price = open
            bar.high_price = high
            bar.low_price = low
            bar.close_price = close
            bars.append(bar)
        return bars
Ejemplo n.º 9
0
class StockMemoEditor(QDialog):
    def __init__(self,
                 sas: StockAnalysisSystem,
                 memo_record: RecordSet,
                 parent: QWidget = None):
        super(StockMemoEditor, self).__init__(parent)

        self.__sas = sas
        self.__memo_recordset = memo_record

        self.__current_stock = None
        self.__current_index = None
        self.__current_record: Record = None

        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_stock = SecuritiesSelector(data_utility)
        self.__table_memo_index = EasyQTableWidget()

        self.__datetime_time = QDateTimeEdit(QDateTime().currentDateTime())
        self.__line_brief = QLineEdit()
        self.__text_record = QTextEdit()

        self.__button_new = QPushButton('新建')
        self.__button_apply = QPushButton('保存')

        self.init_ui()
        self.config_ui()

    def init_ui(self):
        root_layout = QHBoxLayout()
        self.setLayout(root_layout)

        group_box, group_layout = create_v_group_box('')
        group_layout.addWidget(self.__combo_stock, 1)
        group_layout.addWidget(self.__table_memo_index, 99)
        root_layout.addWidget(group_box, 4)

        group_box, group_layout = create_v_group_box('')
        group_layout.addLayout(
            horizon_layout(
                [QLabel('时间:'), self.__datetime_time, self.__button_new],
                [1, 99, 1]))
        group_layout.addLayout(
            horizon_layout([QLabel('摘要:'), self.__line_brief], [1, 99]))
        group_layout.addWidget(self.__text_record)
        group_layout.addLayout(
            horizon_layout([QLabel(''), self.__button_apply], [99, 1]))
        root_layout.addWidget(group_box, 6)

        self.setMinimumSize(500, 600)

    def config_ui(self):
        self.setWindowTitle('笔记编辑器')
        self.__datetime_time.setCalendarPopup(True)

        self.__combo_stock.setEditable(False)
        self.__combo_stock.currentIndexChanged.connect(
            self.on_combo_select_changed)

        self.__table_memo_index.insertColumn(0)
        self.__table_memo_index.insertColumn(0)
        self.__table_memo_index.setHorizontalHeaderLabels(['时间', '摘要'])
        self.__table_memo_index.setSelectionMode(
            QAbstractItemView.SingleSelection)
        self.__table_memo_index.setSelectionBehavior(
            QAbstractItemView.SelectRows)
        self.__table_memo_index.itemSelectionChanged.connect(
            self.on_table_selection_changed)
        self.__table_memo_index.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)

        self.__button_new.clicked.connect(self.on_button_new)
        self.__button_apply.clicked.connect(self.on_button_apply)

        # self.setWindowFlags(
        #     QtCore.Qt.Window |
        #     QtCore.Qt.CustomizeWindowHint |
        #     QtCore.Qt.WindowTitleHint |
        #     QtCore.Qt.WindowCloseButtonHint |
        #     QtCore.Qt.WindowStaysOnTopHint
        # )

    def on_button_new(self):
        if self.__current_stock is None:
            QMessageBox.information(self, '错误', '请选择需要做笔记的股票', QMessageBox.Ok,
                                    QMessageBox.Ok)
        self.create_new_memo(None)

    def on_button_apply(self):
        _time = self.__datetime_time.dateTime().toPyDateTime()
        brief = self.__line_brief.text()
        content = self.__text_record.toPlainText()

        if not str_available(brief):
            QMessageBox.information(self, '错误', '请至少填写笔记摘要', QMessageBox.Ok,
                                    QMessageBox.Ok)
            return

        if self.__current_index is not None:
            ret = self.__current_record.update_record(self.__current_index,
                                                      _time, brief, content,
                                                      True)
        else:
            ret, index = self.__current_record.add_record(
                _time, brief, content, 'memo', True)
            self.__current_index = index
        if ret:
            self.__reload_stock_memo()

    def on_combo_select_changed(self):
        input_securities = self.__combo_stock.get_input_securities()
        self.__load_stock_memo(input_securities)

    def on_table_selection_changed(self):
        if self.__current_record is None:
            return
        sel_index = self.__table_memo_index.GetCurrentIndex()
        if sel_index < 0:
            return
        sel_item = self.__table_memo_index.item(sel_index, 0)
        if item is None:
            return
        df_index = sel_item.data(Qt.UserRole)
        self.__load_memo_by_index(df_index)

    def select_stock(self, stock_identity: str):
        index = self.__combo_stock.findData(stock_identity)
        if index != -1:
            print('Select combox index: %s' % index)
            self.__combo_stock.setCurrentIndex(index)
        else:
            print('No index in combox for %s' % stock_identity)
            self.__combo_stock.setCurrentIndex(-1)
            self.__load_stock_memo(stock_identity)

    def select_memo_by_time(self, _time: datetime.datetime):
        if self.__current_record is None or self.__current_record.is_empty():
            self.create_new_memo(_time)
            return

        df = self.__current_record.get_records('memo')
        time_serial = df['time'].dt.normalize()
        select_df = df[time_serial == _time.replace(
            hour=0, minute=0, second=0, microsecond=0)]

        select_index = None
        for index, row in select_df.iterrows():
            select_index = index
            break

        if select_index is not None:
            self.select_memo_by_index(select_index)
        else:
            self.create_new_memo(_time)

    def select_memo_by_index(self, index: int):
        for row in range(0, self.__table_memo_index.rowCount()):
            table_item = self.__table_memo_index.item(row, 0)
            row_index = table_item.data(Qt.UserRole)
            if row_index == index:
                self.__table_memo_index.selectRow(row)
                break

    def create_new_memo(self, _time: datetime.datetime):
        self.__table_memo_index.clearSelection()
        if _time is not None:
            self.__datetime_time.setDateTime(_time)
        self.__line_brief.setText('')
        self.__text_record.setText('')
        self.__current_index = None

    # -------------------------------------------------------------------

    def __load_stock_memo(self, stock_identity: str):
        print('Load stock memo for %s' % stock_identity)

        self.__table_memo_index.clear()
        self.__table_memo_index.setRowCount(0)
        self.__table_memo_index.setHorizontalHeaderLabels(['时间', '摘要'])

        self.__current_stock = stock_identity
        self.__current_record = None
        self.__current_index = None

        if self.__memo_recordset is None or \
                self.__current_stock is None or self.__current_stock == '':
            return

        self.__current_record = self.__memo_recordset.get_record(
            stock_identity)
        df = self.__current_record.get_records('memo')

        select_index = None
        for index, row in df.iterrows():
            if select_index is None:
                select_index = index
            self.__table_memo_index.AppendRow(
                [datetime2text(row['time']), row['brief']], index)
        self.select_memo_by_index(select_index)

    def __reload_stock_memo(self):
        self.__line_brief.setText('')
        self.__text_record.setText('')
        self.__table_memo_index.clear()
        self.__table_memo_index.setRowCount(0)
        self.__table_memo_index.setHorizontalHeaderLabels(['时间', '摘要'])

        if self.__current_record is None:
            print('Warning: Current record is None, cannot reload.')
            return
        df = self.__current_record.get_records('memo')

        select_index = self.__current_index
        for index, row in df.iterrows():
            if select_index is None:
                select_index = index
            self.__table_memo_index.AppendRow(
                [datetime2text(row['time']), row['brief']], index)
        self.select_memo_by_index(select_index)

    def __load_memo_by_index(self, index: int):
        self.__table_memo_index.clearSelection()
        if self.__current_record is None or index is None:
            return

        df = self.__current_record.get_records('memo')
        s = df.loc[index]
        if len(s) == 0:
            return
        self.__current_index = index

        _time = s['time']
        brief = s['brief']
        content = s['content']

        self.__datetime_time.setDateTime(to_py_datetime(_time))
        self.__line_brief.setText(brief)
        self.__text_record.setText(content)
Ejemplo n.º 10
0
class StockChartUi(QWidget):
    ADJUST_TAIL = 0
    ADJUST_HEAD = 1
    ADJUST_NONE = 2

    RETURN_LOG = 3
    RETURN_SIMPLE = 4

    def __init__(self, memo_data: StockMemoData):
        super(StockChartUi, self).__init__()

        self.__memo_data = memo_data
        self.__memo_data.add_observer(self)

        self.__sas = memo_data.get_sas()
        self.__memo_record: StockMemoRecord = memo_data.get_memo_record()

        self.__in_edit_mode = True
        self.__paint_securities = ''
        self.__paint_trade_data = None

        # # Record
        # user_path = os.path.expanduser('~')
        # project_path = sas.get_project_path() if sas is not None else os.getcwd()
        #
        # self.__root_path = \
        #     memo_path_from_project_path(project_path) if user_path == '' else \
        #     memo_path_from_user_path(user_path)
        # self.__memo_record = Record(os.path.join(self.__root_path, 'stock_memo.csv'), STOCK_MEMO_COLUMNS)

        # vnpy chart
        self.__vnpy_chart = ChartWidget()

        # Memo editor
        self.__memo_editor: StockMemoEditor = self.__memo_data.get_data(
            'editor')

        # Timer for workaround signal fired twice
        self.__accepted = False
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_name = SecuritiesSelector(data_utility)
        self.__button_ensure = QPushButton('确定')

        self.__check_abs = QCheckBox('固定价格范围')
        self.__check_memo = QCheckBox('笔记')
        self.__check_volume = QCheckBox('成交量')

        self.__radio_adj_tail = QRadioButton('后复权')
        self.__radio_adj_head = QRadioButton('前复权')
        self.__radio_adj_none = QRadioButton('不复权')
        self.__group_adj = QButtonGroup(self)

        self.__radio_log_return = QRadioButton('对数收益')
        self.__radio_simple_return = QRadioButton('算术收益')
        self.__group_return = QButtonGroup(self)

        self.__init_ui()
        self.__config_ui()

    def __init_ui(self):
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        self.__group_adj.addButton(self.__radio_adj_tail)
        self.__group_adj.addButton(self.__radio_adj_head)
        self.__group_adj.addButton(self.__radio_adj_none)
        group_box_adj, group_layout = create_h_group_box('')
        group_layout.addWidget(self.__radio_adj_tail)
        group_layout.addWidget(self.__radio_adj_head)
        group_layout.addWidget(self.__radio_adj_none)

        self.__group_return.addButton(self.__radio_log_return)
        self.__group_return.addButton(self.__radio_simple_return)
        group_box_return, group_layout = create_h_group_box('')
        group_layout.addWidget(self.__radio_log_return)
        group_layout.addWidget(self.__radio_simple_return)

        group_box, group_layout = create_v_group_box('Securities')

        main_layout.addWidget(self.__vnpy_chart, 99)
        main_layout.addWidget(group_box)

        group_layout.addLayout(
            horizon_layout([
                self.__combo_name, group_box_adj, group_box_return,
                self.__button_ensure, self.__check_abs, self.__check_volume,
                self.__check_memo
            ]))

    def __config_ui(self):
        data_utility = self.__sas.get_data_hub_entry().get_data_utility()
        index_dict = data_utility.get_support_index()

        self.__combo_name.setEditable(True)
        for key in index_dict:
            self.__combo_name.addItem(key + ' | ' + index_dict.get(key), key)

        self.__radio_adj_none.setChecked(True)
        self.__radio_simple_return.setChecked(True)

        self.__check_abs.clicked.connect(self.on_button_abs)
        self.__check_memo.clicked.connect(self.on_button_memo)
        self.__check_volume.clicked.connect(self.on_button_volume)
        self.__button_ensure.clicked.connect(self.on_button_ensure)

        self.setMinimumWidth(1280)
        self.setMinimumHeight(800)

        # --------------------- Editor ----------------------

        # self.__memo_editor.closeEvent = self.on_editor_closed

        # ---------------------- Chart ----------------------

        self.__vnpy_chart.add_plot("candle", hide_x_axis=True)
        self.__vnpy_chart.add_plot("volume", maximum_height=200)
        self.__vnpy_chart.add_plot("memo", maximum_height=50)

        self.__vnpy_chart.add_item(CandleItem, "candle", "candle")
        self.__vnpy_chart.add_item(VolumeItem, "volume", "volume")
        self.__vnpy_chart.add_item(MemoItem, "memo", "memo")

        self.__vnpy_chart.add_cursor()
        self.__vnpy_chart.scene().sigMouseClicked.connect(
            self.on_chart_clicked)

    @pyqtSlot(MouseClickEvent)
    def on_chart_clicked(self, event: MouseClickEvent):
        if not event.double or self.__accepted:
            return
        self.__accepted = True
        scene_pt = event.scenePos()
        items = self.__vnpy_chart.scene().items(scene_pt)
        for i in items:
            if isinstance(i, pg.PlotItem):
                view = i.getViewBox()
                view_pt = view.mapSceneToView(scene_pt)
                self.popup_memo_editor(view_pt.x())
                break
        # print("Plots:" + str([x for x in items if isinstance(x, pg.PlotItem)]))

    def on_timer(self):
        # Workaround for click event double fire
        self.__accepted = False

    def on_button_abs(self):
        enable = self.__check_abs.isChecked()
        self.__vnpy_chart.get_item('candle').set_y_range_dynamic(not enable)
        self.__vnpy_chart.refresh_history()
        self.__vnpy_chart.update()

    def on_button_memo(self):
        # enable = self.__check_memo.isChecked()
        # self.__vnpy_chart.enable_item('memo', enable)
        pass

    def on_button_volume(self):
        # enable = self.__check_volume.isChecked()
        # self.__vnpy_chart.enable_item('volume', enable)
        pass

    def on_button_ensure(self):
        input_securities = self.__combo_name.currentText()
        if '|' in input_securities:
            input_securities = input_securities.split('|')[0].strip()
        self.show_security(input_securities)

    # ------------------- Interface of StockMemoData.Observer --------------------

    # def on_memo_updated(self):
    #     self.load_security_memo()
    #     self.__vnpy_chart.refresh_history()

    def on_data_updated(self, name: str, data: any):
        nop(data)
        if self.__in_edit_mode and name == 'memo_record':
            self.load_security_memo()
            self.__vnpy_chart.refresh_history()
            self.__in_edit_mode = False

    # ----------------------------------------------------------------------------

    def show_security(self, security: str, check_update: bool = False):
        if self.__radio_adj_tail.isChecked():
            adjust_method = StockChartUi.ADJUST_TAIL
        elif self.__radio_adj_head.isChecked():
            adjust_method = StockChartUi.ADJUST_HEAD
        elif self.__radio_adj_none.isChecked():
            adjust_method = StockChartUi.ADJUST_NONE
        else:
            adjust_method = StockChartUi.ADJUST_NONE

        if self.__radio_log_return.isChecked():
            return_style = StockChartUi.RETURN_LOG
        elif self.__radio_simple_return.isChecked():
            return_style = StockChartUi.RETURN_SIMPLE
        else:
            return_style = StockChartUi.RETURN_SIMPLE

        if not self.load_security_data(security, adjust_method, return_style,
                                       check_update):
            QMessageBox.information(
                self, QtCore.QCoreApplication.translate('History', '没有数据'),
                QtCore.QCoreApplication.translate('History',
                                                  '没有交易数据,请检查证券编码或更新本地数据'),
                QMessageBox.Ok, QMessageBox.Ok)
        else:
            self.load_security_memo()
            self.__vnpy_chart.refresh_history()

    def popup_memo_editor(self, ix: float):
        bar_data = self.__vnpy_chart.get_bar_manager().get_bar(ix)
        if bar_data is not None:
            _time = bar_data.datetime.to_pydatetime()
            self.popup_memo_editor_by_time(_time)
        else:
            self.popup_memo_editor_by_time(now())
        self.__in_edit_mode = True

    def popup_memo_editor_by_time(self, _time: datetime):
        self.__memo_editor.select_security(self.__paint_securities)
        self.__memo_editor.select_memo_by_day(_time)
        # self.__memo_editor.show()
        self.__memo_editor.exec()

    def load_security_data(self,
                           securities: str,
                           adjust_method: int,
                           return_style: int,
                           check_update: bool = False) -> bool:
        data_utility = self.__sas.get_data_hub_entry().get_data_utility()
        index_dict = data_utility.get_support_index()

        # if securities != self.__paint_securities or self.__paint_trade_data is None:

        if securities in index_dict.keys():
            uri = 'TradeData.Index.Daily'
        else:
            uri = 'TradeData.Stock.Daily'

        with futures.ThreadPoolExecutor(max_workers=2) as executor:
            # Code is beautiful.
            # But the un-canceled network (update task) will still block the main thread.
            # What the hell...

            if check_update:
                future_update: futures.Future = executor.submit(
                    self.__update_security_data, uri, securities)
                WaitingWindow.wait_future(
                    '检查更新数据中...\n'
                    '取消则使用离线数据\n'
                    '注意:取消更新后在网络连接超时前界面仍可能卡住,这或许是Python的机制导致\n'
                    '如果想禁用自动更新功能请删除StockChartUi.py中check_update相关的代码',
                    future_update, None)

            future_load_calc: futures.Future = executor.submit(
                self.__load_calc_security_data, uri, securities, adjust_method,
                return_style)
            if not WaitingWindow.wait_future('载入数据中...', future_load_calc,
                                             None):
                return False
            trade_data = future_load_calc.result(timeout=0)

        if trade_data is None:
            return False

        bars = self.df_to_bar_data(trade_data, securities)
        self.__vnpy_chart.get_bar_manager().clear_all()
        self.__vnpy_chart.get_bar_manager().update_history(bars)

        return True

    def load_security_memo(self) -> bool:
        self.__combo_name.select_security(self.__paint_securities, True)

        memo_record = self.__memo_record.get_stock_memos(
            self.__paint_securities)
        bar_manager = self.__vnpy_chart.get_bar_manager()

        if memo_record is None or bar_manager is None:
            return False

        try:
            memo_df = memo_record.copy()
            memo_df['normalised_time'] = memo_df['time'].dt.normalize()
            memo_df_grouped = memo_df.groupby('normalised_time')
        except Exception as e:
            return False
        finally:
            pass

        append_items = []
        max_memo_count = 0
        for group_time, group_df in memo_df_grouped:
            memo_count = len(group_df)
            max_memo_count = max(memo_count, max_memo_count)
            if not bar_manager.set_item_data(group_time, 'memo', group_df):
                bar = BarData(datetime=group_time,
                              exchange=Exchange.SSE,
                              symbol=self.__paint_securities)
                bar.extra = {'memo': group_df}
                append_items.append(bar)
        bar_manager.set_item_data_range('memo', 0.0, max_memo_count)
        bar_manager.update_history(append_items)

        # memo_item = self.__vnpy_chart.get_item('memo')
        # if memo_item is not None:
        #     memo_item.refresh_history()
        # self.__vnpy_chart.update_history()

        return True

    def __update_security_data(self, uri: str, securities: str):
        self.__sas.get_data_hub_entry().get_data_utility().check_update(
            uri, securities)

    def __load_calc_security_data(self, uri: str, securities: str,
                                  adjust_method: int, return_style: int):
        trade_data = self.__sas.get_data_hub_entry().get_data_center().query(
            uri, securities)

        self.__paint_trade_data = trade_data
        self.__paint_securities = securities

        if self.__paint_trade_data is None or len(
                self.__paint_trade_data) == 0:
            return None

        trade_data = pd.DataFrame()
        if adjust_method == StockChartUi.ADJUST_TAIL and 'adj_factor' in self.__paint_trade_data.columns:
            trade_data['open'] = self.__paint_trade_data[
                'open'] * self.__paint_trade_data['adj_factor']
            trade_data['close'] = self.__paint_trade_data[
                'close'] * self.__paint_trade_data['adj_factor']
            trade_data['high'] = self.__paint_trade_data[
                'high'] * self.__paint_trade_data['adj_factor']
            trade_data['low'] = self.__paint_trade_data[
                'low'] * self.__paint_trade_data['adj_factor']
        elif adjust_method == StockChartUi.ADJUST_HEAD and 'adj_factor' in self.__paint_trade_data.columns:
            trade_data['open'] = self.__paint_trade_data[
                'open'] / self.__paint_trade_data['adj_factor']
            trade_data['close'] = self.__paint_trade_data[
                'close'] / self.__paint_trade_data['adj_factor']
            trade_data['high'] = self.__paint_trade_data[
                'high'] / self.__paint_trade_data['adj_factor']
            trade_data['low'] = self.__paint_trade_data[
                'low'] / self.__paint_trade_data['adj_factor']
        else:
            trade_data['open'] = self.__paint_trade_data['open']
            trade_data['close'] = self.__paint_trade_data['close']
            trade_data['high'] = self.__paint_trade_data['high']
            trade_data['low'] = self.__paint_trade_data['low']
        trade_data['amount'] = self.__paint_trade_data['amount']
        trade_data['trade_date'] = pd.to_datetime(
            self.__paint_trade_data['trade_date'])

        if return_style == StockChartUi.RETURN_LOG:
            trade_data['open'] = np.log(trade_data['open'])
            trade_data['close'] = np.log(trade_data['close'])
            trade_data['high'] = np.log(trade_data['high'])
            trade_data['low'] = np.log(trade_data['low'])

        return trade_data

    # TODO: Move it to common place
    @staticmethod
    def df_to_bar_data(df: pd.DataFrame,
                       securities: str,
                       exchange: Exchange = Exchange.SSE) -> [BarData]:
        # 98 ms
        bars = []
        for trade_date, amount, open, close, high, low in \
                zip(df['trade_date'], df['amount'], df['open'], df['close'], df['high'], df['low']):
            bar = BarData(datetime=trade_date,
                          exchange=exchange,
                          symbol=securities)

            bar.interval = Interval.DAILY
            bar.volume = amount * 10000
            bar.open_interest = 0
            bar.open_price = open
            bar.high_price = high
            bar.low_price = low
            bar.close_price = close
            bars.append(bar)
        return bars
Ejemplo n.º 11
0
class StockMemoEditor(QDialog):
    LIST_HEADER = ['Time', 'Preview']

    # class Observer:
    #     def __init__(self):
    #         pass
    #
    #     def on_memo_updated(self):
    #         pass

    def __init__(self, memo_data: StockMemoData, parent: QWidget = None):
        self.__memo_data = memo_data
        super(StockMemoEditor, self).__init__(parent)

        # The filter of left list
        # self.__filter_identity: str = ''
        # self.__filter_datetime: datetime.datetime = None

        # The stock that selected by combobox or outer setting
        self.__current_stock = None

        # The current index of editing memo
        # Not None: Update exists
        # None: Create new
        self.__current_index = None

        # The memo list that displaying in the left list
        self.__current_memos: pd.DataFrame = None

        self.__observers = []

        self.__sas = self.__memo_data.get_sas(
        ) if self.__memo_data is not None else None
        self.__memo_record = self.__memo_data.get_memo_record(
        ) if self.__memo_data is not None else None

        data_utility = self.__sas.get_data_hub_entry().get_data_utility(
        ) if self.__sas is not None else None
        self.__combo_stock = SecuritiesSelector(data_utility)
        self.__table_memo_index = EasyQTableWidget()

        self.__datetime_time = QDateTimeEdit(QDateTime().currentDateTime())
        self.__line_brief = QLineEdit()
        self.__text_record = QTextEdit()

        self.__button_new = QPushButton('New')
        self.__button_apply = QPushButton('Save')
        self.__button_delete = QPushButton('Delete')

        self.init_ui()
        self.config_ui()

    def init_ui(self):
        root_layout = QHBoxLayout()
        self.setLayout(root_layout)

        group_box, group_layout = create_v_group_box('')
        group_layout.addWidget(self.__combo_stock, 1)
        group_layout.addWidget(self.__table_memo_index, 99)
        group_layout.addWidget(self.__button_delete, 1)
        root_layout.addWidget(group_box, 4)

        group_box, group_layout = create_v_group_box('')
        group_layout.addLayout(
            horizon_layout(
                [QLabel('Time:'), self.__datetime_time, self.__button_new],
                [1, 99, 1]))
        group_layout.addLayout(
            horizon_layout([QLabel('Brief:'), self.__line_brief], [1, 99]))
        group_layout.addLayout(
            horizon_layout([QLabel('Content:'), QLabel('')], [1, 99]))
        group_layout.addWidget(self.__text_record)
        group_layout.addLayout(
            horizon_layout([QLabel(''), self.__button_apply], [99, 1]))
        root_layout.addWidget(group_box, 6)

        self.setMinimumSize(500, 600)

    def config_ui(self):
        self.setWindowTitle('Memo Editor')
        self.__datetime_time.setCalendarPopup(True)

        self.__combo_stock.setEditable(False)
        self.__combo_stock.currentIndexChanged.connect(
            self.on_combo_select_changed)

        self.__table_memo_index.insertColumn(0)
        self.__table_memo_index.insertColumn(0)
        self.__table_memo_index.setHorizontalHeaderLabels(
            StockMemoEditor.LIST_HEADER)
        self.__table_memo_index.setSelectionMode(
            QAbstractItemView.SingleSelection)
        self.__table_memo_index.setSelectionBehavior(
            QAbstractItemView.SelectRows)
        self.__table_memo_index.itemSelectionChanged.connect(
            self.on_table_selection_changed)
        self.__table_memo_index.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)

        self.__button_new.clicked.connect(self.on_button_new)
        self.__button_apply.clicked.connect(self.on_button_apply)
        self.__button_delete.clicked.connect(self.on_button_delete)

        # self.setWindowFlags(
        #     QtCore.Qt.Window |
        #     QtCore.Qt.CustomizeWindowHint |
        #     QtCore.Qt.WindowTitleHint |
        #     QtCore.Qt.WindowCloseButtonHint |
        #     QtCore.Qt.WindowStaysOnTopHint
        # )

    def on_button_new(self):
        if not str_available(self.__current_stock):
            QMessageBox.information(self, '错误', '请选择需要做笔记的股票', QMessageBox.Ok,
                                    QMessageBox.Ok)
        self.create_new_memo(None)
        # self.__trigger_memo_updated()

    # ['time', 'security', 'brief', 'content', 'classify']

    def on_button_apply(self):
        _time = self.__datetime_time.dateTime().toPyDateTime()
        brief = self.__line_brief.text()
        content = self.__text_record.toPlainText()

        if not str_available(brief):
            QMessageBox.information(self, '错误', '请至少填写笔记摘要', QMessageBox.Ok,
                                    QMessageBox.Ok)
            return

        if self.__current_index is not None:
            ret = self.__memo_record.update_record(
                self.__current_index, {
                    'time': _time,
                    'brief': brief,
                    'content': content,
                    'classify': 'memo',
                }, True)
        else:
            if str_available(self.__current_stock):
                ret, index = self.__memo_record.add_record(
                    {
                        'time': _time,
                        'security': self.__current_stock,
                        'brief': brief,
                        'content': content,
                        'classify': 'memo',
                    }, True)
                self.__current_index = index
            else:
                ret = False
                QMessageBox.information(self, '错误', '没有选择做笔记的股票',
                                        QMessageBox.Ok, QMessageBox.Ok)
        if ret:
            self.load_security_memo(self.__current_stock)

            if self.__current_index is not None:
                self.select_memo_by_memo_index(self.__current_index)
            else:
                self.select_memo_by_list_index(0)

        # self.__trigger_memo_updated()
        self.__memo_data.broadcast_data_updated('memo_record')

    def on_button_delete(self):
        if self.__current_index is not None:
            self.__memo_record.del_records(self.__current_index)
            self.__memo_record.save()
            self.load_security_memo(self.__current_stock)
            self.update_memo_list()
            self.__memo_data.broadcast_data_updated('memo_record')

    def on_combo_select_changed(self):
        input_securities = self.__combo_stock.get_input_securities()
        if input_securities != self.__current_stock:
            self.load_security_memo(input_securities)

    def on_table_selection_changed(self):
        sel_index = self.__table_memo_index.GetCurrentIndex()
        if sel_index < 0:
            return
        sel_item = self.__table_memo_index.item(sel_index, 0)
        if sel_item is None:
            return
        df_index = sel_item.data(Qt.UserRole)
        self.load_edit_memo(df_index)

    # --------------------------------------------------------------------------------------------

    # def add_observer(self, ob: Observer):
    #     self.__observers.append(ob)
    #
    # def __trigger_memo_updated(self):
    #     for ob in self.__observers:
    #         ob.on_memo_updated()

    # ------------------------------------- Select Functions -------------------------------------
    #                     Will update UI control, which will trigger the linkage

    def select_security(self, security: str):
        self.__combo_stock.select_security(security, True)

    def select_memo_by_day(self, _time: datetime.datetime):
        if self.__current_memos is None or self.__current_memos.empty:
            self.create_new_memo(_time)
            return

        df = self.__current_memos
        time_serial = df['time'].dt.normalize()
        select_df = df[time_serial == _time.replace(
            hour=0, minute=0, second=0, microsecond=0)]

        select_index = None
        for index, row in select_df.iterrows():
            select_index = index
            break

        if select_index is not None:
            self.select_memo_by_memo_index(select_index)
        else:
            self.create_new_memo(_time)

    def select_memo_by_memo_index(self, memo_index: int):
        for row in range(0, self.__table_memo_index.rowCount()):
            row_memo_index = self.__table_memo_index.item(row,
                                                          0).data(Qt.UserRole)
            if row_memo_index == memo_index:
                self.__table_memo_index.selectRow(row)
                break

    def select_memo_by_list_index(self, list_index: int):
        if list_index >= 0:
            self.__table_memo_index.selectRow(list_index)
            self.__table_memo_index.scrollToItem(
                self.__table_memo_index.item(list_index, 0))
        else:
            self.__table_memo_index.clearSelection()
            self.create_new_memo(now())

    # ----------------------------------- Select and Load Logic -----------------------------------

    def load_security_memo(self, security: str):
        condition = {'classify': 'memo'}
        if str_available(security):
            condition['security'] = security
        df = self.__memo_record.get_records(condition)

        self.__current_memos = df
        self.__current_stock = security

        self.update_memo_list()

    def update_memo_list(self):
        self.__table_memo_index.clear()
        self.__table_memo_index.setRowCount(0)
        self.__table_memo_index.setHorizontalHeaderLabels(
            StockMemoEditor.LIST_HEADER)

        for index, row in self.__current_memos.iterrows():
            row_index = self.__table_memo_index.rowCount()

            brief = row['brief']
            content = row['content']
            text = brief if str_available(brief) else content
            # https://stackoverflow.com/a/2873416/12929244
            self.__table_memo_index.AppendRow([
                datetime2text(row['time']), text[:30] + (text[30:] and '...')
            ], index)
            self.__table_memo_index.item(row_index,
                                         0).setData(Qt.UserRole, index)

        self.__current_index = None
        self.__enter_new_mode()

    def create_new_memo(self, _time: datetime.datetime or None):
        self.__table_memo_index.clearSelection()
        if _time is not None:
            self.__datetime_time.setDateTime(_time)
        self.__line_brief.setText('')
        self.__text_record.setText('')
        self.__current_index = None
        self.__enter_new_mode()

    def load_edit_memo(self, index: int):
        """
        The index should exist in left memo list
        :param index:
        :return:
        """
        df = self.__current_memos
        if df is None or df.empty:
            self.create_new_memo()
            return

        s = df.loc[index]
        if len(s) == 0:
            self.create_new_memo()
            return

        self.__current_index = index

        _time = s['time']
        brief = s['brief']
        content = s['content']

        self.__datetime_time.setDateTime(to_py_datetime(_time))
        self.__line_brief.setText(str(brief))
        self.__text_record.setText(str(content))
        self.__enter_edit_mode()

    def __enter_new_mode(self):
        self.__button_new.setText('New *')

    def __enter_edit_mode(self):
        self.__button_new.setText('New')
Ejemplo n.º 12
0
class StockMemoDeck(QWidget):
    STATIC_HEADER = ['Code', 'Name']

    def __init__(self, memo_data: StockMemoData):
        super(StockMemoDeck, self).__init__()
        self.__memo_data = memo_data

        if self.__memo_data is not None:
            self.__memo_data.add_observer(self)

            self.__sas = self.__memo_data.get_sas()
            self.__memo_record: StockMemoRecord = self.__memo_data.get_memo_record(
            )
            self.__memo_editor: StockMemoEditor = self.__memo_data.get_data(
                'editor')
            self.__data_utility = self.__sas.get_data_hub_entry(
            ).get_data_utility() if self.__sas is not None else None
        else:
            # For layout debug
            self.__sas = None
            self.__memo_record = None
            self.__memo_editor = None
            self.__data_utility = None

        self.__memo_extras = []
        self.__list_securities = []
        self.__show_securities = []

        # ---------------- Page -----------------

        self.__page = 1
        self.__item_per_page = 20

        self.__button_first = QPushButton('|<')
        self.__button_prev = QPushButton('<')
        self.__spin_page = QSpinBox()
        self.__label_total_page = QLabel('/ 1')
        self.__button_jump = QPushButton('GO')
        self.__button_next = QPushButton('>')
        self.__button_last = QPushButton('>|')

        self.__button_reload = QPushButton('Reload')
        self.__check_show_black_list = QCheckBox('Black List')

        # --------------- Widgets ---------------

        self.__memo_table = TableViewEx()
        self.__stock_selector = \
            SecuritiesSelector(self.__data_utility) if self.__data_utility is not None else QComboBox()
        self.__line_path = QLineEdit(self.__memo_data.get_root_path() if self.
                                     __memo_data is not None else root_path)
        self.__info_panel = QLabel(NOTE)
        self.__button_new = QPushButton('New')
        self.__button_filter = QPushButton('Filter')
        self.__button_browse = QPushButton('Browse')
        # self.__button_black_list = QPushButton('Black List')

        self.__layout_extra = QHBoxLayout()

        self.init_ui()
        self.config_ui()

        self.show_securities(self.__memo_record.get_all_security() if self.
                             __memo_record is not None else [])

    def init_ui(self):
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        # Page control
        page_control_line = QHBoxLayout()
        page_control_line.addWidget(self.__button_reload, 1)
        page_control_line.addWidget(self.__check_show_black_list, 1)

        page_control_line.addWidget(QLabel(''), 99)
        page_control_line.addWidget(self.__button_first, 1)
        page_control_line.addWidget(self.__button_prev, 1)
        page_control_line.addWidget(self.__spin_page, 1)
        page_control_line.addWidget(self.__label_total_page, 1)
        page_control_line.addWidget(self.__button_jump, 1)
        page_control_line.addWidget(self.__button_next, 1)
        page_control_line.addWidget(self.__button_last, 1)

        group_box, group_layout = create_v_group_box('Stock Memo')
        group_layout.addWidget(self.__memo_table)
        group_layout.addLayout(page_control_line)
        main_layout.addWidget(group_box, 10)

        group_box, group_layout = create_h_group_box('Edit')
        right_area = QVBoxLayout()
        group_layout.addWidget(self.__info_panel, 4)
        group_layout.addLayout(right_area, 6)

        line = horizon_layout([
            QLabel('股票选择:'), self.__stock_selector, self.__button_filter,
            self.__button_new
        ], [1, 10, 1, 1])
        right_area.addLayout(line)

        line = horizon_layout(
            [QLabel('保存路径:'), self.__line_path, self.__button_browse],
            [1, 10, 1])
        right_area.addLayout(line)

        # line = horizon_layout([QLabel('其它功能:'), self.__button_black_list, QLabel('')],
        #                       [1, 1, 10])

        self.__layout_extra.addWidget(QLabel('其它功能:'))
        self.__layout_extra.addStretch()
        right_area.addLayout(self.__layout_extra)

        main_layout.addWidget(group_box, 1)

    def config_ui(self):
        self.setMinimumSize(800, 600)
        self.__info_panel.setWordWrap(True)

        # -------------- Page Control --------------

        self.__button_first.clicked.connect(
            partial(self.__on_page_control, '|<'))
        self.__button_prev.clicked.connect(partial(self.__on_page_control,
                                                   '<'))
        self.__button_jump.clicked.connect(partial(self.__on_page_control,
                                                   'g'))
        self.__button_next.clicked.connect(partial(self.__on_page_control,
                                                   '>'))
        self.__button_last.clicked.connect(
            partial(self.__on_page_control, '>|'))

        self.__button_reload.clicked.connect(self.__on_button_reload)

        self.__check_show_black_list.setChecked(False)
        self.__check_show_black_list.clicked.connect(
            self.__on_check_show_black_list)

        self.__memo_table.SetColumn(self.__memo_table_columns())
        self.__memo_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.__memo_table.setSelectionMode(QAbstractItemView.SingleSelection)
        self.__memo_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.__memo_table.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.__memo_table.doubleClicked.connect(
            self.__on_memo_item_double_clicked)

        self.__button_new.clicked.connect(self.__on_button_new_clicked)
        self.__button_browse.clicked.connect(self.__on_button_browse)
        self.__button_filter.clicked.connect(self.__on_button_filter_clicked)

        self.__button_filter.setDefault(True)
        line_editor = self.__stock_selector.lineEdit()
        if line_editor is not None:
            line_editor.returnPressed.connect(self.__on_button_filter_clicked)

    def add_memo_extra(self, extra: MemoExtra):
        extra.set_memo_ui(self)
        self.__memo_extras.append(extra)

        global_entry_text = extra.global_entry_text()
        if str_available(global_entry_text):
            button = QPushButton(global_entry_text)
            button.clicked.connect(
                partial(self.__on_button_global_entry, extra))
            self.__layout_extra.insertWidget(1, button)

    def update_list(self):
        self.__update_memo_securities_list()

    def show_securities(self, securities: [str]):
        self.__update_securities(securities)
        self.__update_memo_securities_list()

    # ------------------- Interface of StockMemoData.Observer --------------------

    # def on_memo_updated(self):
    #     self.update_list()

    def on_data_updated(self, name: str, data: any):
        nop(data)
        if name in ['memo_record', 'tags']:
            self.update_list()

    # ----------------------------------------------------------------------------

    def __on_page_control(self, button_mark: str):
        new_page = self.__page
        if button_mark == '|<':
            new_page = 0
        elif button_mark == '<':
            new_page -= 1
        elif button_mark == 'g':
            new_page = self.__spin_page.value()
        elif button_mark == '>':
            new_page += 1
        elif button_mark == '>|':
            new_page = self.__max_page()

        new_page = max(1, new_page)
        new_page = min(self.__max_page(), new_page)

        if self.__page != new_page:
            self.__page = new_page
            self.__spin_page.setValue(self.__page)
            self.update_list()

    def __on_button_browse(self):
        folder = str(
            QFileDialog.getExistingDirectory(
                self,
                "Select Private Folder",
                directory=self.__line_path.text()))
        if folder == '':
            return
        self.__line_path.setText(folder)

        # Save to system config
        self.__sas.get_config().set('memo_path', folder)
        self.__sas.get_config().save_config()

        # Update new path to memo extras
        self.__memo_data.set_root_path(folder)

        # TODO: Auto handle path update
        stock_tags: Tags = self.__memo_data.get_data('tags')
        if stock_tags is not None:
            stock_tags.load(os.path.join(folder, 'tags.json'))
        self.__memo_data.get_memo_record().load(
            os.path.join(folder, 'stock_memo.csv'))

    def __on_button_new_clicked(self):
        security = self.__stock_selector.get_input_securities()
        self.__memo_editor.select_security(security)
        self.__memo_editor.create_new_memo(now())
        self.__memo_editor.exec()

    def __on_button_filter_clicked(self):
        input_security = self.__stock_selector.get_input_securities()
        list_securities = self.__data_utility.guess_securities(input_security)
        self.show_securities(list_securities)

    def __on_button_reload(self):
        self.show_securities(self.__memo_record.get_all_security() if self.
                             __memo_record is not None else [])

    def __on_check_show_black_list(self):
        self.__update_securities()
        self.__update_memo_securities_list()

    def __on_memo_item_double_clicked(self, index: QModelIndex):
        item_data = index.data(Qt.UserRole)
        if item_data is not None and isinstance(item_data, tuple):
            memo_extra, security = item_data
            memo_extra.security_entry(security)
            # print('Double Click on memo item: %s - %s' % (memo_extra.title_text(), security))

    def __on_button_global_entry(self, extra: MemoExtra):
        extra.global_entry()

    def __update_memo_securities_list(self, securities: [str] or None = None):
        if securities is None:
            securities = self.__show_securities

        columns = self.__memo_table_columns()

        self.__memo_table.Clear()
        self.__memo_table.SetColumn(columns)

        index = []
        offset = (self.__page - 1) * self.__item_per_page

        for i in range(offset, offset + self.__item_per_page):
            if i < 0 or i >= len(securities):
                continue
            security = securities[i]
            index.append(str(i + 1))

            self.__memo_table.AppendRow([''] * len(columns))
            row_count = self.__memo_table.RowCount()
            row = row_count - 1

            col = 0
            self.__memo_table.SetItemText(row, col, security)
            self.__memo_table.SetItemData(row, col, security)

            col = 1
            self.__memo_table.SetItemText(
                row, col, self.__data_utility.stock_identity_to_name(security))

            for memo_extra in self.__memo_extras:
                if not str_available(memo_extra.title_text()):
                    continue

                col += 1
                text = memo_extra.security_entry_text(security)

                self.__memo_table.SetItemText(row, col, text)
                self.__memo_table.SetItemData(row, col, (memo_extra, security))

                # _item = self.__memo_table.GetItem(row, col)
                # _item.clicked.connect(partial(self.__on_memo_item_clicked, _item, security, memo_extra))

        model = self.__memo_table.model()
        model.setVerticalHeaderLabels(index)

    def __update_securities(self, list_securities: [str] or None = None):
        if list_securities is None:
            # Just refresh the show securities
            pass
        else:
            self.__list_securities = list_securities \
                if isinstance(list_securities, (list, tuple)) else [list_securities]
        self.__update_show_securities()
        self.__update_page_control()

    def __update_show_securities(self):
        self.__show_securities = self.__list_securities

        show_black_list = self.__check_show_black_list.isChecked()
        if not show_black_list:
            black_list: BlackList = self.__memo_data.get_data('black_list')
            if black_list is not None:
                black_list_securities = black_list.all_black_list()
                self.__show_securities = list(
                    set(self.__show_securities).difference(
                        set(black_list_securities)))

    def __memo_table_columns(self) -> [str]:
        return StockMemoDeck.STATIC_HEADER + [
            memo_extra.title_text() for memo_extra in self.__memo_extras
            if str_available(memo_extra.title_text())
        ]

    def __max_page(self) -> int:
        return (len(self.__show_securities) + self.__item_per_page -
                1) // self.__item_per_page

    def __update_page_control(self):
        self.__page = 1
        max_page = self.__max_page()
        self.__spin_page.setValue(1)
        self.__spin_page.setMinimum(1)
        self.__spin_page.setMaximum(max_page)
        self.__label_total_page.setText('/ %s' % max_page)