diff --git a/config.ini b/config.ini index 2a36d57..35e46b5 100644 --- a/config.ini +++ b/config.ini @@ -1,9 +1,7 @@ [config] -; miniqmtpath = /Users/gao/Workspace/quant -miniQMTPath=D:\\Programs\\DTQMT\\userdata_mini -; grid_price = 10.9,10.0,9.1,8.2,7.3,6.4,5.5,4.6,3.7,2.8,1.9,1.0 -grid_price=1.665,1.660,1.655,1.650,1.645,1.640,1.635,1.630,1.625,1.620,1.615,1.610 -grid_volume = 200 +miniqmtpath = D:/Programs/DTQMT/userdata_mini +grid_price = 11.0,10.0,9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0,0.0 +grid_volume = 100 account_no = 99082560 max_enabled_targets = 10 diff --git a/core/eventbus.py b/core/eventbus.py new file mode 100644 index 0000000..eeba1a0 --- /dev/null +++ b/core/eventbus.py @@ -0,0 +1,37 @@ +# 定义事件处理函数 + +MarketDataUpdate = "market_data_update" +ActionEventEnableTrade = "enable_trade" +ResultEventTradeEnabled = "trade_enabled" +ActionEventDisableTrade = "disable_trade" +ResultEventTradeDisabled = "trade_disabled" +# 市场数据监听控制事件 +ActionEnableMarketData = "enable_market_data" +ActionDisableMarketData = "disable_market_data" +MarketDataEnabled = "market_data_enabled" +MarketDataDisabled = "market_data_disabled" + + +class EventBus: + def __init__(self): + self.listeners = {} # 管理各种event的订阅情况 + + def subscribe(self, event_type, listener): + if event_type not in self.listeners: + self.listeners[event_type] = [] + self.listeners[event_type].append(listener) + + def publish(self, event_type, data): + if event_type in self.listeners: + for listener in self.listeners[event_type]: + listener(data) + + +# # 订阅事件 +# event_bus.subscribe('my_event', handle_event) + +# # 发布事件 +# event_bus.publish('my_event', {'key': 'value'}) + +# 创建事件总线实例 +event_bus = EventBus() \ No newline at end of file diff --git a/core/main_controller.py b/core/main_controller.py index 60cfe06..e58f4f1 100644 --- a/core/main_controller.py +++ b/core/main_controller.py @@ -1,10 +1,12 @@ # coding:utf-8 +from core.strategy_db import TradeTarget +from typing import Any +from core.eventbus import ActionDisableMarketData, ActionEnableMarketData, ActionEventDisableTrade, ActionEventEnableTrade, MarketDataUpdate, MarketDataEnabled, MarketDataDisabled, ResultEventTradeDisabled, ResultEventTradeEnabled, event_bus from xtquant.xttrader import XtQuantTrader -import time, sys +import time from peewee import ModelSelect import xtquant.xtconstant as xtconstant -sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore import core.strategy_db as strategy_db import sfgrid_constants from core.sfgrid_strategy import SFGridStrategy @@ -14,11 +16,15 @@ from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtOrderResponse, XtPo from xtquant import xtdata from xtquant.xttrader import XtQuantTraderCallback import datetime +import core.ui as ui # 量化核心控制对象 class SFGridController(XtQuantTraderCallback): def __init__(self, account_no: str, miniQmtPath: str): super().__init__() + + self.registerEventHandler() + self.appUi = ui.TradeTargetUI() xtdata.enable_hello = False @@ -49,8 +55,35 @@ class SFGridController(XtQuantTraderCallback): self.seq = None print('- [成功]三疯交易系统初始化完成') + # 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("add_trade_target", self.onAddTradeTarget) + + 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 hold(self): + self.appUi.run() def startMarketData(self): print('- 启动市场数据订阅') @@ -58,6 +91,7 @@ class SFGridController(XtQuantTraderCallback): if self.seq == -1: print('- 市场数据订阅失败') else: + event_bus.publish(MarketDataEnabled, True) print(f'- 市场数据订阅成功, 订阅号={self.seq}') @@ -65,11 +99,22 @@ class SFGridController(XtQuantTraderCallback): print('- 停止市场数据订阅') 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: + print(f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确') + return + + # 检查是否已存在该标的 + existing_target = strategy_db.TradeTarget.get_or_none(strategy_db.TradeTarget.stock_code == stock_code) + if existing_target: + print(f'交易标的 {stock_code} {stock_name} 已存在') + return + new_target = strategy_db.TradeTarget.create( stock_name=stock_name, stock_code=stock_code, @@ -96,8 +141,8 @@ class SFGridController(XtQuantTraderCallback): print(f'新增交易标的失败 {stock_code} {e}') - def del_trade_target(self, index:int): - target: strategy_db.TradeTarget = self.instrument_pool[index] + def del_trade_target(self, id:int): + target: strategy_db.TradeTarget = self.instrument_pool[id] # self.stock_trade_ctrl. del self.stock_trade_ctrl[target.stock_code] target.delete_instance() @@ -107,28 +152,33 @@ class SFGridController(XtQuantTraderCallback): def init_instrument_pool(self, xtTrader:XtQuantTrader, account:StockAccount): self.refresh_targets() - for temp in self.instrument_pool: - tradeTarget:strategy_db.TradeTarget = temp + for id in self.instrument_pool: + tradeTarget:strategy_db.TradeTarget = self.instrument_pool[id] tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore result = tradeTarget.save() print(f' |- 同步当前持仓信息 {tradeTarget.stock_code}, {tradeTarget.current_position}, result = {result}') - stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore + stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account) # type: ignore self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController + event_bus.publish(MarketDataUpdate, tradeTarget) print(f'- [成功]交易标的信息初始化, 共 {len(self.instrument_pool)} 个标的') def refresh_targets(self): # 更新标的池 - self.instrument_pool:ModelSelect = strategy_db.TradeTarget.select() + results:ModelSelect = strategy_db.TradeTarget.select() + self.instrument_pool: dict[int, strategy_db.TradeTarget] = {} + for temp in results: + result :strategy_db.TradeTarget = temp + self.instrument_pool[result.get_id()] = result self.print_pool() def print_pool(self): print("- [信息]标的池信息") - for i in range(len(self.instrument_pool)): - target: strategy_db.TradeTarget = self.instrument_pool[i] + for id in self.instrument_pool: + target: strategy_db.TradeTarget = self.instrument_pool[id] status = "新建" if target.status == 0 else "已建初始仓" - print(f' [序号-{i}] 股票代码: {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 + print(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 def print_position_info(self): positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account) @@ -173,34 +223,29 @@ class SFGridController(XtQuantTraderCallback): # 初始化指定标的交易控制器 - def start_stock_trade(self, index: int): - tradeTarget = self.instrument_pool[index] + 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] - if tradeController.isEnabled(): - print(f"标的交易控制器已存在且正在运行 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n") - else: - print(f"标的交易控制器已存在但未运行,重新启动 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n") - tradeController.enabledTrading(True) + + tradeTarget = tradeController.enabledTrading(True) + self.instrument_pool[id] = tradeTarget + event_bus.publish(ResultEventTradeEnabled, tradeTarget) else: - stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore - self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController print(f"\t创建标的交易控制器 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}") - def pause_stock_trade(self, index: int): - tradeTarget = self.instrument_pool[index] - if tradeTarget.stock_code in self.stock_trade_ctrl: - tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code] - if tradeController.isEnabled(): - print(f"暂停标的交易 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n") - tradeController.enabledTrading(False) - else: - print(f"标的交易已暂停 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n") + def pause_stock_trade(self, id: int): + localTarget: strategy_db.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) + self.instrument_pool[id] = tradeTarget + event_bus.publish(ResultEventTradeDisabled, tradeTarget) else: - print(f"标的交易控制器不存在 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n") - + print(f"标的交易控制器不存在 {localTarget.stock_code} {localTarget.stock_name}\n") # ====== 市场回调方法 -- 以下方法由XtQuantData调用 ====== @@ -213,8 +258,8 @@ class SFGridController(XtQuantTraderCallback): self.add_trade_target(stock_code) self.stock_trade_ctrl[stock_code].enabledTrading(True) else: # 指定目标 当前主要使用这种模式 - for target in self.instrument_pool: - stock_code = target.stock_code + for id in self.instrument_pool: + stock_code = self.instrument_pool[id].stock_code # 如果存在对应的StockTradeController,则调用其onDataUpdate方法 if stock_code not in self.stock_trade_ctrl or stock_code not in data: # print(f"股票代码 {stock_code} 未在交易控制器中找到,跳过处理。\n") @@ -251,8 +296,8 @@ class SFGridController(XtQuantTraderCallback): else: print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'") - def test_sim_trade(self, index: int, orderType: int): - tradeTarget:strategy_db.TradeTarget = self.instrument_pool[index] + def test_sim_trade(self, id: int, orderType: int): + tradeTarget:strategy_db.TradeTarget = self.instrument_pool[id] ctrl:SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code] trade: XtTrade = None # type: ignore if orderType == xtconstant.STOCK_BUY: @@ -260,7 +305,7 @@ class SFGridController(XtQuantTraderCallback): sfgrid_constants.account_no, '300083.SZ', xtconstant.STOCK_BUY, - 1, 1, tradeTarget.current_buy_price, sfgrid_constants.grid_volume, 1000, + 1, 1, tradeTarget.plan_buy_price, sfgrid_constants.grid_volume, 1000, tradeTarget.current_buy_order_no, None, ctrl.getName(), None, None, None, None, None, tradeTarget.stock_name) else: @@ -306,5 +351,4 @@ class SFGridController(XtQuantTraderCallback): :param response: XtAccountStatus 对象 :return: """ - print(datetime.datetime.now(), sys._getframe().f_code.co_name) - + print(datetime.datetime.now(), status) \ No newline at end of file diff --git a/core/sfgrid_strategy.py b/core/sfgrid_strategy.py index 0b351d7..9536c8e 100644 --- a/core/sfgrid_strategy.py +++ b/core/sfgrid_strategy.py @@ -1,5 +1,4 @@ -from re import L -from core import util +from core.eventbus import MarketDataUpdate, event_bus from core.strategy_db import TradeTarget from core.util import queryPendingOrder @@ -11,42 +10,49 @@ import threading class SFGridStrategy: - def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount, enabled: bool = False): + def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount): self.tradeTarget:TradeTarget = tradeTarget self.xt_trader: xttrader.XtQuantTrader = xt_trader self.account:StockAccount = account - self.enabledTrading(enabled) + self.enabledTrading(bool(tradeTarget.enabled)) # 修复类型兼容性问题 self.dataUpdateLock = threading.Lock() + print(f'标的{self.tradeTarget.targetName()}交易启动状态 {self.tradeTarget.enabled}') def getName(self): return "SFGRID" + + def saveProxy(self): + rc = self.tradeTarget.save() + event_bus.publish(MarketDataUpdate, self.tradeTarget) + return rc - def enabledTrading(self, enabled: bool): + def enabledTrading(self, enabled: bool) -> TradeTarget: self.tradeTarget.enabled = enabled # type: ignore - self.tradeTarget.save() - pendingOrders = queryPendingOrder(str(self.tradeTarget.stock_code),self.getName(), self.xt_trader,self.account) - - if len(pendingOrders) > 0: - print(f' |- 已存在{len(pendingOrders)}订单,全部取消,按需要重下。') - for order in pendingOrders: - self.xt_trader.cancel_order_stock(self.account, order.order_id) + self.saveProxy() if enabled: print(f" |- 标的{self.tradeTarget.targetName()}交易启动, position {self.tradeTarget.current_position}") # 建仓状态检查 if int(self.tradeTarget.current_position) == 0 and int(self.tradeTarget.status) == 0: # type: ignore self.tradeTarget.grid_index = 1 # type: ignore - self.tradeTarget.save() - self.initBuyOrderId = self.xt_trader.order_stock_async( - self.account, - str(self.tradeTarget.stock_code), - xtconstant.STOCK_BUY, - sfgrid_constants.grid_volume, - xtconstant.FIX_PRICE, - sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)], # type: ignore - 'sf_grid', f'{self.tradeTarget.stock_code}_init_buy') - print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓 买单已发出 InitBuyOrderSeq: {self.initBuyOrderId} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume}\n") # type: ignore + self.saveProxy() + + orders = queryPendingOrder(str(self.tradeTarget.stock_code),self.getName(), self.xt_trader,self.account) + if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == sfgrid_constants.grid_price[1]]) > 0: + # 已存在未交易的多单 + order = [order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == sfgrid_constants.grid_price[1]][0] + print(f' |- 已存在未交易的建仓单,不重复下单:{order.order_id}') + else: + self.initBuyOrderId = self.xt_trader.order_stock_async( + self.account, + str(self.tradeTarget.stock_code), + xtconstant.STOCK_BUY, + sfgrid_constants.grid_volume, + xtconstant.FIX_PRICE, + sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)], # type: ignore + self.getName(), f'{self.tradeTarget.stock_code}_init_buy') + print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓 买单已发出 InitBuyOrderSeq: {self.initBuyOrderId} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume}\n") # type: ignore else: # 交易阶段,检查仓位,检查现有订单 print(f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}") @@ -55,68 +61,78 @@ class SFGridStrategy: print(f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}') else: print(f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}') - + else: + print(f" |- 标的{self.tradeTarget.targetName()}交易监控暂停") + return self.tradeTarget def isEnabled(self) -> bool: - return bool(self.tradeTarget.enabled) + print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}') + return bool(self.tradeTarget.enabled) # 修复返回类型问题 def onDataUpdate(self, data): print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - START') - self.dataUpdateLock.acquire() - print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - LOCKED') - try: - index = self.tradeTarget.grid_index - price = -1 if index>=len(sfgrid_constants.grid_price) else sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType] - 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[int(index) - 1] # pyright: ignore[reportArgumentType] + + if self.tradeTarget.enabled and self.tradeTarget.status == 1: # 交易中 + self.dataUpdateLock.acquire() + print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - LOCKED') + try: + index = self.tradeTarget.grid_index + price = -1 if index>=len(sfgrid_constants.grid_price) else sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType] + 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[int(index) - 1] # pyright: ignore[reportArgumentType] - lastPrice = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice'])) - print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 价格: {lastPrice}, 网格序号: {index}, 网格价格: {price}, 计划多单价: {lowPrice}, 计划空单价: {highPrice}") - - if lastPrice <= lowPrice: # 下下方多单 - orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account) - if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == lowPrice]) > 0: - # 已存在未交易的多单 - print(f' |- 已存在未交易的多单,不重复下单') - else: - print(f' |- 下网格多单') - self.tradeTarget.current_buy_order_no = self.xt_trader.order_stock_async( - self.account, - str(self.tradeTarget.stock_code), - xtconstant.STOCK_BUY, - sfgrid_constants.grid_volume, - xtconstant.FIX_PRICE, - lowPrice, - self.getName(), # strategy_name - self.tradeTarget.stock_code # remark # type: ignore - ) - self.tradeTarget.current_buy_price = float(lowPrice) # type: ignore - print(f' |- 下网格多单号 {self.tradeTarget.current_buy_order_no}, 网格基准价 {price}, 下单价 {lowPrice}, 下单量 {sfgrid_constants.grid_volume}') - elif lastPrice == highPrice: # 下上方空单 - orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account) - if len([order for order in orders if order.order_type == xtconstant.STOCK_SELL and order.price == highPrice]) > 0: - # 已存在未交易的空单 - print(f' |- 已存在未交易的空单,不重复下单') - else: - print(f' |- 下网格空单') - self.tradeTarget.current_sell_order_no = self.xt_trader.order_stock_async( + lastPrice = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice'])) + self.tradeTarget.market_price = lastPrice # type: ignore + print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 价格: {lastPrice}, 网格序号: {index}, 网格价格: {price}, 计划多单价: {lowPrice}, 计划空单价: {highPrice}") + + if lastPrice <= lowPrice: # 下下方多单 + orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account) + if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == lowPrice]) > 0: + # 已存在未交易的多单 + print(f' |- 已存在未交易的多单,不重复下单') + else: + print(f' |- 下网格多单') + self.tradeTarget.current_buy_order_no = self.xt_trader.order_stock_async( self.account, str(self.tradeTarget.stock_code), - xtconstant.STOCK_SELL, + xtconstant.STOCK_BUY, sfgrid_constants.grid_volume, xtconstant.FIX_PRICE, - highPrice, - self.getName(), - self.tradeTarget.stock_code) # type: ignore - self.tradeTarget.current_sell_price = float(highPrice) # type: ignore - print(f' |- 下网格空单号 {self.tradeTarget.current_sell_order_no}, 网格基准价 {price}, 下单价 {highPrice}, 下单量 {sfgrid_constants.grid_volume}') - self.tradeTarget.save() - finally: - print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock') - self.dataUpdateLock.release() - print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - END') - - + lowPrice, + self.getName(), # strategy_name + self.tradeTarget.stock_code # remark # type: ignore + ) + self.tradeTarget.plan_buy_price = float(lowPrice) # type: ignore + print(f' |- 下网格多单号 {self.tradeTarget.current_buy_order_no}, 网格基准价 {price}, 下单价 {lowPrice}, 下单量 {sfgrid_constants.grid_volume}') + elif lastPrice == highPrice: # 下上方空单 + orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account) + if len([order for order in orders if order.order_type == xtconstant.STOCK_SELL and order.price == highPrice]) > 0: + # 已存在未交易的空单 + print(f' |- 已存在未交易的空单,不重复下单') + else: + print(f' |- 下网格空单') + self.tradeTarget.current_sell_order_no = self.xt_trader.order_stock_async( + self.account, + str(self.tradeTarget.stock_code), + xtconstant.STOCK_SELL, + sfgrid_constants.grid_volume, + xtconstant.FIX_PRICE, + highPrice, + self.getName(), + self.tradeTarget.stock_code) # type: ignore + self.tradeTarget.plan_sell_price = float(highPrice) # type: ignore + print(f' |- 下网格空单号 {self.tradeTarget.current_sell_order_no}, 网格基准价 {price}, 下单价 {highPrice}, 下单量 {sfgrid_constants.grid_volume}') + finally: + print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock') + event_bus.publish('market_data', self.tradeTarget) + self.saveProxy() + self.dataUpdateLock.release() + print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - END') + elif self.tradeTarget.enabled and self.tradeTarget.status == 0: # 交易中 + print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 交易中但未建初始仓,仅更新市场价") + self.tradeTarget.market_price = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice'])) # type: ignore + self.saveProxy() + def onAsyncOrderResponse(self, order:XtOrderResponse): print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:START') self.dataUpdateLock.acquire() @@ -131,7 +147,7 @@ class SFGridStrategy: self.tradeTarget.current_sell_order_no = order.order_id else: print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]: 不在策略监控范围内') - rc = self.tradeTarget.save() + rc = self.saveProxy() finally: print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:release lock') self.dataUpdateLock.release() @@ -148,7 +164,7 @@ class SFGridStrategy: self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore self.tradeTarget.grid_index = 1 # type: ignore self.tradeTarget.status = 1 # type: ignore - self.tradeTarget.save() + self.saveProxy() print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓订单ID: {self.initBuyOrderId}已成交 ") print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}') print(f' 当前持仓: {self.tradeTarget.current_position}') @@ -158,7 +174,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 - self.tradeTarget.save() + self.saveProxy() print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_sell_order_no} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}\n") # type: ignore print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}') print(f' 当前持仓: {self.tradeTarget.current_position}') @@ -168,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 - self.tradeTarget.save() + self.saveProxy() print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_buy_order_no} Price: {trade.traded_price} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}") print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}') print(f' 当前持仓: {self.tradeTarget.current_position}') diff --git a/core/strategy_db.py b/core/strategy_db.py index afaf972..d176740 100644 --- a/core/strategy_db.py +++ b/core/strategy_db.py @@ -14,10 +14,11 @@ class TradeTarget(BaseModel): stock_name = CharField() current_position = IntegerField() grid_index = IntegerField() + market_price = FloatField() last_trade_price = FloatField() - current_buy_price = FloatField() + plan_buy_price = FloatField() current_buy_order_no = CharField(default='') - current_sell_price = FloatField() + plan_sell_price = FloatField() current_sell_order_no = CharField(default='') status = IntegerField(default=0) # 0表示新标的,1表示已建初始仓,正常交易中 enabled = BooleanField(default=False) # 是否启动交易线程 diff --git a/ui.py b/core/ui.py similarity index 79% rename from ui.py rename to core/ui.py index 3c15b18..cdc57ba 100644 --- a/ui.py +++ b/core/ui.py @@ -1,26 +1,48 @@ -import random import tkinter as tk from tkinter import ttk, messagebox, filedialog -from typing import List, Optional from datetime import datetime +from core.eventbus import ActionDisableMarketData, ActionEnableMarketData, ActionEventDisableTrade, ActionEventEnableTrade, MarketDataUpdate, MarketDataEnabled, MarketDataDisabled, ResultEventTradeDisabled, ResultEventTradeEnabled, event_bus from core.strategy_db import TradeTarget import configparser import sfgrid_constants + class TradeTargetUI: - def __init__(self, trade_targets: List[TradeTarget]): - self.data:dict[str, TradeTarget] = {} - for temp in trade_targets: - target:TradeTarget = temp - self.data[str(target.get_id())] = target + def __init__(self): + self.data:dict[int, TradeTarget] = {} + self.market_data_enabled = True # 添加市场数据监听状态变量 + self.registerEventHandler() self.root = tk.Tk() self.root.title("三疯交易系统") self.root.geometry("1200x700") - # 创建界面 self.create_ui() + def registerEventHandler(self): + event_bus.subscribe(MarketDataUpdate, self.onTradeTargetUpdated) + event_bus.subscribe(ResultEventTradeEnabled, self.onTradeEnabled) + event_bus.subscribe(ResultEventTradeDisabled, self.onTradeDisabled) + event_bus.subscribe(MarketDataEnabled, self.onMarketDataToggled) + event_bus.subscribe(MarketDataDisabled, self.onMarketDataToggled) + + def onMarketDataToggled(self, data:bool): + self.market_data_enabled = self.market_data_switch_var.get() + self.add_log("INFO", "市场数据监听已" + ("启用" if data else "禁用")) + + def onTradeEnabled(self, target:TradeTarget): + self.add_log("INFO", f"交易启用: {target.stock_code} - {target.stock_name}") + + def onTradeDisabled(self, target:TradeTarget): + self.add_log("INFO", f"交易禁用: {target.stock_code} - {target.stock_name}") + + + def onTradeTargetUpdated(self, target:TradeTarget): + # 更新或添加数据到本地缓存 + self.data[target.get_id()] = target + # 刷新表格显示 + self.refresh_table() + def create_ui(self): """创建UI界面""" # 创建菜单栏 @@ -29,10 +51,56 @@ class TradeTargetUI: # 主框架 main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 创建工具栏 + toolbar_frame = ttk.Frame(main_frame) + toolbar_frame.pack(fill=tk.X, pady=(0, 10)) + + # 工具栏按钮 + ttk.Button(toolbar_frame, text="▶️ 启动交易", + command=self.start_selected_trade, width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar_frame, text="⏸ 暂停交易", + command=self.pause_selected_trade, width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar_frame, text="➕ 添加标的", + command=self.add_trade_target, width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(toolbar_frame, text="🗑 删除标的", + command=self.delete_selected_trade, width=12).pack(side=tk.LEFT, padx=2) + + # 添加分隔符 + ttk.Separator(toolbar_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=10) + + # 市场数据监听开关 + self.market_data_switch_var = tk.BooleanVar(value=True) + self.market_data_switch = ttk.Checkbutton( + toolbar_frame, + text="📊 市场数据", + variable=self.market_data_switch_var, + command=self.toggle_market_data, + width=12 + ) + 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) + def toggle_market_data(self): + """切换市场数据监听状态""" + print(f'市场数据监听开关') + self.market_data_enabled = self.market_data_switch_var.get() + if self.market_data_enabled: + event_bus.publish(ActionEnableMarketData, True) + else: + event_bus.publish(ActionDisableMarketData, True) + def create_menu_bar(self): """创建菜单栏""" menubar = tk.Menu(self.root) @@ -65,33 +133,9 @@ class TradeTargetUI: def create_trade_target_table(self, parent): """创建交易标的表格""" - # 创建工具栏 - toolbar_frame = ttk.Frame(parent) - toolbar_frame.pack(fill=tk.X, pady=(0, 10)) - - # 工具栏按钮 - ttk.Button(toolbar_frame, text="▶️ 启动交易", - command=self.start_selected_trade, width=12).pack(side=tk.LEFT, padx=2) - ttk.Button(toolbar_frame, text="⏸ 暂停交易", - command=self.pause_selected_trade, width=12).pack(side=tk.LEFT, padx=2) - ttk.Button(toolbar_frame, text="➕ 添加标的", - command=self.add_trade_target, width=12).pack(side=tk.LEFT, padx=2) - ttk.Button(toolbar_frame, text="🗑 删除标的", - command=self.delete_selected_trade, width=12).pack(side=tk.LEFT, padx=2) - - # 添加分隔符 - ttk.Separator(toolbar_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=10) - - # 日志显示/隐藏按钮 - 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.Separator(parent, orient='horizontal').pack(fill=tk.X, pady=5) columns = ("ID", - "股票代码", "股票名称", "持仓数量", "网格索引", + "股票代码", "股票名称", "市场价", "持仓数量", "网格索引", "最新成交价", "计划买入价", "买入订单号", "计划卖出价", "卖出订单号", "启用状态", "交易状态" ) @@ -103,15 +147,16 @@ class TradeTargetUI: "ID": (50, tk.CENTER), "股票代码": (90, tk.CENTER), "股票名称": (100, tk.CENTER), + "市场价": (60, tk.CENTER), "持仓数量": (90, tk.CENTER), - "网格索引": (80, tk.CENTER), - "最新成交价": (100, tk.CENTER), - "计划买入价": (100, tk.CENTER), + "网格索引": (50, tk.CENTER), + "最新成交价": (60, tk.CENTER), + "计划买入价": (60, tk.CENTER), "买入订单号": (100, tk.CENTER), - "计划卖出价": (100, tk.CENTER), + "计划卖出价": (60, tk.CENTER), "卖出订单号": (100, tk.CENTER), - "启用状态": (80, tk.CENTER), - "交易状态": (80, tk.CENTER) + "启用状态": (100, tk.CENTER), + "交易状态": (100, tk.CENTER) } for col in columns: @@ -143,9 +188,9 @@ class TradeTargetUI: else: return "🔴 错误状态" - def get_trade_status_indicator(self, status: int) -> str: + def get_trade_enabled_indicator(self, enabled: bool) -> str: """获取交易状态指示器""" - if status == 1: + if enabled: return "🟢 策略运行" else: return "🟡 策略暂停" @@ -158,15 +203,16 @@ class TradeTargetUI: target.id, # type: ignore target.stock_code, target.stock_name, + f"{target.market_price:.3f}", target.current_position, target.grid_index, f"{target.last_trade_price:.2f}", - f"{target.current_buy_price:.2f}", + f"{target.plan_buy_price:.2f}", target.current_buy_order_no, - f"{target.current_sell_price:.2f}", + f"{target.plan_sell_price:.2f}", target.current_sell_order_no, self.get_status_indicator(target), - self.get_trade_status_indicator(target.status) # type: ignore + self.get_trade_enabled_indicator(target.enabled) # type: ignore ] self.trade_table.insert('', tk.END, values=values) @@ -178,9 +224,9 @@ class TradeTargetUI: self.log_table = ttk.Treeview(parent, columns=columns, show='headings', height=8) log_column_configs = { - "timestamp": ("时间", 120), - "level": ("级别", 60), - "message": ("消息", 200) + "timestamp": ("时间", 100), + "level": ("级别", 50), + "message": ("消息", 850) } for col in columns: @@ -191,12 +237,6 @@ class TradeTargetUI: # 填充示例日志 sample_logs = [ ("2024-01-15 10:30:15", "INFO", "系统启动成功"), - ("2024-01-15 10:31:22", "DEBUG", "加载交易标的: 5个"), - ("2024-01-15 10:32:45", "INFO", "000001 - 网格交易线程启动"), - ("2024-01-15 10:33:10", "WARNING", "601318 - 未启用交易"), - ("2024-01-15 10:34:30", "ERROR", "300750 - 订单提交失败"), - ("2024-01-15 10:35:18", "INFO", "600036 - 买入订单创建成功"), - ("2024-01-15 10:36:05", "INFO", "数据刷新完成") ] for log in sample_logs: @@ -239,11 +279,15 @@ class TradeTargetUI: # 从列表中找到对应的target对象 for id in self.data: - if target_id == id: # type: ignore + if int(target_id) == id: # type: ignore return self.data[id] return None - + + def onLog(self, level: str, message: str): + """接收外部日志消息并显示在日志组件中""" + self.add_log(level, message) + def start_selected_trade(self): """启动选中的交易""" target = self.get_selected_target() @@ -263,9 +307,13 @@ class TradeTargetUI: if result: target.enabled = True # type: ignore - self.add_log("INFO", f"已启动交易: {target.stock_code} - {target.stock_name}") - self.refresh_table() - messagebox.showinfo("启动成功", f"已启动 {target.stock_code} ({target.stock_name}) 的交易") + event_bus.publish(ActionEventEnableTrade, target.get_id()) + # self.add_log("INFO", f"已启动交易: {target.stock_code} - {target.stock_name}") + # self.refresh_table() + # messagebox.showinfo("启动成功", f"已启动 {target.stock_code} ({target.stock_name}) 的交易") + + def on_trade_enabled(self, target: TradeTarget): + event_bus.publish(ActionEventEnableTrade, target) def pause_selected_trade(self): """暂停选中的交易""" @@ -286,9 +334,10 @@ class TradeTargetUI: if result: target.enabled = False # type: ignore - self.add_log("INFO", f"已暂停交易: {target.stock_code} - {target.stock_name}") - self.refresh_table() - messagebox.showinfo("暂停成功", f"已暂停 {target.stock_code} ({target.stock_name}) 的交易") + event_bus.publish(ActionEventDisableTrade, target.get_id()) + # self.add_log("INFO", f"已暂停交易: {target.stock_code} - {target.stock_name}") + # self.refresh_table() + # messagebox.showinfo("暂停成功", f"已暂停 {target.stock_code} ({target.stock_name}) 的交易") def delete_selected_trade(self): """删除选中的交易标的""" @@ -317,8 +366,56 @@ class TradeTargetUI: def add_trade_target(self): """添加新的交易标的""" - # TODO: 实现添加交易标的的对话框 - messagebox.showinfo("提示", "添加交易标的功能待实现") + # 创建顶层窗口 + add_window = tk.Toplevel(self.root) + add_window.title("添加交易标的") + add_window.geometry("400x150") + add_window.resizable(False, False) + + # 设置窗口模态 + add_window.transient(self.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 + add_window.geometry(f"400x150+{x}+{y}") + + # 创建输入框架 + input_frame = ttk.Frame(add_window, padding=20) + input_frame.pack(fill=tk.BOTH, expand=True) + + # 股票代码输入 + ttk.Label(input_frame, text="股票代码:").grid(row=0, column=0, sticky=tk.W, pady=5) + stock_code_entry = ttk.Entry(input_frame, width=30) + stock_code_entry.grid(row=0, column=1, pady=5, padx=(10, 0)) + stock_code_entry.focus() + + # 按钮框架 + button_frame = ttk.Frame(input_frame) + button_frame.grid(row=1, column=0, columnspan=2, pady=20) + + def confirm_add(): + stock_code = stock_code_entry.get().strip() + if not stock_code: + messagebox.showwarning("输入错误", "请输入股票代码") + return + + # 发布事件通知主控制器添加标的 + event_bus.publish("add_trade_target", stock_code) + add_window.destroy() + + def cancel_add(): + add_window.destroy() + + # 确认和取消按钮 + ttk.Button(button_frame, text="确认", command=confirm_add, width=10).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="取消", command=cancel_add, width=10).pack(side=tk.LEFT, padx=5) + + # 绑定回车键确认 + stock_code_entry.bind('', lambda event: confirm_add()) + self.add_log("INFO", "点击添加交易标的按钮") def toggle_log_panel(self): @@ -348,6 +445,13 @@ class TradeTargetUI: timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.log_table.insert('', 0, values=(timestamp, level, message)) + def clear_logs(self): + """清空日志记录""" + # 删除所有日志项 + for item in self.log_table.get_children(): + self.log_table.delete(item) + self.add_log("INFO", "日志已清空") + def system_settings(self): """系统设置""" settings_window = tk.Toplevel(self.root) @@ -355,7 +459,7 @@ class TradeTargetUI: # 设置窗口大小 window_width = 700 - window_height = 600 + window_height = 550 # 先设置为模态窗口 settings_window.transient(self.root) @@ -651,5 +755,4 @@ class TradeTargetUI: def run(self): """运行程序""" - self.root.mainloop() - + self.root.mainloop() \ No newline at end of file diff --git a/core/util.py b/core/util.py index 2e2d848..05052ef 100644 --- a/core/util.py +++ b/core/util.py @@ -1,6 +1,3 @@ -from typing import Any - - import sfgrid_constants import xtquant.xtconstant as xtconstant from xtquant import xtdata, xttrader @@ -43,6 +40,8 @@ def is_trading_time(): def getInstrumentName(stock_code): # print(f"getInstrumentName: 获取标的名称 {stock_code}") detail = xtdata.get_instrument_detail(stock_code, False) + if detail is None: + return "UnNamed" return detail['InstrumentName'] diff --git a/starter.py b/starter.py index 26b463f..b870d67 100644 --- a/starter.py +++ b/starter.py @@ -1,10 +1,13 @@ # coding:utf-8 -import sys -sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore from core import strategy_db from core.main_controller import SFGridController import sfgrid_constants as sdConstants -import ui + +def startTrade(index: int): + ctrl.start_stock_trade(index) + +def pauseTrade(index: int): + ctrl.pause_stock_trade(index) if __name__ == '__main__': sdConstants.initConfig() @@ -12,10 +15,7 @@ if __name__ == '__main__': strategy_db.db.create_tables([strategy_db.TradeTarget]) print('- [成功]数据库模块初始化') - targets = strategy_db.TradeTarget.select() - appUi = ui.TradeTargetUI(trade_targets=targets) - print(f'{sdConstants.account_no} : {sdConstants.miniQMTPath}') ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath) - appUi.run() + ctrl.hold() diff --git a/starter.spec b/starter.spec new file mode 100644 index 0000000..935fc84 --- /dev/null +++ b/starter.spec @@ -0,0 +1,37 @@ +# -*- mode: python ; coding: utf-8 -*- + +a = Analysis( + ['starter.py'], + pathex=[], + binaries=[], + datas=[('config.ini', '.')], # 明确包含配置文件 + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['PyQt5', 'PyQt6', 'PySide2', 'PySide6', 'matplotlib', 'numpy', 'pandas', 'jupyter', 'notebook', 'ipython'], # 排除不必要的包 + noarchive=False, + optimize=2, # 启用最高级别优化 +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='starter', + debug=False, + bootloader_ignore_signals=False, + strip=True, # 去除调试符号 + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) \ No newline at end of file