示例#1
0
    def __init__(self, sas_if: sasIF, task_manager):
        super(AnnouncementDownloaderUi, self).__init__()

        # ---------------- ext var ----------------

        self.__sas_if = sas_if
        # self.__data_center = self.__data_hub.get_data_center() if self.__data_hub is not None else None
        # self.__data_utility = self.__data_hub.get_data_utility() if self.__data_hub is not None else None
        self.__task_manager = task_manager
        self.__translate = QtCore.QCoreApplication.translate

        # Timer for update stock list
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        self.__combo_name = SecuritiesSelector(self.__sas_if, self)
        self.__radio_annual_report = QRadioButton('年报')
        self.__radio_customize_filter = QRadioButton('自定义')
        self.__line_filter_include = QLineEdit()
        self.__line_filter_exclude = QLineEdit()
        self.__button_download = QPushButton('确定')

        self.__datetime_since = QDateTimeEdit(
            QDateTime.currentDateTime().addYears(-3))
        self.__datetime_until = QDateTimeEdit(QDateTime.currentDateTime())

        self.init_ui()
    def __init__(self, memo_context: dict):
        self.__memo_context = memo_context
        # self.__memo_context.add_observer(self)
        self.__sas_if: sasIF = self.__memo_context.get('sas_if')
        super(StockChartUi, self).__init__()

        # self.__sas = memo_context.get_sas()
        # self.__memo_record: StockMemoRecord = memo_context.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_context.get('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(
            self.__sas_if) if self.__sas_if is not None else QComboBox()
        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_context: dict):
        self.__memo_context = memo_context
        self.__sas_if: sasIF = self.__memo_context.get(
            'sas_if') if self.__memo_context is not None else None
        super(StockMemoDeck, self).__init__()

        if self.__memo_context is not None:
            # self.__memo_context.add_observer(self)
            self.__sas_if: sasIF = self.__memo_context.get('sas_if')
            self.__memo_editor: StockMemoEditor = self.__memo_context.get(
                'editor')
        else:
            # For layout debug
            self.__sas_if: sasIF = None
            self.__memo_editor = 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.__sas_if) if self.__sas_if is not None else QComboBox()
        # TODO: Path from server
        self.__line_path = QLineEdit('')
        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.__sas_if.stock_memo_get_all_security(
        ) if self.__sas_if is not None else [])
        def __init__(self, sas_if: sasIF):
            self.__sas_if = sas_if
            super(BlackListUi.BlackListEditor, self).__init__()

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

            self.init_ui()
            self.config_ui()
    class BlackListEditor(QWidget):
        def __init__(self, sas_if: sasIF):
            self.__sas_if = sas_if
            super(BlackListUi.BlackListEditor, self).__init__()

            self.__stock_selector = SecuritiesSelector(
                sas_if) if sas_if 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()
示例#6
0
    def __init__(self, sas_if: sasIF):
        super(ChartLab, self).__init__()

        # ---------------- ext var ----------------

        self.__sas_if = sas_if
        self.__inited = False
        self.__plot_table = {}
        self.__paint_data = None

        # ------------- plot resource -------------

        self.__figure = plt.figure()
        self.__canvas = FigureCanvas(self.__figure)

        # -------------- ui resource --------------

        self.__data_frame_widget = None

        self.__combo_factor = QComboBox()
        self.__label_comments = QLabel('')

        # Parallel comparison
        self.__radio_parallel_comparison = QRadioButton('横向比较')
        self.__combo_year = QComboBox()
        self.__combo_quarter = QComboBox()
        self.__combo_industry = QComboBox()

        # Longitudinal comparison
        self.__radio_longitudinal_comparison = QRadioButton('纵向比较')
        self.__combo_stock = SecuritiesSelector(self.__sas_if)

        # Limitation
        self.__line_lower = QLineEdit('')
        self.__line_upper = QLineEdit('')

        self.__button_draw = QPushButton('绘图')
        self.__button_show = QPushButton('查看绘图数据')

        self.init_ui()
    def __init__(self, memo_context: dict, parent: QWidget = None):
        self.__memo_context = memo_context
        self.__sas_if: sasIF = self.__memo_context.get('sas_if')
        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_context.get_sas() if self.__memo_context is not None else None
        # self.__memo_record = self.__memo_context.get_memo_record() if self.__memo_context 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(self.__sas_if) if self.__sas_if is not None else QComboBox()
        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()
