diff --git a/.gitignore b/.gitignore index c18dd8d..35bf4c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ __pycache__/ +dist/ +example.db diff --git a/core/constants.py b/core/constants.py new file mode 100644 index 0000000..dd64a00 --- /dev/null +++ b/core/constants.py @@ -0,0 +1,6 @@ +import xtquant.xtconstant as xtconstant + +OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买 +OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖 +OrderTypeInit = "0" # 建仓 +OrderTypeNone = "None" \ No newline at end of file diff --git a/core/database.py b/core/database.py new file mode 100644 index 0000000..3629252 --- /dev/null +++ b/core/database.py @@ -0,0 +1,14 @@ +from peewee import SqliteDatabase, Model, CharField, IntegerField, FloatField, BooleanField + +from core.logger import LogLevel, PrintLog +from xtquant import xtconstant + +# 连接到SQLite数据库 +db = SqliteDatabase('example.db') + +db.connect() +PrintLog(LogLevel.INFO, '- [成功]数据库连接') +# 定义基础模型类 +class BaseModel(Model): + class Meta: + database = db \ No newline at end of file diff --git a/core/logger.py b/core/logger.py index 9938f81..a306836 100644 --- a/core/logger.py +++ b/core/logger.py @@ -1,6 +1,7 @@ from enum import Enum from core.eventbus import EventPrintLog, event_bus +import sfgrid_config class LogLevel(Enum): DEBUG = "DEBUG" @@ -17,3 +18,5 @@ class LogData: def PrintLog(level:LogLevel, message:str): data = LogData(level, message) event_bus.publish(EventPrintLog, data) + if sfgrid_config.console_log: + print(f'{level.value} {message}') diff --git a/core/main_controller.py b/core/main_controller.py index 048473d..7ac52a4 100644 --- a/core/main_controller.py +++ b/core/main_controller.py @@ -1,30 +1,30 @@ # coding:utf-8 -from core.strategy_db import TradeTarget +from core.sfgrid.model import TradeTarget from core.eventbus import ActionDisableMarketData, ActionEnableMarketData, ActionEventAddTradeTarget, ActionEventDeleteTradeTarget, ActionEventDisableTrade, ActionEventEnableTrade, EventTradeTargetUpdate, MarketDataUpdate, MarketDataEnabled, MarketDataDisabled, ResultEventTradeDisabled, ResultEventTradeEnabled, ResultEventTradeTargetDeleted, ActionEventGridFix, event_bus from xtquant.xttrader import XtQuantTrader import time from peewee import ModelSelect -import core.strategy_db as strategy_db -import sfgrid_constants -from core.sfgrid_strategy import SFGridStrategy +import core.sfgrid.model as model +import sfgrid_config +from core.sfgrid.sfgrid_strategy import SFGridStrategy from core.util import getInstrumentName, getStockPosition, queryPendingOrder from xtquant.xttrader import XtQuantTrader -from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtOrderResponse, XtPosition, XtTrade +from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtPosition, XtTrade from xtquant import xtdata from xtquant.xttrader import XtQuantTraderCallback import datetime -import core.ui as ui +import core.sfgrid.ui as ui from core.logger import PrintLog, LogLevel -from core.objects import GridFixData +from core.sfgrid.objects import GridFixData # 量化核心控制对象 -class SFGridController(XtQuantTraderCallback): +class MainController(XtQuantTraderCallback): def __init__(self, account_no: str, miniQmtPath: str): super().__init__() self.registerEventHandler() - self.appUi = ui.TradeTargetUI() + # self.appUi = ui.TradeTargetUI() xtdata.enable_hello = False @@ -118,13 +118,10 @@ class SFGridController(XtQuantTraderCallback): except Exception as e: PrintLog(LogLevel.ERROR, f"网格修正更新失败: {str(e)}") - def hold(self): - self.appUi.run() - def startMarketData(self): PrintLog(LogLevel.INFO, '- 启动市场数据订阅') - self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate) + # self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate) if self.seq == -1: PrintLog(LogLevel.ERROR, '- 市场数据订阅失败') else: @@ -149,12 +146,12 @@ class SFGridController(XtQuantTraderCallback): return # 检查是否已存在该标的 - existing_target = strategy_db.TradeTarget.get_or_none(strategy_db.TradeTarget.stock_code == stock_code) + existing_target = model.TradeTarget.get_or_none(model.TradeTarget.stock_code == stock_code) if existing_target: PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在') return - new_target = strategy_db.TradeTarget.create( + new_target = model.TradeTarget.create( stock_name=stock_name, stock_code=stock_code, market_price=0.0, @@ -171,7 +168,7 @@ class SFGridController(XtQuantTraderCallback): PrintLog(LogLevel.INFO, f'新增交易标的 {stock_code} {stock_name}, {new_target.id}') # 刷新标的持仓 pos = getStockPosition(stock_code, self.xt_trader, self.account) # type: ignore - strategy_db.TradeTarget.update(current_position=pos).where(strategy_db.TradeTarget.stock_code == stock_code).execute() + model.TradeTarget.update(current_position=pos).where(model.TradeTarget.stock_code == stock_code).execute() # 更新标的池 self.refresh_targets() # 添加交易控制器 @@ -189,7 +186,7 @@ class SFGridController(XtQuantTraderCallback): PrintLog(LogLevel.ERROR, f"交易标的 ID {id} 不存在") return - target: strategy_db.TradeTarget = self.instrument_pool[id] + target: model.TradeTarget = self.instrument_pool[id] # 如果存在交易控制器,先停止交易 if target.stock_code in self.stock_trade_ctrl: @@ -215,9 +212,9 @@ class SFGridController(XtQuantTraderCallback): for id in self.instrument_pool: target:TradeTarget = self.instrument_pool[id] status = "新建" if target.status == 0 else "已建初始仓" - PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name} 当前持仓: {getStockPosition(target.stock_code, self.xt_trader, self.account)} 网格索引: {target.grid_index} 基准价格 {sfgrid_constants.grid_price[target.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if target.enabled else '交易已停止'}') # type: ignore + PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name} 当前持仓: {getStockPosition(target.stock_code, self.xt_trader, self.account)} 网格索引: {target.grid_index} 基准价格 {sfgrid_config.grid_price[target.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if target.enabled else '交易已停止'}') # type: ignore - tradeTarget:strategy_db.TradeTarget = self.instrument_pool[id] + tradeTarget:model.TradeTarget = self.instrument_pool[id] tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore result = tradeTarget.save() PrintLog(LogLevel.INFO, f' |- 同步[{target.stock_code}-{target.stock_name}]持仓信息[{'成功' if result == 1 else '失败'}]') @@ -230,10 +227,10 @@ class SFGridController(XtQuantTraderCallback): def refresh_targets(self): # 更新标的池 - results:ModelSelect = strategy_db.TradeTarget.select() - self.instrument_pool: dict[int, strategy_db.TradeTarget] = {} + results:ModelSelect = model.TradeTarget.select() + self.instrument_pool: dict[int, model.TradeTarget] = {} for temp in results: - result :strategy_db.TradeTarget = temp + result :model.TradeTarget = temp self.instrument_pool[result.get_id()] = result def print_position_info(self): @@ -293,7 +290,7 @@ class SFGridController(XtQuantTraderCallback): def pause_stock_trade(self, id: int): - localTarget: strategy_db.TradeTarget = self.instrument_pool[id] + localTarget: model.TradeTarget = self.instrument_pool[id] print(f'暂停标的交易 {localTarget.stock_code} - enabled {localTarget.enabled}') if localTarget.stock_code in self.stock_trade_ctrl: tradeController: SFGridStrategy = self.stock_trade_ctrl[localTarget.stock_code] @@ -307,96 +304,3 @@ class SFGridController(XtQuantTraderCallback): else: print(f"标的交易控制器不存在 {localTarget.stock_code} {localTarget.stock_name}\n") - - # ====== 市场回调方法 -- 以下方法由XtQuantData调用 ====== - def onDataUpdate(self, data): - # 收集所有市场数据用于市场监控 - for stock_code, tickData in data.items(): - if stock_code in self.stock_trade_ctrl: - stock_controller: SFGridStrategy = self.stock_trade_ctrl[stock_code] - stock_controller.onDataUpdate(data) - else: - # 非目标交易,发布市场数据更新事件用于市场监控 - lastPrice = tickData['lastPrice'] - if lastPrice == 10 or stock_code in self.listening_stock: - # 发布市场数据更新事件用于市场监控 - market_target = TradeTarget() - market_target.stock_code = stock_code - market_target.stock_name = getInstrumentName(stock_code) # type: ignore - market_target.market_price = lastPrice # type: ignore - event_bus.publish(MarketDataUpdate, market_target) - if stock_code not in self.listening_stock: - self.listening_stock.append(stock_code) - - - # ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ====== - def on_connected(self): - """ - 连接成功推送 - """ - print(datetime.datetime.now(), '连接成功回调') - - def on_disconnected(self): - """ - 连接断开 - :return: - """ - print(datetime.datetime.now(), '连接断开回调') - - def on_stock_order(self, order:XtOrder): - """ - 委托回报推送 - :param order: XtOrder对象 - :return: - """ - print(f'orderd {order.strategy_name}-{order.stock_code} {order.order_id} {order.order_volume}-{order.order_status}') - stockCode = order.stock_code - ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] - # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 - if ctrl is not None and order.strategy_name == ctrl.getName(): - print(f'controller info {ctrl.getName()}') - ctrl.onAsyncOrderResponse(order) # type: ignore - else: - print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'") - - - def on_stock_trade(self, trade:XtTrade): - """ - 成交变动推送 - :param trade: XtTrade对象 - :return: - """ - stockCode = trade.stock_code - ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] - # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 - if ctrl is not None and trade.strategy_name == ctrl.getName(): - ctrl.onOrderTrade(trade) - else: - print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}") - - # def on_order_stock_async_response(self, response:XtOrderResponse): - # stockCode = response.order_remark - # ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] - # # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 - # if ctrl is not None and response.strategy_name == ctrl.getName(): - # ctrl.onAsyncOrderResponse(response) - # else: - # print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}") - - def on_order_error(self, order_error): - """ - 委托失败推送 - :param order_error:XtOrderError 对象 - :return: - """ - # print("on order_error callback") - # print(order_error.order_id, order_error.error_id, order_error.error_msg) - print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}") - - - def on_account_status(self, status): - """ - :param response: XtAccountStatus 对象 - :return: - """ - print(datetime.datetime.now(), status) \ No newline at end of file diff --git a/core/main_ui.py b/core/main_ui.py new file mode 100644 index 0000000..a4d7d94 --- /dev/null +++ b/core/main_ui.py @@ -0,0 +1,327 @@ +import time +import tkinter as tk +from tkinter import ttk +from core.logger import LogLevel, PrintLog +from core.sfgrid.ui import TradeTargetUI +import sfgrid_config +from xtquant import xtdata +from xtquant.xttrader import XtQuantTrader, XtQuantTraderCallback +import datetime +from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtOrderResponse, XtPosition, XtTrade + +class MainWindow(XtQuantTraderCallback): + def __init__(self): + self.root = tk.Tk() + self.root.title("三疯交易系统") + self.root.geometry("1400x700") + + # 当前选中的策略Tab索引 + self.current_strategy_index = 0 + # 存储各个Frame的引用 + self.strategy_frames = {} + # 日志面板可见性标志 + self.log_visible = False + + self.initQmt() + # 创建界面 + self.create_ui() + + def initQmt(self): + xtdata.enable_hello = False + + session_id = int(time.time()) + + self.xt_trader: XtQuantTrader = XtQuantTrader(sfgrid_config.miniQMTPath, session_id) + self.xt_trader.register_callback(self) + self.xt_trader.start() + self.xt_trader.connect() + PrintLog(LogLevel.INFO, f'- [{'成功' if self.xt_trader.connected else '失败'}]市场交易连接: {sfgrid_config.miniQMTPath}') + if self.xt_trader.connected == False: + self.inited: bool = False + return + else: + self.inited = True + + self.account= StockAccount(sfgrid_config.account_no, 'STOCK') + PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {self.account.account_id}') # pyright: ignore[reportAttributeAccessIssue] + subscribe_result = self.xt_trader.subscribe(self.account) + PrintLog(LogLevel.INFO, f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅') + if subscribe_result == 0: + self.inited = True + else: + self.inited = False + return + + def create_ui(self): + """创建UI界面""" + # 主容器 + main_container = ttk.Frame(self.root) + main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 中间主体区域(左右布局) + content_area = ttk.Frame(main_container) + content_area.pack(fill=tk.BOTH, expand=True) + + # 左侧Tab按钮栏(垂直排列) + tab_bar_frame = ttk.Frame(content_area) + tab_bar_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) + + # 创建Tab按钮(垂直排列,文字垂直显示) + self.tab_buttons = [] + strategy_names = ["三疯\n网格", "通用\n网格", "涨停\n分析"] + + for idx, name in enumerate(strategy_names): + btn = ttk.Button( + tab_bar_frame, + text=name, + command=lambda i=idx: self.switch_strategy_tab(i), + width=4 + ) + btn.pack(side=tk.TOP, pady=2, fill=tk.X) + self.tab_buttons.append(btn) + + # 在Tab按钮下方添加退出按钮和日志按钮(底部对齐) + # 使用一个填充Frame将按钮推到底部 + spacer = ttk.Frame(tab_bar_frame) + spacer.pack(side=tk.TOP, fill=tk.X, ipady=10) + + # 清空日志按钮(底部第三个) + clear_log_btn = ttk.Button( + tab_bar_frame, + text="🗑", # 垃圾桶图标 + command=self.clear_logs, + width=3 + ) + clear_log_btn.pack(side=tk.TOP, pady=2, fill=tk.X) + + # 日志显示按钮(退出按钮上方) + self.log_toggle_btn = ttk.Button( + tab_bar_frame, + text="📋", # 日志图标 + command=self.toggle_log_panel, + width=3 + ) + self.log_toggle_btn.pack(side=tk.TOP, pady=2, fill=tk.X) + + # 退出按钮(最底部) + exit_btn = ttk.Button( + tab_bar_frame, + text="⏻", # 电源图标 + command=self.on_exit, + width=3 + ) + exit_btn.pack(side=tk.TOP, pady=2, fill=tk.X) + + # 添加垂直分隔线 + separator = ttk.Separator(content_area, orient='vertical') + separator.pack(side=tk.LEFT, fill=tk.Y, padx=1) + + # 右侧内容区域容器(用于放置不同策略的Frame) + self.content_container = ttk.Frame(content_area) + self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 创建各个策略的Frame + self.create_strategy_frames(strategy_names) + + # 创建全局日志面板(默认隐藏) + self.create_global_log_panel(main_container) + + # 默认显示第一个策略 + self.switch_strategy_tab(0) + + def create_global_log_panel(self, parent): + """创建全局日志面板""" + # 日志区域(默认隐藏) + self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10) + # 默认不显示,通过工具栏按钮控制 + + # 创建日志表格 + columns = ("timestamp", "level", "message") + + self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8) + + log_column_configs = { + "timestamp": ("时间", 100), + "level": ("级别", 50), + "message": ("消息", 1150) # 调整宽度适应全局布局 + } + + for col in columns: + title, width = log_column_configs[col] + self.log_table.heading(col, text=title) + self.log_table.column(col, width=width, anchor=tk.W) + + # 添加初始日志 + from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功")) + + # 滚动条 + scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview) + self.log_table.configure(yscrollcommand=scrollbar.set) + + self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + def add_log(self, level:LogLevel, message): + """添加日志记录 - 全局方法""" + from datetime import datetime + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.log_table.insert('', 0, values=(timestamp, level.value, message)) + + def clear_logs(self): + """清空日志记录""" + # 删除所有日志项 + for item in self.log_table.get_children(): + self.log_table.delete(item) + self.add_log(LogLevel.DEBUG, "日志已清空") + + def create_strategy_frames(self, strategy_names): + """创建各个策略的Frame""" + for idx, name in enumerate(strategy_names): + if idx == 0: + # 第一个Tab使用TradeTargetUI,传入main_window引用 + frame = TradeTargetUI(self.content_container, self) + self.strategy_frames[idx] = frame + else: + # 其他策略使用占位Frame + frame = ttk.Frame(self.content_container) + self.strategy_frames[idx] = frame + + # 添加占位内容 + placeholder = ttk.Label( + frame, + text=f"{name} - 策略界面将在此实现", + font=('Arial', 14), + foreground='gray' + ) + placeholder.pack(expand=True) + + def switch_strategy_tab(self, index): + """切换策略Tab""" + # 隐藏当前Frame + if self.current_strategy_index in self.strategy_frames: + self.strategy_frames[self.current_strategy_index].pack_forget() + + # 更新当前索引 + self.current_strategy_index = index + + # 显示选中的Frame + if index in self.strategy_frames: + self.strategy_frames[index].pack(fill=tk.BOTH, expand=True) + + # 更新Tab按钮样式(可选,用于视觉反馈) + self.update_tab_button_styles() + + + + + def update_tab_button_styles(self): + """更新Tab按钮的样式以显示选中状态""" + # 注意:ttk.Button的样式需要通过ttk.Style来设置 + # 这里简化处理,仅作为接口预留 + pass + + def toggle_log_panel(self): + """切换日志面板的显示/隐藏""" + if self.log_visible: + # 隐藏日志面板 + self.log_frame.pack_forget() + self.log_visible = False + self.log_toggle_btn.config(text="📋") # 日志图标 + else: + # 显示日志面板 + self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) + self.log_visible = True + self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏 + + def on_exit(self): + """退出程序""" + from tkinter import messagebox + result = messagebox.askyesno("确认退出", "确定要退出系统吗?") + if result: + self.root.destroy() + + def run(self): + """运行程序""" + self.root.mainloop() + + + # ====== 市场回调方法 -- 以下方法由XtQuantData调用 ====== + def onDataUpdate(self, data): + # 收集所有市场数据用于市场监控 + print(f'market data update {len(data)}') + + + # ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ====== + def on_connected(self): + """ + 连接成功推送 + """ + print(datetime.datetime.now(), '连接成功回调') + + def on_disconnected(self): + """ + 连接断开 + :return: + """ + print(datetime.datetime.now(), '连接断开回调') + + def on_stock_order(self, order:XtOrder): + """ + 委托回报推送 + :param order: XtOrder对象 + :return: + """ + print(f'orderd {order.strategy_name}-{order.stock_code} {order.order_id} {order.order_volume}-{order.order_status}') + # stockCode = order.stock_code + # ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + # if ctrl is not None and order.strategy_name == ctrl.getName(): + # print(f'controller info {ctrl.getName()}') + # ctrl.onAsyncOrderResponse(order) # type: ignore + # else: + # print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'") + + + def on_stock_trade(self, trade:XtTrade): + """ + 成交变动推送 + :param trade: XtTrade对象 + :return: + """ + print(f"委托回调 投资备注 {trade.stock_code}-{trade.instrument_name} {trade.strategy_name} 不匹配 {trade.order_remark}") + # stockCode = trade.stock_code + # ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + # if ctrl is not None and trade.strategy_name == ctrl.getName(): + # ctrl.onOrderTrade(trade) + # else: + # print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}") + + def on_order_stock_async_response(self, response:XtOrderResponse): + print(f"委托回调 投资备注 {response.error_msg}{response.strategy_name} {response.order_remark}") + + # stockCode = response.order_remark + # ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + # if ctrl is not None and response.strategy_name == ctrl.getName(): + # ctrl.onAsyncOrderResponse(response) + # else: + # print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}") + + def on_order_error(self, order_error): + """ + 委托失败推送 + :param order_error:XtOrderError 对象 + :return: + """ + print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}") + + + def on_account_status(self, status): + """ + :param response: XtAccountStatus 对象 + :return: + """ + print(datetime.datetime.now(), status) \ No newline at end of file diff --git a/core/sfgrid/main_controller.py b/core/sfgrid/main_controller.py new file mode 100644 index 0000000..98efe18 --- /dev/null +++ b/core/sfgrid/main_controller.py @@ -0,0 +1,402 @@ +# coding:utf-8 +from core.sfgrid.model import TradeTarget +from core.eventbus import ActionDisableMarketData, ActionEnableMarketData, ActionEventAddTradeTarget, ActionEventDeleteTradeTarget, ActionEventDisableTrade, ActionEventEnableTrade, EventTradeTargetUpdate, MarketDataUpdate, MarketDataEnabled, MarketDataDisabled, ResultEventTradeDisabled, ResultEventTradeEnabled, ResultEventTradeTargetDeleted, ActionEventGridFix, event_bus +from xtquant.xttrader import XtQuantTrader +import time +from peewee import ModelSelect + +import core.sfgrid.model as model +import sfgrid_config +from core.sfgrid.sfgrid_strategy import SFGridStrategy +from core.util import getInstrumentName, getStockPosition, queryPendingOrder +from xtquant.xttrader import XtQuantTrader +from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtPosition, XtTrade +from xtquant import xtdata +from xtquant.xttrader import XtQuantTraderCallback +import datetime +import core.sfgrid.ui as ui +from core.logger import PrintLog, LogLevel +from core.sfgrid.objects import GridFixData + +# 量化核心控制对象 +class SFGridController(XtQuantTraderCallback): + def __init__(self, account_no: str, miniQmtPath: str): + super().__init__() + + self.registerEventHandler() + self.appUi = ui.TradeTargetUI() + + xtdata.enable_hello = False + + session_id = int(time.time()) + + self.xt_trader: XtQuantTrader = XtQuantTrader(miniQmtPath, session_id) + self.xt_trader.register_callback(self) + self.xt_trader.start() + self.xt_trader.connect() + PrintLog(LogLevel.INFO, f'- [{'成功' if self.xt_trader.connected else '失败'}]市场交易连接: {miniQmtPath}') + if self.xt_trader.connected == False: + self.inited: bool = False + return + else: + self.inited = True + + self.account= StockAccount(account_no, 'STOCK') + PrintLog(LogLevel.INFO, f'- [成功]交易账号对象初始化完成, 账号: {self.account.account_id}') # pyright: ignore[reportAttributeAccessIssue] + subscribe_result = self.xt_trader.subscribe(self.account) + PrintLog(LogLevel.INFO, f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅') + if subscribe_result == 0: + self.inited = True + else: + self.inited = False + return + self.listening_stock = [] + self.stock_trade_ctrl = {} + self.init_instrument_pool(self.xt_trader, self.account) # type: ignore + + self.seq = None + PrintLog(LogLevel.INFO, '- [成功]三疯交易系统初始化完成') + self.startMarketData() + + def registerEventHandler(self): + event_bus.subscribe(ActionEventEnableTrade, self.onEnableTrade) + event_bus.subscribe(ActionEventDisableTrade, self.onDisableTrade) + event_bus.subscribe(ActionEnableMarketData, self.onMarketDataEnabled) + event_bus.subscribe(ActionDisableMarketData, self.onMarketDataDisabled) + event_bus.subscribe(ActionEventAddTradeTarget, self.onAddTradeTarget) + event_bus.subscribe(ActionEventDeleteTradeTarget, self.onDeleteTradeTarget) + event_bus.subscribe(ActionEventGridFix, self.onGridFix) + + def onDeleteTradeTarget(self, id: int): + """处理删除交易标的事件""" + self.del_trade_target(id) + # 发布删除完成事件 + event_bus.publish(ResultEventTradeTargetDeleted, id) + + def onAddTradeTarget(self, stock_code: str): + """处理添加交易标的事件""" + self.add_trade_target(stock_code) + + def onMarketDataEnabled(self, data): + """处理市场数据监听启用事件""" + self.startMarketData() + + def onMarketDataDisabled(self, data): + """处理市场数据监听禁用事件""" + self.stopMarketData() + + def onEnableTrade(self, id: int): + self.start_stock_trade(id) + + def onDisableTrade(self, id: int): + self.pause_stock_trade(id) + + def onGridFix(self, data: GridFixData): + """处理网格修正事件""" + self.update_trade_target_grid(data) + + def update_trade_target_grid(self, data: GridFixData): + """更新交易标的网格信息""" + try: + target = data.tradeTarget + grid_index = data.grid_index + + # 更新数据库中的网格索引 + target.grid_index = grid_index + target.save() + + # 更新内存中的交易标的 + if target.get_id() in self.instrument_pool: + self.instrument_pool[target.get_id()] = target + + # 更新交易控制器中的网格信息 + if target.stock_code in self.stock_trade_ctrl: + trade_controller: SFGridStrategy = self.stock_trade_ctrl[target.stock_code] + trade_controller.updateGridIndex(grid_index) # type: ignore + + PrintLog(LogLevel.INFO, f"网格修正已应用: {target.stock_code} - {target.stock_name}, 网格索引: {grid_index}") + except Exception as e: + PrintLog(LogLevel.ERROR, f"网格修正更新失败: {str(e)}") + + def hold(self): + self.appUi.run() + + def startMarketData(self): + PrintLog(LogLevel.INFO, '- 启动市场数据订阅') + + self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate) + if self.seq == -1: + PrintLog(LogLevel.ERROR, '- 市场数据订阅失败') + else: + event_bus.publish(MarketDataEnabled, True) + PrintLog(LogLevel.INFO, f'- 市场数据订阅成功, 订阅号={self.seq}') + + + + def stopMarketData(self): + PrintLog(LogLevel.INFO, '- 停止市场数据订阅') + + if self.seq is not None and self.seq > 0: + xtdata.unsubscribe_quote(self.seq) + event_bus.publish(MarketDataDisabled, False) + + + def add_trade_target(self, stock_code: str): + try: + stock_name = getInstrumentName(stock_code) + if not stock_name: + PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确') + return + + # 检查是否已存在该标的 + existing_target = model.TradeTarget.get_or_none(model.TradeTarget.stock_code == stock_code) + if existing_target: + PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在') + return + + new_target = model.TradeTarget.create( + stock_name=stock_name, + stock_code=stock_code, + market_price=0.0, + current_position=0, + grid_index=0, + last_trade_price=0.0, + plan_buy_price=0.0, + plan_sell_price=0.0, + current_order_price=0.0, + current_order_no='', + current_order_type='' + ) + new_target.save() + PrintLog(LogLevel.INFO, f'新增交易标的 {stock_code} {stock_name}, {new_target.id}') + # 刷新标的持仓 + pos = getStockPosition(stock_code, self.xt_trader, self.account) # type: ignore + model.TradeTarget.update(current_position=pos).where(model.TradeTarget.stock_code == stock_code).execute() + # 更新标的池 + self.refresh_targets() + # 添加交易控制器 + stockTradeController = SFGridStrategy(new_target, self.xt_trader, self.account) # type: ignore + self.stock_trade_ctrl[stock_code] = stockTradeController + + except Exception as e: + PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}') + + + def del_trade_target(self, id:int): + try: + # 检查标的是否存在 + if id not in self.instrument_pool: + PrintLog(LogLevel.ERROR, f"交易标的 ID {id} 不存在") + return + + target: model.TradeTarget = self.instrument_pool[id] + + # 如果存在交易控制器,先停止交易 + if target.stock_code in self.stock_trade_ctrl: + # 停止交易控制器 + del self.stock_trade_ctrl[target.stock_code] + + # 从数据库中删除 + target.delete_instance() + + # 从内存中删除 + del self.instrument_pool[id] + + # 刷新标的池 + self.refresh_targets() + + PrintLog(LogLevel.INFO, f"已删除交易标的: {target.stock_code} - {target.stock_name}") + except Exception as e: + PrintLog(LogLevel.ERROR, f"删除交易标的失败 ID {id}: {str(e)}") + + def init_instrument_pool(self, xtTrader:XtQuantTrader, account:StockAccount): + self.refresh_targets() + + for id in self.instrument_pool: + target:TradeTarget = self.instrument_pool[id] + status = "新建" if target.status == 0 else "已建初始仓" + PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name} 当前持仓: {getStockPosition(target.stock_code, self.xt_trader, self.account)} 网格索引: {target.grid_index} 基准价格 {sfgrid_config.grid_price[target.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if target.enabled else '交易已停止'}') # type: ignore + + tradeTarget:model.TradeTarget = self.instrument_pool[id] + tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore + result = tradeTarget.save() + PrintLog(LogLevel.INFO, f' |- 同步[{target.stock_code}-{target.stock_name}]持仓信息[{'成功' if result == 1 else '失败'}]') + stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account) # type: ignore + self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController + event_bus.publish(EventTradeTargetUpdate, tradeTarget) + + PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.instrument_pool)} 个标的') + + + def refresh_targets(self): + # 更新标的池 + results:ModelSelect = model.TradeTarget.select() + self.instrument_pool: dict[int, model.TradeTarget] = {} + for temp in results: + result :model.TradeTarget = temp + self.instrument_pool[result.get_id()] = result + + def print_position_info(self): + positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account) + if positions: + PrintLog(LogLevel.INFO, "\n- 持仓信息") + for temp in positions: + pos : XtPosition = temp + if pos.volume <=0: + continue + PrintLog(LogLevel.INFO, f"股票代码: {pos.stock_code}-{getInstrumentName(pos.stock_code)}") + PrintLog(LogLevel.INFO, f"总持仓: {pos.volume}") + PrintLog(LogLevel.INFO, f"可用持仓: {pos.can_use_volume}") + PrintLog(LogLevel.INFO, f"持仓成本: {pos.avg_price}") + PrintLog(LogLevel.INFO, "---") + else: + PrintLog(LogLevel.INFO, "\n当前无持仓") + + def print_account_info(self): + temp = self.xt_trader.query_stock_asset(self.account) + asset: XtAsset = temp # type: ignore + + PrintLog(LogLevel.INFO, f"=== 账户信息 {self.account.account_id} ===") # type: ignore + PrintLog(LogLevel.INFO, f"可用资金: {asset.cash}") + PrintLog(LogLevel.INFO, f"总资产: {asset.total_asset}") + PrintLog(LogLevel.INFO, f"证券市值: {asset.market_value}") + + def print_stock_orders(self): + orders = self.xt_trader.query_stock_orders(self.account, cancelable_only=True) + if orders: + PrintLog(LogLevel.INFO, "\n=== 委托信息 ===") + for order in orders: + PrintLog(LogLevel.INFO, f"委托编号: {order.order_id}") + PrintLog(LogLevel.INFO, f"股票代码: {order.stock_code} {getInstrumentName(order.stock_code)}") + PrintLog(LogLevel.INFO, f"委托方向: {order.offset_flag} ") + PrintLog(LogLevel.INFO, f"委托价格: {order.price}") + PrintLog(LogLevel.INFO, f"委托数量: {order.order_volume}") + PrintLog(LogLevel.INFO, f"已成交数量: {order.traded_volume}") + PrintLog(LogLevel.INFO, f"委托状态: {order.order_status} ") + PrintLog(LogLevel.INFO, "---") + else: + PrintLog(LogLevel.INFO, "\n当前无委托记录") + + + # 初始化指定标的交易控制器 + def start_stock_trade(self, id: int): + tradeTarget: TradeTarget = self.instrument_pool[id] + # check existing thread + if tradeTarget.stock_code in self.stock_trade_ctrl: + tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code] + + tradeTarget = tradeController.enabledTrading(True) + self.instrument_pool[id] = tradeTarget + event_bus.publish(ResultEventTradeEnabled, tradeTarget) + else: + PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}") + + + def pause_stock_trade(self, id: int): + localTarget: model.TradeTarget = self.instrument_pool[id] + print(f'暂停标的交易 {localTarget.stock_code} - enabled {localTarget.enabled}') + if localTarget.stock_code in self.stock_trade_ctrl: + tradeController: SFGridStrategy = self.stock_trade_ctrl[localTarget.stock_code] + tradeTarget = tradeController.enabledTrading(False) + orders = queryPendingOrder(localTarget.stock_code, tradeController.getName(), self.xt_trader, self.account) # type: ignore + for order in orders: + self.xt_trader.cancel_order_stock_async(self.account, order.order_id) + print(f'取消未成交订单 {len(orders)}') + self.instrument_pool[id] = tradeTarget + event_bus.publish(ResultEventTradeDisabled, tradeTarget) + else: + print(f"标的交易控制器不存在 {localTarget.stock_code} {localTarget.stock_name}\n") + + + # ====== 市场回调方法 -- 以下方法由XtQuantData调用 ====== + def onDataUpdate(self, data): + # 收集所有市场数据用于市场监控 + for stock_code, tickData in data.items(): + if stock_code in self.stock_trade_ctrl: + stock_controller: SFGridStrategy = self.stock_trade_ctrl[stock_code] + stock_controller.onDataUpdate(data) + else: + # 非目标交易,发布市场数据更新事件用于市场监控 + lastPrice = tickData['lastPrice'] + if lastPrice == 10 or stock_code in self.listening_stock: + # 发布市场数据更新事件用于市场监控 + market_target = TradeTarget() + market_target.stock_code = stock_code + market_target.stock_name = getInstrumentName(stock_code) # type: ignore + market_target.market_price = lastPrice # type: ignore + event_bus.publish(MarketDataUpdate, market_target) + if stock_code not in self.listening_stock: + self.listening_stock.append(stock_code) + + + # ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ====== + def on_connected(self): + """ + 连接成功推送 + """ + print(datetime.datetime.now(), '连接成功回调') + + def on_disconnected(self): + """ + 连接断开 + :return: + """ + print(datetime.datetime.now(), '连接断开回调') + + def on_stock_order(self, order:XtOrder): + """ + 委托回报推送 + :param order: XtOrder对象 + :return: + """ + print(f'orderd {order.strategy_name}-{order.stock_code} {order.order_id} {order.order_volume}-{order.order_status}') + stockCode = order.stock_code + ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + if ctrl is not None and order.strategy_name == ctrl.getName(): + print(f'controller info {ctrl.getName()}') + ctrl.onAsyncOrderResponse(order) # type: ignore + else: + print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'") + + + def on_stock_trade(self, trade:XtTrade): + """ + 成交变动推送 + :param trade: XtTrade对象 + :return: + """ + stockCode = trade.stock_code + ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + if ctrl is not None and trade.strategy_name == ctrl.getName(): + ctrl.onOrderTrade(trade) + else: + print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}") + + # def on_order_stock_async_response(self, response:XtOrderResponse): + # stockCode = response.order_remark + # ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode] + # # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 + # if ctrl is not None and response.strategy_name == ctrl.getName(): + # ctrl.onAsyncOrderResponse(response) + # else: + # print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}") + + def on_order_error(self, order_error): + """ + 委托失败推送 + :param order_error:XtOrderError 对象 + :return: + """ + # print("on order_error callback") + # print(order_error.order_id, order_error.error_id, order_error.error_msg) + print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}") + + + def on_account_status(self, status): + """ + :param response: XtAccountStatus 对象 + :return: + """ + print(datetime.datetime.now(), status) \ No newline at end of file diff --git a/core/strategy_db.py b/core/sfgrid/model.py similarity index 63% rename from core/strategy_db.py rename to core/sfgrid/model.py index 784d021..d17de04 100644 --- a/core/strategy_db.py +++ b/core/sfgrid/model.py @@ -1,19 +1,7 @@ -from peewee import SqliteDatabase, Model, CharField, IntegerField, FloatField, BooleanField +from peewee import CharField, IntegerField, FloatField, BooleanField -from xtquant import xtconstant +from core.database import BaseModel, db -# 连接到SQLite数据库 -db = SqliteDatabase('example.db') - -# 定义基础模型类 -class BaseModel(Model): - class Meta: - database = db - -OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买 -OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖 -OrderTypeInit = "0" # 建仓 -OrderTypeNone = "None" # 定义Target类,对应targets表 class TradeTarget(BaseModel): @@ -33,3 +21,6 @@ class TradeTarget(BaseModel): def targetName(self): return f'{self.stock_name}[{self.stock_code}]' + + +db.create_tables([TradeTarget]) \ No newline at end of file diff --git a/core/objects.py b/core/sfgrid/objects.py similarity index 79% rename from core/objects.py rename to core/sfgrid/objects.py index 2156859..f8a6f47 100644 --- a/core/objects.py +++ b/core/sfgrid/objects.py @@ -1,4 +1,4 @@ -from core.strategy_db import TradeTarget +from core.sfgrid.model import TradeTarget class GridFixData: diff --git a/core/sfgrid_strategy.py b/core/sfgrid/sfgrid_strategy.py similarity index 87% rename from core/sfgrid_strategy.py rename to core/sfgrid/sfgrid_strategy.py index b6b6817..faafe4b 100644 --- a/core/sfgrid_strategy.py +++ b/core/sfgrid/sfgrid_strategy.py @@ -1,18 +1,19 @@ -from core import strategy_db +import core.sfgrid.model as model +from core import constants from core.eventbus import EventTradeTargetUpdate, event_bus -from core.strategy_db import OrderTypeBuy, OrderTypeInit, OrderTypeSell, TradeTarget +from core.constants import OrderTypeBuy, OrderTypeInit, OrderTypeSell from core.util import queryPendingOrder, is_trading_time from xtquant import xttrader, xtconstant from xtquant.xttype import StockAccount, XtOrder, XtTrade -import sfgrid_constants +import sfgrid_config import threading class SFGridStrategy: - def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount): - self.tradeTarget:TradeTarget = tradeTarget + def __init__(self, tradeTarget: model.TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount): + self.tradeTarget:model.TradeTarget = tradeTarget self.xt_trader: xttrader.XtQuantTrader = xt_trader self.account:StockAccount = account self.enabledTrading(bool(tradeTarget.enabled)) # 修复类型兼容性问题 @@ -26,7 +27,7 @@ class SFGridStrategy: self.refreshPlanPrice() self.saveProxy() - def enabledTrading(self, enabled: bool) -> TradeTarget: + def enabledTrading(self, enabled: bool) -> model.TradeTarget: self.tradeTarget.enabled = enabled # type: ignore self.saveProxy() @@ -39,7 +40,7 @@ class SFGridStrategy: else: # 已建仓 # 交易阶段,检查仓位,检查现有订单 print(f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}") - minRequirePosition:int = sfgrid_constants.grid_volume * int(self.tradeTarget.grid_index) # type: ignore + minRequirePosition:int = sfgrid_config.grid_volume * int(self.tradeTarget.grid_index) # type: ignore if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore print(f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}') else: @@ -78,16 +79,16 @@ class SFGridStrategy: index: int = self.tradeTarget.grid_index # pyright: ignore[reportAssignmentType] orderRemark= "" - gridBasePrice = -1 if index>=len(sfgrid_constants.grid_price) or index < 0 else sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType] + gridBasePrice = -1 if index>=len(sfgrid_config.grid_price) or index < 0 else sfgrid_config.grid_price[int(index)] # pyright: ignore[reportArgumentType] - if self.tradeTarget.enabled and self.tradeTarget.status == 0 and lastPrice <= sfgrid_constants.grid_price[1]: # 已启用,未建仓,建仓 - orderPrice = sfgrid_constants.grid_price[index] + if self.tradeTarget.enabled and self.tradeTarget.status == 0 and lastPrice <= sfgrid_config.grid_price[1]: # 已启用,未建仓,建仓 + orderPrice = sfgrid_config.grid_price[index] orderType = xtconstant.STOCK_BUY orderRemark = OrderTypeInit if self.tradeTarget.enabled and self.tradeTarget.status == 1: # 已启用,已建仓,网格单 - lowPrice = -1 if index+1>=len(sfgrid_constants.grid_price) else sfgrid_constants.grid_price[int(index) + 1] # pyright: ignore[reportArgumentType] - highPrice = sfgrid_constants.grid_price[index - 1] + lowPrice = -1 if index+1>=len(sfgrid_config.grid_price) else sfgrid_config.grid_price[int(index) + 1] # pyright: ignore[reportArgumentType] + highPrice = sfgrid_config.grid_price[index - 1] if lastPrice <= lowPrice: # 下下方多单 orderPrice = lowPrice @@ -110,7 +111,7 @@ class SFGridStrategy: self.account, str(self.tradeTarget.stock_code), orderType, - sfgrid_constants.grid_volume, + sfgrid_config.grid_volume, xtconstant.FIX_PRICE, orderPrice, self.getName(), # strategy_name @@ -123,7 +124,7 @@ class SFGridStrategy: orderTypeName = "空单" elif orderRemark == OrderTypeInit: orderTypeName = "建仓单" - print(f' |- {orderTypeName}委托, 单号 {self.tradeTarget.current_order_no}, 网格基准价 {gridBasePrice}, 下单价 {orderPrice}, 下单量 {sfgrid_constants.grid_volume}') + print(f' |- {orderTypeName}委托, 单号 {self.tradeTarget.current_order_no}, 网格基准价 {gridBasePrice}, 下单价 {orderPrice}, 下单量 {sfgrid_config.grid_volume}') finally: print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock') self.saveProxy() @@ -156,13 +157,13 @@ class SFGridStrategy: if not trade.strategy_name == self.getName(): print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]: 不在策略监控范围内{trade.strategy_name}') return - if self.tradeTarget.status == 0 and trade.order_id == self.tradeTarget.current_order_no and trade.order_remark == strategy_db.OrderTypeInit: + if self.tradeTarget.status == 0 and trade.order_id == self.tradeTarget.current_order_no and trade.order_remark == constants.OrderTypeInit: # 此时为建仓成交 self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # 当前持仓数,账户原有持仓不在策略范围内 # type: ignore self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore self.tradeTarget.grid_index = 1 # type: ignore - self.tradeTarget.plan_buy_price = float(sfgrid_constants.grid_price[2]) # type: ignore - self.tradeTarget.plan_sell_price = float(sfgrid_constants.grid_price[0]) # type: ignore + self.tradeTarget.plan_buy_price = float(sfgrid_config.grid_price[2]) # type: ignore + self.tradeTarget.plan_sell_price = float(sfgrid_config.grid_price[0]) # type: ignore self.tradeTarget.status = 1 # type: ignore self.saveProxy() print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓订单ID: {self.tradeTarget.current_order_no}已成交 ") @@ -174,7 +175,7 @@ class SFGridStrategy: self.tradeTarget.current_position = int(self.tradeTarget.current_position) - trade.traded_volume # type: ignore self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) - 1 # type: ignore - print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}\n") # type: ignore + print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {sfgrid_config.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_config.grid_volume} 手续费: {trade.commission}\n") # type: ignore print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}') print(f' 当前持仓: {self.tradeTarget.current_position}') print(f' 网格坐标: {self.tradeTarget.grid_index}') @@ -183,7 +184,7 @@ class SFGridStrategy: self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # type: ignore self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) + 1 # type: ignore - print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {trade.traded_price} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}") + print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_order_no} Price: {trade.traded_price} Volume: {sfgrid_config.grid_volume} 手续费: {trade.commission}") print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}') print(f' 当前持仓: {self.tradeTarget.current_position}') print(f' 网格坐标: {self.tradeTarget.grid_index}') @@ -201,11 +202,11 @@ class SFGridStrategy: buyIdx: int = self.tradeTarget.grid_index + 1 # pyright: ignore[reportAssignmentType] sellIdx: int = self.tradeTarget.grid_index - 1 if self.tradeTarget.grid_index > 0: # 可以下空单 - self.tradeTarget.plan_sell_price = float(sfgrid_constants.grid_price[sellIdx]) # pyright: ignore[reportAttributeAccessIssue] + self.tradeTarget.plan_sell_price = float(sfgrid_config.grid_price[sellIdx]) # pyright: ignore[reportAttributeAccessIssue] else: self.tradeTarget.plan_sell_price = -1.0 # type: ignore - if self.tradeTarget.grid_index < len(sfgrid_constants.grid_price) - 1: - self.tradeTarget.plan_buy_price = float(sfgrid_constants.grid_price[buyIdx]) # pyright: ignore[reportAttributeAccessIssue] + if self.tradeTarget.grid_index < len(sfgrid_config.grid_price) - 1: + self.tradeTarget.plan_buy_price = float(sfgrid_config.grid_price[buyIdx]) # pyright: ignore[reportAttributeAccessIssue] else: self.tradeTarget.plan_buy_price = -1.0 # pyright: ignore[reportAttributeAccessIssue] else: diff --git a/core/ui.py b/core/sfgrid/ui.py similarity index 88% rename from core/ui.py rename to core/sfgrid/ui.py index 33588ff..0a11e71 100644 --- a/core/ui.py +++ b/core/sfgrid/ui.py @@ -8,15 +8,20 @@ import threading import time import core.eventbus as eBus from core.logger import LogData, LogLevel -from core.strategy_db import TradeTarget +from core.sfgrid.model import TradeTarget, db import configparser -import sfgrid_constants -from core.objects import GridFixData +import sfgrid_config +from core.sfgrid.objects import GridFixData from core.util import getInstrumentName -class TradeTargetUI: - def __init__(self): +class TradeTargetUI(ttk.Frame): + def __init__(self, parent, main_window): + super().__init__(parent) + + # 保存主窗口的引用,用于访问全局日志 + self.main_window = main_window + self.tradeTargetData:dict[int, TradeTarget] = {} self.market_data_enabled = False # 添加市场数据监听状态变量 self.ui_refresh_enabled = False # 添加UI刷新线程状态变量 @@ -28,9 +33,6 @@ class TradeTargetUI: # 市场监控数据 self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}} - self.root = tk.Tk() - self.root.title("三疯交易系统") - self.root.geometry("1400x700") # 创建界面 self.create_ui() @@ -56,8 +58,7 @@ class TradeTargetUI: """刷新循环""" while self.refresh_thread_running: # 在主线程中更新UI - if hasattr(self, 'root') and self.root: - self.root.after(0, self.refresh_table) + self.after(0, self.refresh_table) time.sleep(0.5) # 每0.5秒刷新一次 def stop_refresh_thread(self): @@ -104,11 +105,8 @@ class TradeTargetUI: def create_ui(self): """创建UI界面""" - # 创建菜单栏 - self.create_menu_bar() - - # 主框架 - main_frame = ttk.Frame(self.root) + # 主框架(使用self作为父容器) + main_frame = ttk.Frame(self) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建工具栏 @@ -141,15 +139,6 @@ class TradeTargetUI: # ) # self.market_data_switch.pack(side=tk.LEFT, padx=2) - # 日志显示/隐藏按钮 - self.log_toggle_btn = ttk.Button(toolbar_frame, text="📋 显示日志", - command=self.toggle_log_panel, width=12) - self.log_toggle_btn.pack(side=tk.LEFT, padx=2) - - # 添加清空日志按钮 - ttk.Button(toolbar_frame, text="🗑 清空日志", - command=self.clear_logs, width=12).pack(side=tk.LEFT, padx=2) - # 表格区域 self.create_tables_area(main_frame) @@ -180,24 +169,7 @@ class TradeTargetUI: self.refresh_thread_running = False self.add_log(LogLevel.INFO, "UI刷新线程已停止") - def create_menu_bar(self): - """创建菜单栏""" - menubar = tk.Menu(self.root) - self.root.config(menu=menubar) - - # 系统菜单 - system_menu = tk.Menu(menubar, tearoff=0) - menubar.add_cascade(label="系统", menu=system_menu) - system_menu.add_command(label="系统设置", command=self.system_settings) - system_menu.add_separator() - system_menu.add_command(label="退出", command=self.on_exit) - - def on_exit(self): - """退出程序""" - # 停止刷新线程 - self.stop_refresh_thread() - # 关闭窗口 - self.root.destroy() + def create_tables_area(self, parent): """创建表格区域""" @@ -218,15 +190,6 @@ class TradeTargetUI: # 创建市场监控表格 self.create_market_monitor_table(market_frame) - - # 下方操作日志区域(默认隐藏) - self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10) - # 默认不显示,通过工具栏按钮控制 - # self.log_frame.pack(fill=tk.X, pady=(5, 0)) - self.log_visible = False # 日志区域可见性标志 - - # 创建操作日志表格 - self.create_log_table(self.log_frame) def create_trade_target_table(self, parent): """创建交易标的表格""" @@ -395,37 +358,7 @@ class TradeTargetUI: self.trade_table.insert('', tk.END, values=values) - def create_log_table(self, parent): - """创建操作日志表格""" - columns = ("timestamp", "level", "message") - - self.log_table = ttk.Treeview(parent, columns=columns, show='headings', height=8) - - log_column_configs = { - "timestamp": ("时间", 100), - "level": ("级别", 50), - "message": ("消息", 850) - } - - for col in columns: - title, width = log_column_configs[col] - self.log_table.heading(col, text=title) - self.log_table.column(col, width=width, anchor=tk.W) - - # 填充示例日志 - sample_logs = [ - ("2024-01-15 10:30:15", "INFO", "系统启动成功"), - ] - - for log in sample_logs: - self.log_table.insert('', tk.END, values=log) - - # 滚动条 - scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.log_table.yview) - self.log_table.configure(yscrollcommand=scrollbar.set) - - self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + def get_status_text(self, status): """获取状态文本""" @@ -533,20 +466,23 @@ class TradeTargetUI: def add_trade_target(self): """添加新的交易标的""" + # 获取顶层窗口 + root = self.winfo_toplevel() + # 创建顶层窗口 - add_window = tk.Toplevel(self.root) + add_window = tk.Toplevel(root) add_window.title("添加交易标的") add_window.geometry("400x150") add_window.resizable(False, False) # 设置窗口模态 - add_window.transient(self.root) + add_window.transient(root) add_window.grab_set() # 居中显示 - self.root.update_idletasks() - x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 200 - y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 75 + root.update_idletasks() + x = root.winfo_x() + (root.winfo_width() // 2) - 200 + y = root.winfo_y() + (root.winfo_height() // 2) - 75 add_window.geometry(f"400x150+{x}+{y}") # 创建输入框架 @@ -585,18 +521,7 @@ class TradeTargetUI: self.add_log(LogLevel.INFO, "点击添加交易标的按钮") - def toggle_log_panel(self): - """切换日志面板的显示/隐藏""" - if self.log_visible: - # 隐藏日志面板 - self.log_frame.pack_forget() - self.log_visible = False - self.log_toggle_btn.config(text="📋 显示日志") - else: - # 显示日志面板 - self.log_frame.pack(fill=tk.X, pady=(5, 0)) - self.log_visible = True - self.log_toggle_btn.config(text="📋 隐藏日志") + def refresh_table(self): """刷新表格数据""" @@ -626,23 +551,19 @@ class TradeTargetUI: self.populate_market_table() def onLog(self, data:LogData): - self.add_log(data.level, data.message) + # 使用全局日志 + self.main_window.add_log(data.level, data.message) def add_log(self, level:LogLevel, message): - """添加日志记录""" - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.log_table.insert('', 0, values=(timestamp, level.value, message)) - - def clear_logs(self): - """清空日志记录""" - # 删除所有日志项 - for item in self.log_table.get_children(): - self.log_table.delete(item) - self.add_log(LogLevel.DEBUG, "日志已清空") + """添加日志记录 - 转发到全局日志""" + self.main_window.add_log(level, message) def system_settings(self): """系统设置""" - settings_window = tk.Toplevel(self.root) + # 获取顶层窗口 + root = self.winfo_toplevel() + + settings_window = tk.Toplevel(root) settings_window.title("网格交易系统配置") # 设置窗口大小 @@ -650,17 +571,17 @@ class TradeTargetUI: window_height = 550 # 先设置为模态窗口 - settings_window.transient(self.root) + settings_window.transient(root) # 确保主窗口完全初始化 - self.root.update_idletasks() + root.update_idletasks() # 获取主窗口的实际大小(包括边框) # 使用winfo_rootx/rooty获取窗口在屏幕上的绝对位置 - main_x = self.root.winfo_rootx() - main_y = self.root.winfo_rooty() - main_width = self.root.winfo_width() - main_height = self.root.winfo_height() + main_x = root.winfo_rootx() + main_y = root.winfo_rooty() + main_width = root.winfo_width() + main_height = root.winfo_height() # 计算设置窗口相对于主窗口的居中位置 x = main_x + (main_width - window_width) // 2 @@ -691,7 +612,7 @@ class TradeTargetUI: # 读取当前配置 config = configparser.ConfigParser() - config_path = sfgrid_constants.get_config_path() + config_path = sfgrid_config.get_config_path() config.read(config_path, encoding='utf-8') # 创建输入框字典用于保存引用 @@ -918,12 +839,12 @@ class TradeTargetUI: config.set('config', 'account_no', entries['account_no'].get()) # 写入配置文件 - config_path = sfgrid_constants.get_config_path() + config_path = sfgrid_config.get_config_path() with open(config_path, 'w', encoding='utf-8') as configfile: config.write(configfile) # 重新加载配置到内存中 - sfgrid_constants.initConfig() + sfgrid_config.initConfig() messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。") self.add_log(LogLevel.INFO, f"系统配置已更新 - 网格数量: {len(grid_prices)}") @@ -952,20 +873,23 @@ class TradeTargetUI: def create_grid_correction_window(self, target: TradeTarget): """创建网格修正窗口""" + # 获取顶层窗口 + root = self.winfo_toplevel() + # 创建顶层窗口 - correction_window = tk.Toplevel(self.root) + correction_window = tk.Toplevel(root) correction_window.title(f"网格修正 - {target.stock_code} ({target.stock_name})") correction_window.geometry("500x400") correction_window.resizable(False, False) # 设置窗口模态 - correction_window.transient(self.root) + correction_window.transient(root) correction_window.grab_set() # 居中显示 - self.root.update_idletasks() - x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 250 - y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 200 + root.update_idletasks() + x = root.winfo_x() + (root.winfo_width() // 2) - 250 + y = root.winfo_y() + (root.winfo_height() // 2) - 200 correction_window.geometry(f"500x400+{x}+{y}") # 创建主框架 @@ -1005,7 +929,7 @@ class TradeTargetUI: required_position_frame.pack(fill=tk.X, pady=5) grid_index_value = getattr(target, 'grid_index') - required_position = grid_index_value * sfgrid_constants.grid_volume + required_position = grid_index_value * sfgrid_config.grid_volume ttk.Label(required_position_frame, text="需求持仓量:", width=12).pack(side=tk.LEFT) required_position_label = ttk.Label(required_position_frame, text=str(required_position), width=10, anchor=tk.CENTER) required_position_label.pack(side=tk.LEFT, padx=5) @@ -1030,7 +954,7 @@ class TradeTargetUI: # 增加按钮 ttk.Button(grid_index_frame, text="+", width=3, - command=lambda: self.increase_grid_index(grid_index_var, len(sfgrid_constants.grid_price)-1, target, required_position_label, position_status_label)).pack(side=tk.LEFT, padx=(5, 0)) + command=lambda: self.increase_grid_index(grid_index_var, len(sfgrid_config.grid_price)-1, target, required_position_label, position_status_label)).pack(side=tk.LEFT, padx=(5, 0)) # 当前价格(实时更新) price_frame = ttk.Frame(options_frame) @@ -1087,7 +1011,7 @@ class TradeTargetUI: setattr(target, 'grid_index', new_grid_index) # 重新计算需求持仓量 - required_position = new_grid_index * sfgrid_constants.grid_volume + required_position = new_grid_index * sfgrid_config.grid_volume # 检查持仓量是否满足要求 current_position = getattr(target, 'current_position') @@ -1111,9 +1035,7 @@ class TradeTargetUI: # 添加日志 self.add_log(LogLevel.INFO, f"网格修正已保存: {target.stock_code} - {target.stock_name}, 网格序号: {new_grid_index}") - def run(self): - """运行程序""" - self.root.mainloop() + def decrease_grid_index(self, grid_index_var: tk.IntVar, target: TradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label): """减少网格序号""" @@ -1134,7 +1056,7 @@ class TradeTargetUI: def update_required_position_and_status(self, grid_index: int, target: TradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label): """更新需求持仓量和持仓状态""" # 计算需求持仓量 - required_position = grid_index * sfgrid_constants.grid_volume + required_position = grid_index * sfgrid_config.grid_volume required_position_label.config(text=str(required_position)) # 更新持仓量状态 diff --git a/core/util.py b/core/util.py index 05052ef..1371717 100644 --- a/core/util.py +++ b/core/util.py @@ -1,4 +1,4 @@ -import sfgrid_constants +import sfgrid_config import xtquant.xtconstant as xtconstant from xtquant import xtdata, xttrader from xtquant.xttype import StockAccount, XtOrder, XtPosition @@ -58,7 +58,7 @@ def getStockPosition(stock_code: str, xt_trader: xttrader.XtQuantTrader, account return volume def minPosition(gridIndex:int): - return sfgrid_constants.grid_volume * gridIndex + return sfgrid_config.grid_volume * gridIndex def queryPendingOrder(stock_code:str, tag: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount) -> list[XtOrder]: if stock_code == None or tag == None: diff --git a/sfgrid_constants.py b/sfgrid_config.py similarity index 89% rename from sfgrid_constants.py rename to sfgrid_config.py index 1358a93..92afdaf 100644 --- a/sfgrid_constants.py +++ b/sfgrid_config.py @@ -1,6 +1,6 @@ from typing import List import configparser -import os +from pathlib import Path import sys # miniQMTPath = r'D:\\Programs\\DTQMT_MN\\userdata_mini' # miniQMT软件的安装路径 @@ -10,20 +10,21 @@ miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路 grid_price:List[float] = [] # 网格价格设置,从高到低 grid_volume:int = 100 # 每个网格的交易手数 account_no:str = '99082560' +console_log = True # account_no:str = '89009170' # 交易账号 -def get_config_path(): +def get_config_path() -> Path: """获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)""" if getattr(sys, 'frozen', False): # 打包后的可执行文件环境 # sys._MEIPASS是PyInstaller解压临时文件的目录 # 配置文件应该放在可执行文件同目录下 - base_path = os.path.dirname(sys.executable) + base_path = Path(sys.executable).parent else: # 开发环境 - base_path = os.path.dirname(os.path.abspath(__file__)) + base_path = Path(__file__).resolve().parent - return os.path.join(base_path, 'config.ini') + return base_path / 'config.ini' def create_default_config(): """创建默认配置文件""" @@ -46,7 +47,7 @@ def initConfig(): config_path = get_config_path() # 检查配置文件是否存在,不存在则创建 - if not os.path.exists(config_path): + if not config_path.exists(): create_default_config() config = configparser.ConfigParser() diff --git a/starter.py b/starter.py index be7e0b3..83c3b7c 100644 --- a/starter.py +++ b/starter.py @@ -1,20 +1,21 @@ # coding:utf-8 -from core import strategy_db -from core.main_controller import SFGridController +from core.database import db +from core.main_ui import MainWindow +from core.sfgrid.main_controller import SFGridController from core.logger import LogLevel, PrintLog -import sfgrid_constants as sdConstants +import sfgrid_config as sdConstants -def startTrade(index: int): - ctrl.start_stock_trade(index) +# def startTrade(index: int): +# ctrl.start_stock_trade(index) -def pauseTrade(index: int): - ctrl.pause_stock_trade(index) +# def pauseTrade(index: int): +# ctrl.pause_stock_trade(index) if __name__ == '__main__': sdConstants.initConfig() - strategy_db.db.connect() - strategy_db.db.create_tables([strategy_db.TradeTarget]) - PrintLog(LogLevel.INFO, '- [成功]数据库模块初始化') - ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath) + # ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath) - ctrl.hold() + # ctrl.hold() + + window = MainWindow() + window.run() diff --git a/starter.spec b/starter.spec index 935fc84..b44d58c 100644 --- a/starter.spec +++ b/starter.spec @@ -28,7 +28,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=False, + console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None,