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__(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__(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__(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()
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()
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__(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()
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
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)
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
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')
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)