PyQtGraph应用(五):k线回放复盘功能实现
本文介绍了基于PyQtGraph实现k线回放复盘功能的方法。通过控制k线数据的绘制范围,实现了类似TradingView和AICoin的k线播放、暂停、前进、后退功能。文章对比了国内外行情软件的k线回放功能,并展示了自定义实现的预览效果。代码在原有IndicatorsViewWidget基础上扩展,通过处理不同时间周期的数据索引,实现了日期匹配和周期切换逻辑。核心包括初始化动画参数、处理日期匹配、
·
PyQtGraph应用(五):k线回放复盘功能实现
前言
本文示例在前面文章基础上扩展。
k线回放复盘这个功能感觉还是挺实用的,但国内各大主流行情软件却没有提供这个功能,目前只在国外的行情软件(如:TradingView-付费、AICoin-不支持国内A股)用过,索性就自己写个好了。
效果对比及预览
TradingView:
AICoin:
代码实现:
代码实现
主要在此前分享的:PyQTGraph应用(四)基础上扩展。
此前分享的IndicatorsViewWidget中k线、指标绘制逻辑是传入要绘制的k线数据,这里只需要控制一下绘制的范围,就可以实现k线指标播放、暂停、前进、后退的功能。
初始化回放动画
def init_animation(self, data, start_date, b_init=True):
dict_return = {}
code = data['code']
self.logger.info(f"初始化动画:{code}, 日期:{start_date}")
self.update_stock_data_dict(code)
df = self.get_stock_data()
if df is None or df.empty:
self.logger.warning(f"数据为空,无法初始化动画:{code}")
self.sig_init_review_animation_finished.emit(False, dict_return)
return dict_return
if start_date is not None and start_date != "":
self.min_animation_index = 0
self.max_animation_index = len(df) - 1
checked_id = self.period_button_group.checkedId()
last_period_text = self.period_button_group.button(self.last_period_btn_checked_id).text()
last_period = TimePeriod.from_label(last_period_text)
target_period_text = self.period_button_group.button(checked_id).text()
target_period = TimePeriod.from_label(target_period_text)
current_period_date_col = 'time' if TimePeriod.is_minute_level(last_period) else 'date'
if b_init:
matching_indices = df[df['date'] <= start_date].index
if len(matching_indices) > 0:
# 普通处理
self.logger.info(f"动画初始化--找到 {len(matching_indices)} 个匹配的日期记录")
self.start_animation_index = matching_indices[-1]
self.logger.info(f"start_date索引: {self.start_animation_index}")
review_period_process_data = ReviewPeriodProcessData()
review_period_process_data.current_period = target_period
review_period_process_data.current_start_date_time = df['date'].iloc[self.start_animation_index]
review_period_process_data.current_min_index = 0
review_period_process_data.current_max_index = self.max_animation_index
review_period_process_data.current_index = self.start_animation_index
review_period_process_data.current_start_index = self.start_animation_index
self.dict_period_process_data[target_period] = review_period_process_data
self.update_chart(data, self.start_animation_index)
dict_return = {
"start_date_index": self.start_animation_index,
"min_index": self.min_animation_index,
"max_index": self.max_animation_index,
"start_date": review_period_process_data.current_start_date_time
}
self.last_period_btn_checked_id = 7
self.min_period = TimePeriod.DAY
else:
self.logger.warning(f"普通处理--未找到匹配的日期记录:{start_date}")
else:
if checked_id >= 8:
matching_indices = df[df['date'] < start_date].index # 这里使用<是因为本周未结束时,Baostock无本周周线数据,因此加载上周周线数据。
else:
matching_indices = df[df['date'] == start_date].index
matching_indices_len = len(matching_indices)
self.logger.info(f"切换--找到 {matching_indices_len} 个匹配的日期记录")
if matching_indices_len > 0:
# 周期切换步骤。来源周期,目标周期
# 目标周期是否第一次切换?
# 第一次切换默认到最后索引
# 不是第一次切换则判断来源周期索引是否发生变化
# 有发生变化则更新目标周期索引,没有发生变化则自动切换到目标周期索引
s_last_period_text = TimePeriod.get_chinese_label(last_period)
s_target_period_text = TimePeriod.get_chinese_label(target_period)
self.logger.info(f"周期切换--来源周期:{s_last_period_text},目标周期:{s_target_period_text}")
self.logger.info(f"来源周期当前索引:{self.current_animation_index},开始索引:{self.start_animation_index}")
last_current_index = self.dict_period_process_data[last_period].current_index
last_start_index = self.dict_period_process_data[last_period].current_start_index
self.logger.info(f"来源周期[{s_last_period_text}]索引:{last_current_index},日期:{self.dict_period_process_data[last_period].current_date_time},来源周期[{s_last_period_text}]开始索引:{last_start_index}, 开始日期:{self.dict_period_process_data[last_period].current_start_date_time}")
index = self.get_target_index_auto(last_period, target_period)
self.logger.info(f"目标周期索引:{index}")
self.start_animation_index = matching_indices[index]
if target_period not in self.dict_period_process_data:
# 第1次切换,默认到最后索引
self.logger.info(f"第1次切换")
# index = self.get_target_index_auto(last_period, target_period)
# self.start_animation_index = matching_indices[index]
else:
target_current_index = self.dict_period_process_data[target_period].current_index
target_start_index = self.dict_period_process_data[target_period].current_start_index
self.logger.info(f"上次目标周期[{s_target_period_text}]索引:{target_current_index},日期:{self.dict_period_process_data[target_period].current_date_time},上次目标周期[{s_target_period_text}]开始索引:{target_start_index},日期:{self.dict_period_process_data[target_period].current_start_date_time}")
# TODO优化:更新来源周期索引判断是否合成目标周期索引对应的值
review_period_process_data = ReviewPeriodProcessData()
review_period_process_data.current_period = target_period
review_period_process_data.current_start_date_time = df['date'].iloc[self.start_animation_index]
review_period_process_data.current_min_index = 0
review_period_process_data.current_max_index = self.max_animation_index
review_period_process_data.current_index = self.start_animation_index
review_period_process_data.current_start_index = self.start_animation_index
self.dict_period_process_data[target_period] = review_period_process_data
if target_period < self.min_period:
self.min_period = target_period
self.logger.info(f"更新最小周期为:{TimePeriod.get_chinese_label(self.min_period)}")
self.update_chart(data, self.start_animation_index)
dict_return = {
"start_date_index": self.start_animation_index,
"min_index": self.min_animation_index,
"max_index": self.max_animation_index,
"start_date": review_period_process_data.current_start_date_time
}
else:
self.logger.warning(f"周期切换处理--未找到匹配的日期记录:{start_date}")
else:
self.logger.info("start_date为空")
self.sig_init_review_animation_finished.emit(True, dict_return)
return dict_return
维护了k线数据的索引以及当前显示索引位置后,添加播放/暂停控制接口:
播放:
def start_animation(self, start_index=None):
"""开始动画播放"""
self.animation_timer.start(self.animation_speed)
self.is_playing = True
self.enable_period_btn(False)
暂停:
def pause_animation(self):
"""暂停动画播放"""
self.animation_timer.stop()
self.is_playing = False
self.enable_period_btn(True)
停止:
def stop_animation(self):
"""停止动画播放"""
self.animation_timer.stop()
self.is_playing = False
self.enable_period_btn(True)
设置播放速度:
def set_animation_speed(self, speed_ms):
"""设置动画播放速度"""
self.animation_speed = speed_ms
if self.is_playing:
self.animation_timer.stop()
self.animation_timer.start(self.animation_speed)
再添加前进、后退接口:
前进指定步数:
def step_forward(self, steps=1):
"""向前播放指定步数"""
if self.df_data is None or self.df_data.empty:
return
new_index = self.current_animation_index + steps
if new_index < 0:
QMessageBox.warning(self, "提示", "已到达最前")
self.sig_animation_play_finished.emit()
return
if new_index > self.max_animation_index:
QMessageBox.warning(self, "提示", "已到达最后")
return
data = self.df_data.iloc[0]
self.update_chart(data, new_index)
回退指定步数:
def step_backward(self, steps=1):
"""向后回退指定步数"""
if self.df_data is None or self.df_data.empty:
return
new_index = self.current_animation_index - steps
if new_index < 0:
QMessageBox.warning(self, "提示", "已到达最前")
return
data = self.df_data.iloc[0]
self.update_chart(data, new_index)
回退到最前:
def back_to_front(self):
"""回到最前"""
if self.df_data is None or self.df_data.empty:
return
data = self.df_data.iloc[0]
self.update_chart(data, 0)
前进到最后:
def back_to_end(self):
"""回到最后"""
if self.df_data is None or self.df_data.empty:
return
data = self.df_data.iloc[0]
self.update_chart(data, self.max_animation_index)
跳到指定索引:
def go_to_target_index(self, index):
"""跳转到指定索引"""
if self.df_data is None or self.df_data.empty:
return
if index == self.current_animation_index:
return
data = self.df_data.iloc[0]
self.update_chart(data, index)
然后就是外层的ReviewWidget实现,UI如下:
代码:
from PyQt5 import QtCore, uic, QtGui
from PyQt5.QtWidgets import QWidget, QDialog, QMessageBox, QListWidget, QListWidgetItem
from PyQt5.QtCore import QDate, QFile
import random
from datetime import date
from manager.logging_manager import get_logger
from gui.qt_widgets.MComponents.demo_trading_card_widget import DemoTradingCardWidget
from gui.qt_widgets.MComponents.demo_trading_record_widget import DemoTradingRecordWidget
from processor.baostock_processor import BaoStockProcessor
from manager.bao_stock_data_manager import BaostockDataManager
from manager.period_manager import TimePeriod
from manager.review_demo_trading_manager import ReviewDemoTradingManager
class ReviewWidget(QWidget):
def __init__(self, parent=None):
super(ReviewWidget, self).__init__(parent)
uic.loadUi('./src/gui/qt_widgets/MComponents/ReviewWidget.ui', self)
self.init_para()
self.init_ui()
self.init_connect()
# self.load_data('sh.600000', "2025-12-08")
def init_para(self):
self.logger = get_logger(__name__)
self.type = 0 # 0: 模块;1:对话框
self.dict_progress_data = {}
self.current_load_code = ""
self.demo_trading_manager = ReviewDemoTradingManager()
def init_ui(self):
from gui.qt_widgets.MComponents.indicators.indicators_view_widget import IndicatorsViewWidget
self.indicators_view_widget = IndicatorsViewWidget(self)
self.indicators_view_widget.setProperty("review", True)
self.indicators_view_widget.show_review_btn(False)
self.verticalLayout_indicators_view.addWidget(self.indicators_view_widget)
self.comboBox_period.addItems([TimePeriod.get_chinese_label(TimePeriod.DAY), TimePeriod.get_chinese_label(TimePeriod.WEEK), TimePeriod.get_chinese_label(TimePeriod.MINUTE_15), TimePeriod.get_chinese_label(TimePeriod.MINUTE_30), TimePeriod.get_chinese_label(TimePeriod.MINUTE_60)])
self.comboBox_period.setCurrentIndex(0)
self.btn_load_data_random.setAutoDefault(False)
self.btn_load_data_random.setDefault(False)
self.btn_load_data.setAutoDefault(False)
self.btn_load_data.setDefault(False)
self.playing_enabled(True, True)
self.label_total_assets.setText(str(self.demo_trading_manager.get_total_assets()))
self.label_available_balance.setText(str(self.demo_trading_manager.get_available_balance()))
self.listWidget_trading_record = QListWidget(self)
self.demo_trading_record_widget = DemoTradingRecordWidget(self)
self.stackedWidget_trading_record.addWidget(self.listWidget_trading_record)
self.stackedWidget_trading_record.addWidget(self.demo_trading_record_widget)
self.stackedWidget_trading_record.setCurrentWidget(self.listWidget_trading_record)
self.load_qss()
self.btn_play.setProperty("is_play", False)
self.btn_play.style().unpolish(self.btn_play)
self.btn_play.style().polish(self.btn_play)
self.btn_play.update()
def init_connect(self):
self.indicators_view_widget.sig_current_animation_index_changed.connect(self.slot_current_animation_index_changed)
self.indicators_view_widget.sig_init_review_animation_finished.connect(self.slot_init_review_animation_finished)
self.indicators_view_widget.sig_animation_play_finished.connect(self.slot_animation_play_finished)
self.lineEdit_code.editingFinished.connect(self.slot_lineEdit_code_editingFinished)
self.dateEdit.dateChanged.connect(self.slot_dateEdit_dateChanged)
self.comboBox_period.currentIndexChanged.connect(self.slot_comboBox_period_currentIndexChanged)
self.btn_load_data_random.clicked.connect(self.slot_btn_load_data_random_clicked)
self.btn_load_data.clicked.connect(self.slot_btn_load_data_clicked)
self.btn_play.clicked.connect(self.slot_btn_play_clicked)
self.btn_back_to_front.clicked.connect(self.slot_btn_back_to_front_clicked)
self.btn_back_ten.clicked.connect(self.slot_btn_back_ten_clicked)
self.btn_back.clicked.connect(self.slot_btn_back_clicked)
self.btn_move_on.clicked.connect(self.slot_btn_move_on_clicked)
self.btn_move_on_10.clicked.connect(self.slot_btn_move_on_10_clicked)
self.btn_move_to_last.clicked.connect(self.slot_btn_move_to_last_clicked)
self.horizontalSlider_progress.valueChanged.connect(self.slot_horizontalSlider_progress_valueChanged)
# 模拟交易
self.lineEdit_price.editingFinished.connect(self.slot_lineEdit_price_editingFinished)
self.btn_all.clicked.connect(self.slot_btn_all_clicked)
self.btn_one_half.clicked.connect(self.slot_btn_one_half_clicked)
self.btn_one_third.clicked.connect(self.slot_btn_one_third_clicked)
self.btn_a_quarter.clicked.connect(self.slot_btn_a_quarter_clicked)
self.btn_one_in_five.clicked.connect(self.slot_btn_one_in_five_clicked)
self.btn_buy.clicked.connect(self.slot_btn_buy_clicked)
self.btn_sell.clicked.connect(self.slot_btn_sell_clicked)
self.btn_pending_order_cancel.clicked.connect(self.slot_btn_pending_order_cancel_clicked)
self.demo_trading_manager.sig_total_assets_and_available_balance_changed.connect(self.update_assets_and_available_balance)
self.demo_trading_manager.sig_trading_status_changed.connect(self.slot_demo_trading_manager_sig_trading_status_changed)
self.demo_trading_record_widget.sig_btn_return_clicked.connect(self.slot_demo_trading_record_widget_sig_btn_return_clicked)
def load_qss(self, theme="default"):
qss_file_name = f":/theme/{theme}/mcomponents/review_widget.qss"
self.logger.info(f"回放模块样式表文件路径:{qss_file_name}")
qssFile = QFile(qss_file_name)
if qssFile.open(QFile.ReadOnly):
str_qss = str(qssFile.readAll(), encoding='utf-8')
# self.logger.info(f"回放模块样式表内容:{str_qss}")
self.setStyleSheet(str_qss)
else:
self.logger.warning("无法打开回放模块样式表文件")
qssFile.close()
def preload_data(self, df_row):
self.logger.info(f"回放预加载数据: {df_row}")
if df_row is None:
self.logger.warning("回放预加载数据为空")
return
# 对话框模式才会预加载
self.type = 1
self.label_name.setText(df_row["name"])
self.lineEdit_code.setText(df_row["code"])
# self.dateEdit.blockSignals(True)
self.dateEdit.setDate(QDate.fromString(df_row['date'], "yyyy-MM-dd"))
# self.dateEdit.blockSignals(False)
self.btn_load_data_random.hide()
self.lineEdit_code.setEnabled(False)
def update_assets_and_available_balance(self, total_assets, available_balance):
self.label_total_assets.setText(f"{total_assets:.2f}")
self.label_available_balance.setText(f"{available_balance:.2f}")
def update_count_and_amount_labels(self, price, count):
self.lineEdit_count.setText(str(count))
self.lineEdit_amount.setText(str(count * price))
def playing_enabled(self, is_playing, b_init=False):
self.lineEdit_code.setEnabled(not is_playing and self.type == 0)
self.dateEdit.setEnabled(True if b_init else not is_playing)
self.btn_back_to_front.setEnabled(not is_playing)
self.btn_back_ten.setEnabled(not is_playing)
self.btn_back.setEnabled(not is_playing)
self.btn_move_on.setEnabled(not is_playing)
self.btn_move_on_10.setEnabled(not is_playing)
self.btn_move_to_last.setEnabled(not is_playing)
self.horizontalSlider_progress.setEnabled(not is_playing)
self.btn_buy.setEnabled(not is_playing)
self.btn_sell.setEnabled(not is_playing)
self.btn_pending_order_cancel.setEnabled(not is_playing)
self.btn_all.setEnabled(not is_playing)
self.btn_one_half.setEnabled(not is_playing)
self.btn_one_third.setEnabled(not is_playing)
self.btn_a_quarter.setEnabled(not is_playing)
self.btn_one_in_five.setEnabled(not is_playing)
def update_progress_label(self, current_index):
if self.dict_progress_data is not None and self.dict_progress_data != {}:
s_current_progress = f"{current_index}/{self.dict_progress_data['max_index']}"
self.label_progress.setText(s_current_progress)
else:
self.logger.info("进度数据为空")
self.label_progress.setText("")
def update_trading_widgets_status(self):
trading_status = self.demo_trading_manager.get_trading_status()
if trading_status == 1 or trading_status == 3:
self.btn_all.setEnabled(False)
self.btn_one_half.setEnabled(False)
self.btn_one_third.setEnabled(False)
self.btn_a_quarter.setEnabled(False)
self.btn_one_in_five.setEnabled(False)
self.btn_buy.setEnabled(False)
self.btn_sell.setEnabled(False)
self.btn_pending_order_cancel.setEnabled(True)
elif trading_status == 5:
self.btn_all.setEnabled(False)
self.btn_one_half.setEnabled(False)
self.btn_one_third.setEnabled(False)
self.btn_a_quarter.setEnabled(False)
self.btn_one_in_five.setEnabled(False)
self.btn_buy.setEnabled(False)
self.btn_sell.setEnabled(True)
self.btn_pending_order_cancel.setEnabled(False)
else:
self.btn_all.setEnabled(True)
self.btn_one_half.setEnabled(True)
self.btn_one_third.setEnabled(True)
self.btn_a_quarter.setEnabled(True)
self.btn_one_in_five.setEnabled(True)
self.btn_buy.setEnabled(True)
self.btn_sell.setEnabled(False)
self.btn_pending_order_cancel.setEnabled(False)
def reset_trading_record(self):
self.lineEdit_price.blockSignals(True)
self.lineEdit_price.clear()
self.lineEdit_price.blockSignals(False)
self.lineEdit_count.clear()
self.lineEdit_amount.clear()
# 清空收益率曲线
# 清空交易记录列表
def load_data(self, code, date):
stock_codes = [code]
bao_stock_data_manager = BaostockDataManager()
new_dict_lastest_1d_stock_data = bao_stock_data_manager.get_lastest_row_data_dict_by_code_list_auto(stock_codes)
# self.logger.info(f"new_dict_lastest_1d_stock_data: {new_dict_lastest_1d_stock_data}")
if new_dict_lastest_1d_stock_data:
self.indicators_view_widget.update_stock_data_dict(code)
data = new_dict_lastest_1d_stock_data[code].iloc[-1]
# self.indicators_view_widget.update_chart(data, '2025-12-08')
self.dict_progress_data = self.indicators_view_widget.init_animation(data, date)
self.current_load_code = code
# self.demo_trading_manager.reset_trading_record() # 重新加载不用情况当前收益
self.playing_enabled(False)
self.update_trading_widgets_status()
self.reset_trading_record()
else:
self.logger.info(f"结果为空")
def get_random_date(self, latest_date_str=None, days_before_start=360, days_before_end=120):
'''
根据给定的最新日期和范围参数,在指定范围内随机返回一个日期
Args:
latest_date_str (str): 最新日期字符串,格式为 "YYYY-MM-DD",默认为当前日期
days_before_start (int): 最新日期往前推的起始天数,默认为30天
days_before_end (int): 最新日期往前推的结束天数,默认为1天
Returns:
str: 格式为 "YYYY-MM-DD" 的随机日期字符串
Example:
# 在 2026-01-19 前 30 天到 1 天的范围内随机选择日期
random_date = get_random_date("2026-01-19", 30, 1)
'''
from datetime import datetime, timedelta
# 如果未提供最新日期,则使用当前日期
if latest_date_str is None:
latest_date = date.today()
else:
try:
latest_date = datetime.strptime(latest_date_str, "%Y-%M-%D").date()
except ValueError:
# 处理输入日期格式错误的情况
latest_date = date.today()
# 计算范围边界
start_date = latest_date - timedelta(days=days_before_start)
end_date = latest_date - timedelta(days=days_before_end)
# 确保开始日期不晚于结束日期
if start_date > end_date:
start_date, end_date = end_date, start_date
# 计算日期范围内的总天数
total_days = (end_date - start_date).days
# 在范围内随机选择一天
random_day_offset = random.randint(0, total_days)
random_date = start_date + timedelta(days=random_day_offset)
return random_date.strftime("%Y-%m-%d")
# -----------------槽函数----------------
def slot_current_animation_index_changed(self, index):
# self.logger.info(f"收到k线图进度: {index}")
self.horizontalSlider_progress.blockSignals(True)
self.horizontalSlider_progress.setSliderPosition(index)
self.horizontalSlider_progress.blockSignals(False)
self.update_progress_label(index)
date_time = self.indicators_view_widget.get_current_date_time_by_index(index)
dict_kline_price = self.indicators_view_widget.get_kline_price_by_index(index)
trading_status = self.demo_trading_manager.get_trading_status()
target_status = 0
if trading_status == 1:
target_status = 1
elif trading_status == 3:
target_status = 2
elif trading_status == 5:
target_status = 5
self.demo_trading_manager.update_trading_record(target_status, dict_kline_price, date_time)
def slot_init_review_animation_finished(self, success, dict_progress_data):
if success:
self.logger.info("回放动画初始化成功")
self.dict_progress_data = dict_progress_data
if self.dict_progress_data is not None and self.dict_progress_data != {}:
self.horizontalSlider_progress.setMinimum(self.dict_progress_data['min_index'])
self.horizontalSlider_progress.setMaximum(self.dict_progress_data['max_index'])
self.horizontalSlider_progress.blockSignals(True)
self.horizontalSlider_progress.setSliderPosition(self.dict_progress_data['start_date_index'])
self.horizontalSlider_progress.blockSignals(False)
self.update_progress_label(self.dict_progress_data['start_date_index'])
else:
self.logger.info(f"初始化返回的进度数据为空")
def slot_animation_play_finished(self):
self.slot_btn_play_clicked()
def slot_lineEdit_code_editingFinished(self):
code = self.lineEdit_code.text()
dict_lastest_1d_data = BaostockDataManager().get_lastest_1d_stock_data_dict_from_cache()
if code not in dict_lastest_1d_data:
QMessageBox.warning(self, "提示", "请输入正确的股票代码")
return
self.label_name.setText(str(dict_lastest_1d_data[code]['name'].iloc[0]))
self.lineEdit_code.blockSignals(True)
self.lineEdit_code.clearFocus()
self.lineEdit_code.blockSignals(False)
def slot_dateEdit_dateChanged(self, date):
self.logger.info(f"收到日期选择: {date}")
s_date = date.toString("yyyy-MM-dd")
self.logger.info(f"收到日期选择--s_date: {s_date}")
def slot_comboBox_period_currentIndexChanged(self, index):
text = self.comboBox_period.currentText()
self.logger.info(f"收到周期选择: {text}, index: {index}")
def slot_btn_load_data_random_clicked(self):
# 从 dict_lastest_1d_data 中获取一个随机的 code 和对应的 name
dict_lastest_1d_data = BaostockDataManager().get_lastest_1d_stock_data_dict_from_cache()
if dict_lastest_1d_data:
# 随机选择一个 code
code = random.choice(list(dict_lastest_1d_data.keys()))
# 获取对应的 name
name = dict_lastest_1d_data[code]['name'].iloc[0]
newest_date = dict_lastest_1d_data[code]['date'].iloc[0]
print(f"随机股票代码: {code}, 对应名称: {name},最新日期: {newest_date}")
else:
print("没有可用的股票数据")
return
date = self.get_random_date(newest_date)
period = self.comboBox_period.currentText()
self.logger.info(f"点击随机加载数据: {code}, {date}, {period}")
if self.current_load_code == code:
self.logger.info("当前股票数据已加载,无需重复加载")
return
self.load_data(code, date)
start_date = self.dict_progress_data['start_date']
self.logger.info(f"实际开始日期--start_date: {start_date}")
self.label_name.setText(str(dict_lastest_1d_data[code]['name'].iloc[0]))
self.lineEdit_code.blockSignals(True)
self.lineEdit_code.setText(code)
self.lineEdit_code.blockSignals(False)
self.dateEdit.blockSignals(True)
self.dateEdit.setDate(QDate.fromString(start_date, "yyyy-MM-dd"))
self.dateEdit.blockSignals(False)
self.btn_buy.setDefault(True)
def slot_btn_load_data_clicked(self):
code = self.lineEdit_code.text()
date = self.dateEdit.date().toString("yyyy-MM-dd")
period = self.comboBox_period.currentText()
self.logger.info(f"点击加载数据: {code}, {date}, {period}")
if self.current_load_code == code:
self.logger.info("当前股票数据已加载,无需重复加载")
return
self.load_data(code, date)
start_date = self.dict_progress_data['start_date']
self.logger.info(f"实际开始日期--start_date: {start_date}")
self.dateEdit.blockSignals(True)
self.dateEdit.setDate(QDate.fromString(start_date, "yyyy-MM-dd"))
self.dateEdit.blockSignals(False)
self.btn_buy.setDefault(True)
def slot_btn_play_clicked(self):
if self.btn_play.property("is_play"):
self.logger.info("暂停播放")
self.indicators_view_widget.pause_animation()
self.btn_play.setProperty("is_play", False)
# self.btn_play.setIcon(QtGui.QIcon("./src/gui/qt_widgets/images/pause.png"))
self.playing_enabled(False)
else:
self.logger.info("开始播放")
self.indicators_view_widget.start_animation()
self.btn_play.setProperty("is_play", True)
self.playing_enabled(True)
self.btn_play.style().unpolish(self.btn_play)
self.btn_play.style().polish(self.btn_play)
self.btn_play.update()
def slot_btn_back_to_front_clicked(self):
self.indicators_view_widget.back_to_front()
def slot_btn_back_ten_clicked(self):
self.indicators_view_widget.step_backward(10)
def slot_btn_back_clicked(self):
self.indicators_view_widget.step_backward()
def slot_btn_move_on_clicked(self):
self.indicators_view_widget.step_forward()
def slot_btn_move_on_10_clicked(self):
self.indicators_view_widget.step_forward(10)
def slot_btn_move_to_last_clicked(self):
self.indicators_view_widget.back_to_end()
def slot_horizontalSlider_progress_valueChanged(self, value):
self.logger.info(f"进度条值改变: {value}")
self.indicators_view_widget.go_to_target_index(value)
def slot_lineEdit_price_editingFinished(self):
str_price = self.lineEdit_price.text()
str_count = self.lineEdit_count.text()
if str_price == "" or str_count == "":
return
self.lineEdit_amount.setText(str(float(str_price) * int(str_count)))
def slot_btn_all_clicked(self):
str_price = self.lineEdit_price.text()
max_count = self.demo_trading_manager.get_buy_count(float(str_price))
self.logger.info(f"最大可买数量: {max_count}")
self.lineEdit_count.setText(str(max_count))
self.lineEdit_amount.setText(str(max_count * float(str_price)))
def slot_btn_one_half_clicked(self):
str_price = self.lineEdit_price.text()
max_count = self.demo_trading_manager.get_buy_count(float(str_price), 1)
self.logger.info(f"最大可买数量: {max_count}")
self.lineEdit_count.setText(str(max_count))
self.lineEdit_amount.setText(str(max_count * float(str_price)))
def slot_btn_one_third_clicked(self):
str_price = self.lineEdit_price.text()
max_count = self.demo_trading_manager.get_buy_count(float(str_price), 2)
self.logger.info(f"最大可买数量: {max_count}")
self.lineEdit_count.setText(str(max_count))
self.lineEdit_amount.setText(str(max_count * float(str_price)))
def slot_btn_a_quarter_clicked(self):
str_price = self.lineEdit_price.text()
max_count = self.demo_trading_manager.get_buy_count(float(str_price), 3)
self.logger.info(f"最大可买数量: {max_count}")
self.lineEdit_count.setText(str(max_count))
self.lineEdit_amount.setText(str(max_count * float(str_price)))
def slot_btn_one_in_five_clicked(self):
str_price = self.lineEdit_price.text()
max_count = self.demo_trading_manager.get_buy_count(float(str_price), 4)
self.logger.info(f"最大可买数量: {max_count}")
self.lineEdit_count.setText(str(max_count))
self.lineEdit_amount.setText(str(max_count * float(str_price)))
def slot_btn_buy_clicked(self):
if self.current_load_code == "":
self.logger.info("请先加载股票数据")
return
str_code = self.lineEdit_code.text()
str_name = self.label_name.text()
str_price = self.lineEdit_price.text()
str_count = self.lineEdit_count.text()
if str_price == "" or str_count == "":
self.logger.info("请填写价格和数量")
return
current_index = self.horizontalSlider_progress.value()
str_date_time = self.indicators_view_widget.get_current_date_time_by_index(current_index)
self.logger.info(f"点击买入: {str_code}, {str_name}, {str_price}, {str_count}, {str_date_time}")
self.demo_trading_manager.pending_order_buy(str_code, str_name, float(str_price), int(str_count), str_date_time)
def slot_btn_sell_clicked(self):
if self.current_load_code == "":
self.logger.info("请先加载股票数据")
return
str_price = self.lineEdit_price.text()
str_count = self.lineEdit_count.text()
current_index = self.horizontalSlider_progress.value()
str_date_time = self.indicators_view_widget.get_current_date_time_by_index(current_index)
self.logger.info(f"点击卖出: {str_price}, {str_count}, {str_date_time}")
self.demo_trading_manager.pending_order_sell(float(str_price), int(str_count), str_date_time)
def slot_btn_pending_order_cancel_clicked(self):
if self.current_load_code == "":
self.logger.info("请先加载股票数据")
return
current_index = self.horizontalSlider_progress.value()
str_date_time = self.indicators_view_widget.get_current_date_time_by_index(current_index)
dict_kline_price = self.indicators_view_widget.get_kline_price_by_index(current_index)
self.logger.info(f"点击取消挂单: {str_date_time}")
self.demo_trading_manager.update_trading_record(0, dict_kline_price, str_date_time)
def slot_demo_trading_manager_sig_trading_status_changed(self, status):
self.update_trading_widgets_status()
if status == 1:
# 添加Item到ListWidget
demo_trading_card_widget = DemoTradingCardWidget()
demo_trading_card_widget.set_data(self.demo_trading_manager.current_trading_record)
demo_trading_card_widget.update_ui()
self.demo_trading_manager.sig_trading_yield_changed.connect(demo_trading_card_widget.slot_trading_status_changed)
self.demo_trading_manager.sig_trading_status_changed.connect(demo_trading_card_widget.slot_trading_status_changed)
demo_trading_card_widget.clicked.connect(self.slot_demo_trading_card_clicked)
# demo_trading_card_widget.hovered.connect(self.slot_demo_trading_card_hovered)
# demo_trading_card_widget.hoverLeft.connect(self.slot_demo_trading_card_hover_left)
# demo_trading_card_widget.doubleClicked.connect(self.slot_demo_trading_card_double_clicked)
item = QListWidgetItem(self.listWidget_trading_record)
# 设置 item 的大小(可选)
item.setSizeHint(demo_trading_card_widget.sizeHint())
# item.setSizeHint(QtCore.QSize(200, 60))
# 将 item 添加到 list widget,默认添加到最前
# self.listWidget_trading_record.addItem(item)
self.listWidget_trading_record.insertItem(0, item)
# 将自定义 widget 设置为 item 的 widget
self.listWidget_trading_record.setItemWidget(item, demo_trading_card_widget)
elif status == 6:
# 交易完成,更新收益曲线图
list_data = self.demo_trading_manager.get_trding_record_list()
self.logger.info(f"交易完成,更新收益曲线图,长度: {len(list_data)}")
for data in list_data:
self.logger.info(f"code: {data.code}")
self.logger.info(f"name: {data.name}")
self.logger.info(f"买入挂单时间: {data.pending_order_buy_date_time}")
self.logger.info(f"买入挂单取消时间: {data.pending_order_buy_date_time}")
self.logger.info(f"买入挂单价格: {data.buy_price}")
self.logger.info(f"买入挂单成交时间: {data.buy_date_time}")
# self.logger.info(f"买入金额: {data.buy_amount}")
# self.logger.info(f"买入股数: {data.buy_count}")
self.logger.info(f"买出挂单时间: {data.pending_order_sell_date_time}")
self.logger.info(f"买出挂单取消时间: {data.pending_order_sell_cancel_date_time}")
self.logger.info(f"买出挂单价格: {data.sell_price}")
self.logger.info(f"买出挂单时间: {data.sell_date_time}")
# self.logger.info(f"买出金额: {data.sell_amount}")
# self.logger.info(f"买出股数: {data.sell_count}")
self.logger.info(f"交易状态: {data.status}")
self.logger.info(f"收益: {data.trading_yield}")
self.logger.info("\n---------------------------------------------------------\n")
self.widget_total_yield_curve.update_data(list_data)
def slot_demo_trading_card_clicked(self, trading_record):
self.logger.info(f"交易买入挂单时间:{trading_record.pending_order_buy_date_time}")
self.demo_trading_record_widget.update_trading_record(trading_record)
self.stackedWidget_trading_record.setCurrentWidget(self.demo_trading_record_widget)
def slot_demo_trading_record_widget_sig_btn_return_clicked(self):
self.stackedWidget_trading_record.setCurrentWidget(self.listWidget_trading_record)
然后效果就是上面展示的那样啦:
因为基于模块化设计实现,因此也可以嵌入到对话框中:
扩展
《PyQTGraph系列》文章:
更多推荐



所有评论(0)