添加编译文件、打包瘦身、UI逻辑调整
This commit is contained in:
@@ -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()
|
||||
+82
-38
@@ -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)
|
||||
+94
-78
@@ -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}')
|
||||
|
||||
+3
-2
@@ -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) # 是否启动交易线程
|
||||
|
||||
+758
@@ -0,0 +1,758 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
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):
|
||||
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界面"""
|
||||
# 创建菜单栏
|
||||
self.create_menu_bar()
|
||||
|
||||
# 主框架
|
||||
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)
|
||||
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.root.destroy)
|
||||
|
||||
def create_tables_area(self, parent):
|
||||
"""创建表格区域"""
|
||||
# 上方交易标的区域
|
||||
trade_frame = ttk.LabelFrame(parent, text="交易标的详情", padding=10)
|
||||
trade_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
|
||||
|
||||
# 创建交易标的表格
|
||||
self.create_trade_target_table(trade_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):
|
||||
"""创建交易标的表格"""
|
||||
|
||||
columns = ("ID",
|
||||
"股票代码", "股票名称", "市场价", "持仓数量", "网格索引",
|
||||
"最新成交价", "计划买入价", "买入订单号", "计划卖出价", "卖出订单号",
|
||||
"启用状态", "交易状态"
|
||||
)
|
||||
|
||||
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
|
||||
|
||||
# 专业化的列配置
|
||||
column_configs = {
|
||||
"ID": (50, tk.CENTER),
|
||||
"股票代码": (90, tk.CENTER),
|
||||
"股票名称": (100, tk.CENTER),
|
||||
"市场价": (60, tk.CENTER),
|
||||
"持仓数量": (90, tk.CENTER),
|
||||
"网格索引": (50, tk.CENTER),
|
||||
"最新成交价": (60, tk.CENTER),
|
||||
"计划买入价": (60, tk.CENTER),
|
||||
"买入订单号": (100, tk.CENTER),
|
||||
"计划卖出价": (60, tk.CENTER),
|
||||
"卖出订单号": (100, tk.CENTER),
|
||||
"启用状态": (100, tk.CENTER),
|
||||
"交易状态": (100, tk.CENTER)
|
||||
}
|
||||
|
||||
for col in columns:
|
||||
width, anchor = column_configs[col]
|
||||
self.trade_table.heading(col, text=col)
|
||||
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore
|
||||
|
||||
# 填充数据
|
||||
self.populate_trade_table()
|
||||
|
||||
# 滚动条
|
||||
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
|
||||
self.trade_table.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# 绑定双击事件
|
||||
self.trade_table.bind("<Double-1>", self.on_table_double_click)
|
||||
|
||||
def get_status_indicator(self, target: TradeTarget) -> str:
|
||||
"""获取状态指示器(带颜色色块的文本)"""
|
||||
if target.status == 1:
|
||||
# 绿色圆点表示交易中
|
||||
return "🟢 已建仓"
|
||||
elif target.status == 0:
|
||||
# 黄色圆点表示暂停
|
||||
return "🟡 未建仓"
|
||||
else:
|
||||
return "🔴 错误状态"
|
||||
|
||||
def get_trade_enabled_indicator(self, enabled: bool) -> str:
|
||||
"""获取交易状态指示器"""
|
||||
if enabled:
|
||||
return "🟢 策略运行"
|
||||
else:
|
||||
return "🟡 策略暂停"
|
||||
|
||||
def populate_trade_table(self):
|
||||
"""填充交易标的表格数据"""
|
||||
for temp in self.data:
|
||||
target: TradeTarget = self.data[temp]
|
||||
values = [
|
||||
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.plan_buy_price:.2f}",
|
||||
target.current_buy_order_no,
|
||||
f"{target.plan_sell_price:.2f}",
|
||||
target.current_sell_order_no,
|
||||
self.get_status_indicator(target),
|
||||
self.get_trade_enabled_indicator(target.enabled) # type: ignore
|
||||
]
|
||||
|
||||
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):
|
||||
"""获取状态文本"""
|
||||
status_map = {
|
||||
0: "新标的",
|
||||
1: "交易中"
|
||||
}
|
||||
return status_map.get(status, "未知")
|
||||
|
||||
def on_table_double_click(self, event):
|
||||
"""表格双击事件"""
|
||||
selected = self.trade_table.selection()
|
||||
if selected:
|
||||
item = selected[0]
|
||||
values = self.trade_table.item(item)['values']
|
||||
self.add_log("DEBUG", f"双击查看详情: {values[0]} - {values[1]}")
|
||||
|
||||
def get_selected_target(self):
|
||||
"""获取选中的交易标的"""
|
||||
selected = self.trade_table.selection()
|
||||
if not selected:
|
||||
messagebox.showwarning("未选中", "请先选择一个交易标的")
|
||||
return None
|
||||
|
||||
# 获取选中行的ID
|
||||
item = selected[0]
|
||||
values = self.trade_table.item(item)['values']
|
||||
target_id = values[0]
|
||||
|
||||
# 从列表中找到对应的target对象
|
||||
for id in self.data:
|
||||
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()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认启动",
|
||||
f"确定要启动以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
target.enabled = True # type: ignore
|
||||
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):
|
||||
"""暂停选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
if not target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认暂停",
|
||||
f"确定要暂停以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
target.enabled = False # type: ignore
|
||||
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):
|
||||
"""删除选中的交易标的"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认删除",
|
||||
f"确定要删除以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}\n\n"
|
||||
f"⚠️ 此操作不可恢复!",
|
||||
icon='warning'
|
||||
)
|
||||
|
||||
if result:
|
||||
try:
|
||||
del self.data[target.get_id()]
|
||||
self.add_log("WARNING", f"已删除交易标的: {target.stock_code} - {target.stock_name}")
|
||||
self.refresh_table()
|
||||
messagebox.showinfo("删除成功", f"已删除 {target.stock_code} ({target.stock_name})")
|
||||
except Exception as e:
|
||||
self.add_log("ERROR", f"删除失败: {str(e)}")
|
||||
messagebox.showerror("删除失败", f"删除交易标的时出错:{str(e)}")
|
||||
|
||||
def add_trade_target(self):
|
||||
"""添加新的交易标的"""
|
||||
# 创建顶层窗口
|
||||
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('<Return>', lambda event: confirm_add())
|
||||
|
||||
self.add_log("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):
|
||||
"""刷新表格数据"""
|
||||
# 清空表格
|
||||
for item in self.trade_table.get_children():
|
||||
self.trade_table.delete(item)
|
||||
|
||||
# 重新填充
|
||||
self.populate_trade_table()
|
||||
|
||||
def add_log(self, level, message):
|
||||
"""添加日志记录"""
|
||||
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)
|
||||
settings_window.title("网格交易系统配置")
|
||||
|
||||
# 设置窗口大小
|
||||
window_width = 700
|
||||
window_height = 550
|
||||
|
||||
# 先设置为模态窗口
|
||||
settings_window.transient(self.root)
|
||||
|
||||
# 确保主窗口完全初始化
|
||||
self.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()
|
||||
|
||||
# 计算设置窗口相对于主窗口的居中位置
|
||||
x = main_x + (main_width - window_width) // 2
|
||||
y = main_y + (main_height - window_height) // 2
|
||||
|
||||
# 设置窗口大小和位置
|
||||
settings_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||
settings_window.resizable(False, False)
|
||||
|
||||
# 设置为模态窗口
|
||||
settings_window.grab_set()
|
||||
|
||||
# 添加底部按钮区域(先创建,确保固定在底部)
|
||||
button_frame = ttk.Frame(settings_window)
|
||||
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=10)
|
||||
|
||||
# 创建选项卡(在按钮之后创建,填充剩余空间)
|
||||
notebook = ttk.Notebook(settings_window)
|
||||
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 0))
|
||||
|
||||
# 基础设置
|
||||
basic_frame = ttk.Frame(notebook)
|
||||
notebook.add(basic_frame, text="基础设置")
|
||||
|
||||
# 高级设置
|
||||
advanced_frame = ttk.Frame(notebook)
|
||||
notebook.add(advanced_frame, text="高级设置")
|
||||
|
||||
# 读取当前配置
|
||||
config = configparser.ConfigParser()
|
||||
config.read('config.ini')
|
||||
|
||||
# 创建输入框字典用于保存引用
|
||||
entries = {}
|
||||
|
||||
# 网格价格计算参数
|
||||
grid_params = {}
|
||||
|
||||
# 添加网格价格设置(特殊处理)
|
||||
grid_price_frame = ttk.LabelFrame(basic_frame, text="网格价格设置", padding=15)
|
||||
grid_price_frame.pack(fill=tk.X, padx=20, pady=10)
|
||||
|
||||
# 基准价格
|
||||
base_price_row = ttk.Frame(grid_price_frame)
|
||||
base_price_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(base_price_row, text="基准价格:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
base_price_entry = ttk.Entry(base_price_row, width=15, font=('Arial', 10))
|
||||
base_price_entry.insert(0, "10.0")
|
||||
base_price_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(base_price_row, text="元", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
|
||||
grid_params['base_price'] = base_price_entry
|
||||
|
||||
# 网格类型
|
||||
grid_type_row = ttk.Frame(grid_price_frame)
|
||||
grid_type_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_type_row, text="网格类型:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
|
||||
grid_type_var = tk.StringVar(value="金额差")
|
||||
ttk.Radiobutton(grid_type_row, text="百分比", variable=grid_type_var,
|
||||
value="百分比", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(grid_type_row, text="金额差", variable=grid_type_var,
|
||||
value="金额差", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
grid_params['grid_type'] = grid_type_var
|
||||
|
||||
# 网格大小
|
||||
grid_size_row = ttk.Frame(grid_price_frame)
|
||||
grid_size_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(grid_size_row, text="网格大小:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
grid_size_entry = ttk.Entry(grid_size_row, width=15, font=('Arial', 10))
|
||||
grid_size_entry.insert(0, "1.0")
|
||||
grid_size_entry.pack(side=tk.LEFT, padx=5)
|
||||
grid_size_unit_label = ttk.Label(grid_size_row, text="元", foreground='gray', font=('Arial', 9))
|
||||
grid_size_unit_label.pack(side=tk.LEFT)
|
||||
grid_params['grid_size'] = grid_size_entry
|
||||
grid_params['grid_size_unit_label'] = grid_size_unit_label
|
||||
|
||||
# 网格类型改变时更新单位
|
||||
def on_grid_type_change():
|
||||
if grid_type_var.get() == "百分比":
|
||||
grid_size_unit_label.config(text="%")
|
||||
grid_size_entry.delete(0, tk.END)
|
||||
grid_size_entry.insert(0, "1.0")
|
||||
else:
|
||||
grid_size_unit_label.config(text="元")
|
||||
grid_size_entry.delete(0, tk.END)
|
||||
grid_size_entry.insert(0, "1.0")
|
||||
update_preview()
|
||||
|
||||
# 上方网格数量
|
||||
upper_grid_row = ttk.Frame(grid_price_frame)
|
||||
upper_grid_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(upper_grid_row, text="上方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
upper_grid_entry = ttk.Entry(upper_grid_row, width=15, font=('Arial', 10))
|
||||
upper_grid_entry.insert(0, "1")
|
||||
upper_grid_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(upper_grid_row, text="格", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
|
||||
grid_params['upper_count'] = upper_grid_entry
|
||||
|
||||
# 下方网格数量
|
||||
lower_grid_row = ttk.Frame(grid_price_frame)
|
||||
lower_grid_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(lower_grid_row, text="下方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
lower_grid_entry = ttk.Entry(lower_grid_row, width=15, font=('Arial', 10))
|
||||
lower_grid_entry.insert(0, "10")
|
||||
lower_grid_entry.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(lower_grid_row, text="格", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
|
||||
grid_params['lower_count'] = lower_grid_entry
|
||||
|
||||
# 预览按钮和结果显示
|
||||
preview_row = ttk.Frame(grid_price_frame)
|
||||
preview_row.pack(fill=tk.X, pady=10)
|
||||
|
||||
preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列")
|
||||
|
||||
def calculate_grid_prices():
|
||||
"""计算网格价格序列"""
|
||||
try:
|
||||
base_price = float(base_price_entry.get())
|
||||
grid_size = float(grid_size_entry.get())
|
||||
upper_count = int(upper_grid_entry.get())
|
||||
lower_count = int(lower_grid_entry.get())
|
||||
grid_type = grid_type_var.get()
|
||||
|
||||
prices = []
|
||||
|
||||
# 计算上方网格价格
|
||||
for i in range(upper_count, 0, -1):
|
||||
if grid_type == "百分比":
|
||||
price = base_price * (1 + grid_size / 100 * i)
|
||||
else: # 金额差
|
||||
price = base_price + grid_size * i
|
||||
prices.append(round(price, 3))
|
||||
|
||||
# 添加基准价格
|
||||
prices.append(base_price)
|
||||
|
||||
# 计算下方网格价格
|
||||
for i in range(1, lower_count + 1):
|
||||
if grid_type == "百分比":
|
||||
price = base_price * (1 - grid_size / 100 * i)
|
||||
else: # 金额差
|
||||
price = base_price - grid_size * i
|
||||
# 确保价格不为负
|
||||
if price >= 0:
|
||||
prices.append(round(price, 3))
|
||||
else:
|
||||
break
|
||||
|
||||
return prices
|
||||
except ValueError as e:
|
||||
return None
|
||||
|
||||
def update_preview():
|
||||
"""自动更新网格价格序列预览"""
|
||||
prices = calculate_grid_prices()
|
||||
if prices:
|
||||
price_str = ",".join([str(p) for p in prices])
|
||||
preview_result.set(f"网格价格序列: {price_str}")
|
||||
else:
|
||||
preview_result.set("参数错误,请检查!")
|
||||
|
||||
# 绑定输入变化自动预览
|
||||
for entry_widget in (base_price_entry, grid_size_entry, upper_grid_entry, lower_grid_entry):
|
||||
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
|
||||
entry_widget.bind("<FocusOut>", lambda e: update_preview())
|
||||
# 初始预览
|
||||
update_preview()
|
||||
ttk.Label(preview_row, textvariable=preview_result,
|
||||
font=('Arial', 10)).pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# 添加其他基础配置选项
|
||||
other_basic_frame = ttk.LabelFrame(basic_frame, text="交易设置", padding=15)
|
||||
other_basic_frame.pack(fill=tk.X, padx=20, pady=10)
|
||||
|
||||
other_basic_settings = [
|
||||
("网格交易手数", "grid_volume", config.get('config', 'grid_volume'), "每个网格的交易手数"),
|
||||
("最大启用目标数", "max_enabled_targets", config.get('config', 'max_enabled_targets'), "同时运行的最大标的数量")
|
||||
]
|
||||
|
||||
for i, (label, key, default, tooltip) in enumerate(other_basic_settings):
|
||||
frame = ttk.Frame(other_basic_frame)
|
||||
frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
label_widget = ttk.Label(frame, text=label + ":", width=15, font=('Arial', 10))
|
||||
label_widget.pack(side=tk.LEFT)
|
||||
|
||||
entry = ttk.Entry(frame, width=15, font=('Arial', 10))
|
||||
entry.insert(0, default)
|
||||
entry.pack(side=tk.LEFT, padx=5)
|
||||
entries[key] = entry
|
||||
|
||||
# 添加提示信息
|
||||
tip_label = ttk.Label(frame, text=tooltip, font=('Arial', 9), foreground='gray')
|
||||
tip_label.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 添加高级设置选项
|
||||
account_frame = ttk.LabelFrame(advanced_frame, text="账号设置", padding=15)
|
||||
account_frame.pack(fill=tk.X, padx=20, pady=10)
|
||||
|
||||
# 交易账号
|
||||
account_row = ttk.Frame(account_frame)
|
||||
account_row.pack(fill=tk.X, pady=5)
|
||||
ttk.Label(account_row, text="交易账号:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
account_entry = ttk.Entry(account_row, width=15, font=('Arial', 10))
|
||||
account_entry.insert(0, config.get('config', 'account_no'))
|
||||
account_entry.pack(side=tk.LEFT, padx=5)
|
||||
entries['account_no'] = account_entry
|
||||
ttk.Label(account_row, text="QMT交易账号", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# QMT路径特殊处理 - 使用文件浏览器
|
||||
qmt_path_frame = ttk.LabelFrame(advanced_frame, text="软件路径", padding=15)
|
||||
qmt_path_frame.pack(fill=tk.X, padx=20, pady=10)
|
||||
|
||||
qmt_row = ttk.Frame(qmt_path_frame)
|
||||
qmt_row.pack(fill=tk.X, pady=5)
|
||||
|
||||
ttk.Label(qmt_row, text="QMT路径:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
|
||||
|
||||
qmt_entry = ttk.Entry(qmt_row, width=30, font=('Arial', 10))
|
||||
qmt_entry.insert(0, config.get('config', 'miniQMTPath'))
|
||||
qmt_entry.pack(side=tk.LEFT, padx=5)
|
||||
entries['miniQMTPath'] = qmt_entry
|
||||
|
||||
def browse_qmt_path():
|
||||
"""打开文件夹浏览器选择QMT路径"""
|
||||
initial_dir = qmt_entry.get() if qmt_entry.get() else "/"
|
||||
folder_path = filedialog.askdirectory(
|
||||
title="选择miniQMT安装路径",
|
||||
initialdir=initial_dir
|
||||
)
|
||||
if folder_path:
|
||||
qmt_entry.delete(0, tk.END)
|
||||
qmt_entry.insert(0, folder_path)
|
||||
|
||||
ttk.Button(qmt_row, text="📁 浏览...", command=browse_qmt_path, width=10).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Label(qmt_row, text="miniQMT软件安装路径", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 定义保存和取消按钮的功能(button_frame已在上方创建)
|
||||
def save_settings():
|
||||
"""保存配置"""
|
||||
try:
|
||||
# 计算网格价格序列
|
||||
grid_prices = calculate_grid_prices()
|
||||
if not grid_prices:
|
||||
messagebox.showerror("错误", "网格价格参数有误,请检查输入!")
|
||||
return
|
||||
|
||||
grid_price_str = ",".join([str(p) for p in grid_prices])
|
||||
|
||||
# 更新配置对象
|
||||
config.set('config', 'miniQMTPath', entries['miniQMTPath'].get())
|
||||
config.set('config', 'grid_price', grid_price_str)
|
||||
config.set('config', 'grid_volume', entries['grid_volume'].get())
|
||||
config.set('config', 'account_no', entries['account_no'].get())
|
||||
config.set('config', 'max_enabled_targets', entries['max_enabled_targets'].get())
|
||||
|
||||
# 写入配置文件
|
||||
with open('config.ini', 'w') as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
# 重新加载配置到内存中
|
||||
sfgrid_constants.initConfig()
|
||||
|
||||
messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。")
|
||||
self.add_log("INFO", f"系统配置已更新 - 网格数量: {len(grid_prices)}")
|
||||
settings_window.destroy()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
|
||||
self.add_log("ERROR", f"保存配置失败: {str(e)}")
|
||||
|
||||
def cancel_settings():
|
||||
"""取消设置"""
|
||||
settings_window.destroy()
|
||||
|
||||
# 在button_frame中添加按钮
|
||||
ttk.Button(button_frame, text="💾 保存配置", command=save_settings, width=15).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="❌ 取消", command=cancel_settings, width=15).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def run(self):
|
||||
"""运行程序"""
|
||||
self.root.mainloop()
|
||||
+2
-3
@@ -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']
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user