示例#8
0
class AnnouncementDownloaderUi(QWidget):
    def __init__(self, sas_if: sasIF, task_manager):
        super(AnnouncementDownloaderUi, self).__init__()

        # ---------------- ext var ----------------

        self.__sas_if = sas_if
        # self.__data_center = self.__data_hub.get_data_center() if self.__data_hub is not None else None
        # self.__data_utility = self.__data_hub.get_data_utility() if self.__data_hub is not None else None
        self.__task_manager = task_manager
        self.__translate = QtCore.QCoreApplication.translate

        # Timer for update stock list
        self.__timer = QTimer()
        self.__timer.setInterval(1000)
        self.__timer.timeout.connect(self.on_timer)
        self.__timer.start()

        # Ui component
        self.__combo_name = SecuritiesSelector(self.__sas_if, self)
        self.__radio_annual_report = QRadioButton('年报')
        self.__radio_customize_filter = QRadioButton('自定义')
        self.__line_filter_include = QLineEdit()
        self.__line_filter_exclude = QLineEdit()
        self.__button_download = QPushButton('确定')

        self.__datetime_since = QDateTimeEdit(
            QDateTime.currentDateTime().addYears(-3))
        self.__datetime_until = QDateTimeEdit(QDateTime.currentDateTime())

        self.init_ui()

    # ---------------------------------------------------- UI Init -----------------------------------------------------

    def init_ui(self):
        self.__layout_control()
        self.__config_control()

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

        main_layout.addLayout(
            horizon_layout([QLabel('股票代码'), self.__combo_name], [1, 10]))
        main_layout.addLayout(
            horizon_layout([QLabel('报告起始'), self.__datetime_since], [1, 10]))
        main_layout.addLayout(
            horizon_layout([QLabel('报告截止'), self.__datetime_until], [1, 10]))
        main_layout.addLayout(
            horizon_layout([
                QLabel('报告类型'), self.__radio_annual_report,
                self.__radio_customize_filter
            ], [1, 5, 5]))
        main_layout.addLayout(
            horizon_layout([QLabel('包含词条(以,分隔)'), self.__line_filter_include],
                           [1, 10]))
        main_layout.addLayout(
            horizon_layout([QLabel('排除词条(以,分隔)'), self.__line_filter_exclude],
                           [1, 10]))
        main_layout.addWidget(QLabel(DEFAULT_INFO))
        main_layout.addWidget(self.__button_download)

    def __config_control(self):
        # self.__combo_name.setEditable(True)
        # self.__combo_name.addItem('所有')
        # self.__combo_name.addItem('股票列表载入中')
        self.__radio_annual_report.setChecked(True)
        self.__line_filter_include.setEnabled(False)
        self.__line_filter_exclude.setEnabled(False)
        self.__radio_customize_filter.setEnabled(False)
        self.__radio_annual_report.clicked.connect(self.on_radio_report_type)
        self.__radio_customize_filter.clicked.connect(
            self.on_radio_report_type)
        self.__button_download.clicked.connect(self.on_button_download)

    def on_timer(self):
        if self.__combo_name.count() > 1:
            self.__combo_name.insertItem(0, ALL_STOCK_TEXT)
            self.__combo_name.setCurrentIndex(0)
            self.__timer.stop()
        # # Check stock list ready and update combobox
        # if self.__data_utility is not None:
        #     if self.__data_utility.stock_cache_ready():
        #         self.__combo_name.clear()
        #         self.__combo_name.addItem(ALL_STOCK_TEXT)
        #         stock_list = self.__data_utility.get_stock_list()
        #         for stock_identity, stock_name in stock_list:
        #             self.__combo_name.addItem(stock_identity + ' | ' + stock_name, stock_identity)

    def on_radio_report_type(self):
        if self.__radio_annual_report.isChecked():
            self.__line_filter_include.setEnabled(False)
            self.__line_filter_exclude.setEnabled(False)
        else:
            self.__line_filter_include.setEnabled(True)
            self.__line_filter_exclude.setEnabled(True)

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

        input_securities = self.__combo_name.get_input_securities()
        if input_securities == ALL_STOCK_TEXT:
            if self.__sas_if is None:
                QMessageBox.information(
                    self, QtCore.QCoreApplication.translate('main', '提示'),
                    QtCore.QCoreApplication.translate('main', '无法获取股票列表'),
                    QMessageBox.Yes, QMessageBox.No)
                return
            reply = QMessageBox.question(
                self, QtCore.QCoreApplication.translate('main', '操作确认'),
                QtCore.QCoreApplication.translate('main', DOWNLOAD_ALL_TIPS),
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply != QMessageBox.Yes:
                return
        self.__build_download_task(input_securities)

    def __build_download_task(self, securities: str):
        task = AnnouncementDownloadTask()
        task.securities = securities
        task.period_since = self.__datetime_since.dateTime().toPyDateTime()
        task.period_until = self.__datetime_until.dateTime().toPyDateTime()
        task.filter_include = self.__line_filter_include.text().split(',')
        task.filter_exclude = self.__line_filter_exclude.text().split(',')
        task.report_type = \
            AnnouncementDownloadTask.REPORT_TYPE_ANNUAL \
                if self.__radio_annual_report.isChecked() else \
            AnnouncementDownloadTask.REPORT_TYPE_NONE

        task.task_manager = self.__task_manager
        task.sas_if = self.__sas_if
        # task.data_utility = self.__data_utility

        if self.__task_manager is not None:
            self.__task_manager.append_task(task)
        else:
            task.run()
class StockChartUi(QWidget):
    ADJUST_TAIL = 0
    ADJUST_HEAD = 1
    ADJUST_NONE = 2

    RETURN_LOG = 3
    RETURN_SIMPLE = 4

    def __init__(self, memo_context: dict):
        self.__memo_context = memo_context
        # self.__memo_context.add_observer(self)
        self.__sas_if: sasIF = self.__memo_context.get('sas_if')
        super(StockChartUi, self).__init__()

        # self.__sas = memo_context.get_sas()
        # self.__memo_record: StockMemoRecord = memo_context.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_context.get('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(
            self.__sas_if) if self.__sas_if is not None else QComboBox()
        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 = self.__sas_if.sas_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 = self.__sas_if.sas_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)
        memo_record = self.__sas_if.stock_memo_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.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):
        pass
        # 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)
        trade_data = self.__sas_if.sas_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_context: dict, parent: QWidget = None):
        self.__memo_context = memo_context
        self.__sas_if: sasIF = self.__memo_context.get('sas_if')
        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_context.get_sas() if self.__memo_context is not None else None
        # self.__memo_record = self.__memo_context.get_memo_record() if self.__memo_context 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(self.__sas_if) if self.__sas_if is not None else QComboBox()
        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.__sas_if.stock_memo_update_record(
                self.__current_index, self.__current_stock, _time, brief, content, True)
        else:
            if str_available(self.__current_stock):
                ret, index = self.__sas_if.stock_memo_add_record(
                    self.__current_stock, _time, brief, content, 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)

        # TODO: Broadcast update
        # self.__trigger_memo_updated()
        # self.__memo_context.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.__sas_if.stock_memo_delete_record(self.__current_index, None)
            self.load_security_memo(self.__current_stock)
            self.update_memo_list()
            self.__memo_context.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)
        df = self.__sas_if.stock_memo_filter_record(condition)

        self.__current_memos = df
        self.__current_stock = security

        self.update_memo_list()

    def update_memo_list(self):
        if self.__current_memos is None:
            return

        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_context: dict):
        self.__memo_context = memo_context
        self.__sas_if: sasIF = self.__memo_context.get(
            'sas_if') if self.__memo_context is not None else None
        super(StockMemoDeck, self).__init__()

        if self.__memo_context is not None:
            # self.__memo_context.add_observer(self)
            self.__sas_if: sasIF = self.__memo_context.get('sas_if')
            self.__memo_editor: StockMemoEditor = self.__memo_context.get(
                'editor')
        else:
            # For layout debug
            self.__sas_if: sasIF = None
            self.__memo_editor = 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.__sas_if) if self.__sas_if is not None else QComboBox()
        # TODO: Path from server
        self.__line_path = QLineEdit('')
        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.__sas_if.stock_memo_get_all_security(
        ) if self.__sas_if 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
        config = self.__sas_if.sas_get_service_config()
        config['memo_path'] = folder
        self.__sas_if.sas_set_service_config(config)

        # Now those setting are on server side.
        # TODO: How to handle setting update on server side
        # Update new path to memo extras
        # self.__memo_context.set_root_path(folder)
        #
        # stock_tags: Tags = self.__memo_context.get('tags')
        # if stock_tags is not None:
        #     stock_tags.load(os.path.join(folder, 'tags.json'))
        # self.__memo_context.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.__sas_if.sas_guess_stock_identities(
            input_security)
        self.show_securities(list_securities)

    def __on_button_reload(self):
        self.show_securities(self.__sas_if.stock_memo_get_all_security(
        ) if self.__sas_if 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.__sas_if.sas_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_context.get_data('black_list')
            #     if black_list is not None:
            #         black_list_securities = black_list.all_black_list()
            if self.__sas_if is not None:
                black_list_securities = self.__sas_if.all_black_list()
                if black_list_securities is not None:
                    self.__show_securities = list(
                        set(self.__show_securities).difference(
                            set(black_list_securities)))
                else:
                    pass

    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)
示例#12
0
class ChartLab(QWidget):
    def __init__(self, sas_if: sasIF):
        super(ChartLab, self).__init__()

        # ---------------- ext var ----------------

        self.__sas_if = sas_if
        self.__inited = False
        self.__plot_table = {}
        self.__paint_data = None

        # ------------- plot resource -------------

        self.__figure = plt.figure()
        self.__canvas = FigureCanvas(self.__figure)

        # -------------- ui resource --------------

        self.__data_frame_widget = None

        self.__combo_factor = QComboBox()
        self.__label_comments = QLabel('')

        # Parallel comparison
        self.__radio_parallel_comparison = QRadioButton('横向比较')
        self.__combo_year = QComboBox()
        self.__combo_quarter = QComboBox()
        self.__combo_industry = QComboBox()

        # Longitudinal comparison
        self.__radio_longitudinal_comparison = QRadioButton('纵向比较')
        self.__combo_stock = SecuritiesSelector(self.__sas_if)

        # Limitation
        self.__line_lower = QLineEdit('')
        self.__line_upper = QLineEdit('')

        self.__button_draw = QPushButton('绘图')
        self.__button_show = QPushButton('查看绘图数据')

        self.init_ui()

    # ---------------------------------------------------- UI Init -----------------------------------------------------

    def init_ui(self):
        self.__layout_control()
        self.__config_control()

    def __layout_control(self):
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)
        self.setMinimumSize(1280, 800)

        bottom_layout = QHBoxLayout()
        main_layout.addWidget(self.__canvas, 99)
        main_layout.addLayout(bottom_layout, 1)

        group_box, group_layout = create_v_group_box('因子')
        bottom_layout.addWidget(group_box, 2)

        group_layout.addWidget(self.__combo_factor)
        group_layout.addWidget(self.__label_comments)

        group_box, group_layout = create_v_group_box('比较方式')
        bottom_layout.addWidget(group_box, 2)

        line = QHBoxLayout()
        line.addWidget(self.__radio_parallel_comparison, 1)
        line.addWidget(self.__combo_industry, 5)
        line.addWidget(self.__combo_year, 5)
        line.addWidget(self.__combo_quarter, 5)
        group_layout.addLayout(line)

        line = QHBoxLayout()
        line.addWidget(self.__radio_longitudinal_comparison, 1)
        line.addWidget(self.__combo_stock, 10)
        group_layout.addLayout(line)

        group_box, group_layout = create_v_group_box('范围限制')
        bottom_layout.addWidget(group_box, 1)

        line = QHBoxLayout()
        line.addWidget(QLabel('下限'))
        line.addWidget(self.__line_lower)
        group_layout.addLayout(line)

        line = QHBoxLayout()
        line.addWidget(QLabel('上限'))
        line.addWidget(self.__line_upper)
        group_layout.addLayout(line)

        col = QVBoxLayout()
        col.addWidget(self.__button_draw)
        col.addWidget(self.__button_show)
        bottom_layout.addLayout(col, 1)

    def __config_control(self):
        for year in range(now().year, 1989, -1):
            self.__combo_year.addItem(str(year), str(year))
        self.__combo_year.setCurrentIndex(1)

        self.__combo_quarter.addItem('一季报', '03-31')
        self.__combo_quarter.addItem('中报', '06-30')
        self.__combo_quarter.addItem('三季报', '09-30')
        self.__combo_quarter.addItem('年报', '12-31')
        self.__combo_quarter.setCurrentIndex(3)

        self.__combo_industry.addItem('全部', '全部')
        identities = self.__sas_if.sas_get_all_industries()
        for identity in identities:
            self.__combo_industry.addItem(identity, identity)

        if self.__sas_if is not None:
            factors = self.__sas_if.sas_get_all_factors()
            for fct in factors:
                self.__combo_factor.addItem(fct, fct)
        self.on_factor_updated(0)

        self.__combo_stock.setEnabled(False)
        self.__radio_parallel_comparison.setChecked(True)

        self.__radio_parallel_comparison.setToolTip(TIP_PARALLEL_COMPARISON)
        self.__radio_longitudinal_comparison.setToolTip(
            TIP_LONGITUDINAL_COMPARISON)
        self.__line_lower.setToolTip(TIP_LIMIT_UPPER_LOWER)
        self.__line_upper.setToolTip(TIP_LIMIT_UPPER_LOWER)
        self.__button_show.setToolTip(TIP_BUTTON_SHOW)

        self.__button_draw.clicked.connect(self.on_button_draw)
        self.__button_show.clicked.connect(self.on_button_show)

        self.__combo_factor.currentIndexChanged.connect(self.on_factor_updated)
        self.__radio_parallel_comparison.clicked.connect(
            self.on_radio_comparison)
        self.__radio_longitudinal_comparison.clicked.connect(
            self.on_radio_comparison)

        mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei']
        mpl.rcParams['axes.unicode_minus'] = False

    def on_factor_updated(self, value):
        self.__line_lower.setText('')
        self.__line_upper.setText('')
        factor = self.__combo_factor.itemData(value)
        comments = self.__sas_if.sas_get_factor_comments(factor)
        self.__label_comments.setText(comments)

    def on_button_draw(self):
        factor = self.__combo_factor.currentData()
        lower = str2float_safe(self.__line_lower.text(), None)
        upper = str2float_safe(self.__line_upper.text(), None)

        if self.__radio_parallel_comparison.isChecked():
            year = self.__combo_year.currentData()
            month_day = self.__combo_quarter.currentData()
            period = year + '-' + month_day
            industry = self.__combo_industry.currentData()
            self.plot_factor_parallel_comparison(factor, industry,
                                                 text_auto_time(period), lower,
                                                 upper)
        else:
            securities = self.__combo_stock.get_input_securities()
            self.plot_factor_longitudinal_comparison(factor, securities)

    def on_button_show(self):
        if self.__data_frame_widget is not None and \
                self.__data_frame_widget.isVisible():
            self.__data_frame_widget.close()
        if self.__paint_data is not None:
            self.__data_frame_widget = DataFrameWidget(self.__paint_data)
            self.__data_frame_widget.show()

    def on_radio_comparison(self):
        if self.__radio_parallel_comparison.isChecked():
            self.__combo_year.setEnabled(True)
            self.__combo_quarter.setEnabled(True)
            self.__line_lower.setEnabled(True)
            self.__line_upper.setEnabled(True)
            self.__combo_stock.setEnabled(False)
        else:
            self.__combo_year.setEnabled(False)
            self.__combo_quarter.setEnabled(False)
            self.__line_lower.setEnabled(False)
            self.__line_upper.setEnabled(False)
            self.__combo_stock.setEnabled(True)

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

    def plot_factor_parallel_comparison(self, factor: str, industry: str,
                                        period: datetime.datetime,
                                        lower: float, upper: float):
        identities = ''
        if industry != '全部':
            identities = self.__sas_if.sas_get_industry_stocks(industry)
        df = self.__sas_if.sas_factor_query(identities, [factor],
                                            (period, period), {},
                                            readable=True)

        if df is None or df.empty:
            return

        s1 = df[factor]
        if lower is not None and upper is not None:
            s1 = s1.apply(lambda x: (x if x < upper else upper)
                          if x > lower else lower)
        elif lower is not None:
            s1 = s1.apply(lambda x: x if x > lower else lower)
        elif upper is not None:
            s1 = s1.apply(lambda x: x if x < upper else upper)

        plt.clf()
        plt.subplot(1, 1, 1)
        s1.hist(bins=100)
        plt.title(factor)

        self.__canvas.draw()
        self.__canvas.flush_events()

        self.__paint_data = df
        self.__paint_data.sort_values(factor, inplace=True)

    def plot_factor_longitudinal_comparison(self, factor: str,
                                            securities: str):
        df = self.__sas_if.sas_factor_query(securities, [factor],
                                            None, {},
                                            readable=True)
        # Only for annual report
        df = df[df['period'].dt.month == 12]
        df['报告期'] = df['period']
        df.set_index('报告期', inplace=True)

        s1 = df[factor]

        plt.clf()
        plt.subplot(1, 1, 1)
        s1.plot.line()
        plt.title(factor)

        self.__canvas.draw()
        self.__canvas.flush_events()

        self.__paint_data = df
        self.__paint_data.sort_values('period', ascending=False, inplace=True)

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

    def plot(self):
        self.plot_histogram_statistics()

    def plot_histogram_statistics(self):
        # --------------------------- The Data and Period We Want to Check ---------------------------

        stock = ''
        period = (text_auto_time('2018-12-01'), text_auto_time('2018-12-31'))

        # --------------------------------------- Query Pattern --------------------------------------

        # fields_balance_sheet = ['货币资金', '资产总计', '负债合计',
        #                         '短期借款', '一年内到期的非流动负债', '其他流动负债',
        #                         '长期借款', '应付债券', '其他非流动负债', '流动负债合计',
        #                         '应收票据', '应收账款', '其他应收款', '预付款项',
        #                         '交易性金融资产', '可供出售金融资产',
        #                         '在建工程', '商誉', '固定资产']
        # fields_income_statement = ['营业收入', '营业总收入', '减:营业成本', '息税前利润']
        #
        # df, result = batch_query_readable_annual_report_pattern(
        #     self.__data_hub, stock, period, fields_balance_sheet, fields_income_statement)
        # if result is not None:
        #     return result

        # df_balance_sheet, result = query_readable_annual_report_pattern(
        #     self.__data_hub, 'Finance.BalanceSheet', stock, period, fields_balance_sheet)
        # if result is not None:
        #     print('Data Error')
        #
        # df_income_statement, result = query_readable_annual_report_pattern(
        #     self.__data_hub, 'Finance.IncomeStatement', stock, period, fields_income_statement)
        # if result is not None:
        #     print('Data Error')

        # -------------------------------- Merge and Pre-processing --------------------------------

        # df = pd.merge(df_balance_sheet,
        #               df_income_statement,
        #               how='left', on=['stock_identity', 'period'])

        # df = df.sort_values('period')
        # df = df.reset_index()
        # df = df.fillna(0)
        # df = df.replace(0, 1)

        # ------------------------------------- Calc and Plot -------------------------------------

        mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei']
        mpl.rcParams['axes.unicode_minus'] = False

        # font = matplotlib.font_manager.FontProperties(fname='C:/Windows/Fonts/msyh.ttf')
        # mpl.rcParams['axes.unicode_minus'] = False

        # df['应收款'] = df['应收账款'] + df['应收票据']
        # df['净资产'] = df['资产总计'] - df['负债合计']
        # df['短期负债'] = df['短期借款'] + df['一年内到期的非流动负债'] + df['其他流动负债']
        # df['有息负债'] = df['短期负债'] + df['长期借款'] + df['应付债券'] + df['其他非流动负债']
        # df['金融资产'] = df['交易性金融资产'] + df['可供出售金融资产']
        #
        # df['财务费用正'] = df['减:财务费用'].apply(lambda x: x if x > 0 else 0)
        # df['三费'] = df['减:销售费用'] + df['减:管理费用'] + df['财务费用正']

        df = self.__sas_if.sas_auto_query(
            '', period,
            ['减:财务费用', '减:销售费用', '减:管理费用', '营业总收入', '营业收入', '减:营业成本'],
            ['stock_identity', 'period'])

        df['毛利润'] = df['营业收入'] - df['减:营业成本']
        df['财务费用正'] = df['减:财务费用'].apply(lambda x: x if x > 0 else 0)
        df['三费'] = df['减:销售费用'] + df['减:管理费用'] + df['财务费用正']

        s1 = df['三费'] / df['营业总收入']
        s1 = s1.apply(lambda x: (x if x < 1 else 1) if x > -0.1 else -0.1)
        plt.subplot(2, 1, 1)
        s1.hist(bins=100)
        plt.title('三费/营业总收入')

        s2 = df['三费'] / df['毛利润']
        s2 = s2.apply(lambda x: (x if x < 1 else 1) if x > -0.1 else -0.1)
        plt.subplot(2, 1, 2)
        s2.hist(bins=100)
        plt.title('三费/毛利润')

        # s1 = df['货币资金'] / df['有息负债']
        # s1 = s1.apply(lambda x: x if x < 10 else 10)
        # plt.subplot(2, 1, 1)
        # s1.hist(bins=100)
        # plt.title('货币资金/有息负债')
        #
        # s2 = df['有息负债'] / df['资产总计']
        # plt.subplot(2, 1, 2)
        # s2.hist(bins=100)
        # plt.title('有息负债/资产总计')

        # s1 = df['应收款'] / df['营业收入']
        # s1 = s1.apply(lambda x: x if x < 2 else 2)
        # plt.subplot(4, 1, 1)
        # s1.hist(bins=100)
        # plt.title('应收款/营业收入')
        #
        # s2 = df['其他应收款'] / df['营业收入']
        # s2 = s2.apply(lambda x: x if x < 1 else 1)
        # plt.subplot(4, 1, 2)
        # s2.hist(bins=100)
        # plt.title('其他应收款/营业收入')
        #
        # s3 = df['预付款项'] / df['营业收入']
        # s3 = s3.apply(lambda x: x if x < 1 else 1)
        # plt.subplot(4, 1, 3)
        # s3.hist(bins=100)
        # plt.title('预付款项/营业收入')
        #
        # s4 = df['预付款项'] / df['减:营业成本']
        # s4 = s4.apply(lambda x: x if x < 1 else 1)
        # plt.subplot(4, 1, 4)
        # s4.hist(bins=100)
        # plt.title('预付款项/营业成本')

        # s1 = df['商誉'] / df['净资产']
        # s1 = s1.apply(lambda x: (x if x < 1 else 1) if x > 0 else 0)
        # plt.subplot(3, 1, 1)
        # s1.hist(bins=100)
        # plt.title('商誉/净资产')
        #
        # s2 = df['在建工程'] / df['净资产']
        # s2 = s2.apply(lambda x: (x if x < 1 else 1) if x > 0 else 0)
        # plt.subplot(3, 1, 2)
        # s2.hist(bins=100)
        # plt.title('在建工程/净资产')
        #
        # s2 = df['在建工程'] / df['资产总计']
        # s2 = s2.apply(lambda x: (x if x < 1 else 1) if x > 0 else 0)
        # plt.subplot(3, 1, 3)
        # s2.hist(bins=100)
        # plt.title('在建工程/资产总计')

        # s1 = df['固定资产'] / df['资产总计']
        # s1 = s1.apply(lambda x: (x if x < 1 else 1) if x > 0 else 0)
        # plt.subplot(2, 1, 1)
        # s1.hist(bins=100)
        # plt.title('固定资产/资产总计')
        #
        # s2 = df['息税前利润'] / df['固定资产']
        # s2 = s2.apply(lambda x: (x if x < 10 else 10) if x > -10 else -10)
        # plt.subplot(2, 1, 2)
        # s2.hist(bins=100)
        # plt.title('息税前利润/固定资产')

        # self.plot_proportion([
        #     ChartLab.PlotItem('固定资产', '资产总计', 0, 1),
        #     ChartLab.PlotItem('息税前利润', '固定资产', -10, 10),
        # ], text_auto_time('2018-12-31'))

        self.repaint()

    class PlotItem:
        def __init__(self,
                     num: str,
                     den: str,
                     lower: float or None = None,
                     upper: float or None = None,
                     bins: int = 100):
            self.numerator = num
            self.denominator = den
            self.limit_lower = lower
            self.limit_upper = upper
            self.plot_bins = bins

    def plot_proportion(self, plot_set: [PlotItem], period: datetime.datetime):
        df = self.prepare_plot_data(plot_set, period)

        plot_count = len(plot_set)
        for plot_index in range(plot_count):
            plot_item = plot_set[plot_index]
            s = df[plot_item.numerator] / df[plot_item.denominator]
            if plot_item.limit_lower is not None:
                s = s.apply(lambda x: max(x, plot_item.limit_lower))
            if plot_item.limit_upper is not None:
                s = s.apply(lambda x: min(x, plot_item.limit_upper))
            plt.subplot(plot_count, 1, plot_index + 1)
            s.hist(bins=plot_item.plot_bins)
            plt.title(plot_item.numerator + '/' + plot_item.denominator)

    def prepare_plot_data(self, plot_set: [PlotItem],
                          period: datetime.datetime) -> pd.DataFrame:
        fields = []
        for plot_item in plot_set:
            fields.append(plot_item.numerator)
            fields.append(plot_item.denominator)
        fields = list(set(fields))
        return self.__sas_if.sas_auto_query(
            '', (period - datetime.timedelta(days=1), period), fields,
            ['stock_identity', 'period'])