This commit is contained in:
2026-06-04 18:17:48 +08:00
parent db910e03d6
commit 1816d585bf
7 changed files with 894 additions and 340 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
[config] [config]
miniqmtpath = D:/Programs/DTQMT/userdata_mini miniqmtpath = C:/Programs/GJQMT/userdata_mini
account_no = 99082560 account_no = 8882874667
log_level = INFO log_level = INFO
+17 -148
View File
@@ -27,14 +27,12 @@ class MainWindow:
self.logLevel = LogLevel[configLogLevel] self.logLevel = LogLevel[configLogLevel]
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}") PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
# 当前选中的策略Tab索引
self.current_strategy_index = 0
# 存储各个Frame的引用 # 存储各个Frame的引用
self.strategy_frames = {} self.strategy_frames = {}
# 日志面板可见性标志 # 日志面板可见性标志
self.log_visible = False self.log_visible = False
self.create_ui() self.create_ui()
eBus.subscribe(EventPrintLog, self.on_log_event) eBus.subscribe(EventPrintLog, self.on_log_event)
@@ -43,113 +41,24 @@ class MainWindow:
# 主容器 # 主容器
main_container = ttk.Frame(self.root) main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 中间主体区域(左右布局) # 中间主体区域
content_area = ttk.Frame(main_container) content_area = ttk.Frame(main_container)
content_area.pack(fill=tk.BOTH, expand=True) content_area.pack(fill=tk.BOTH, expand=True)
# 左侧Tab按钮栏(垂直排列) # 右侧内容区域容器
tab_bar_frame = ttk.Frame(content_area)
tab_bar_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
# 创建自定义样式
self.create_custom_styles()
# 创建Tab按钮(垂直排列,文字垂直显示)
self.tab_buttons = []
strategy_names = ["网格", "复盘"]
for idx, name in enumerate(strategy_names):
btn = ttk.Button(
tab_bar_frame,
text=name,
command=lambda i=idx: self.switch_strategy_tab(i),
width=4,
style='Bookmark.TButton' # 使用自定义书签样式
)
btn.pack(side=tk.TOP, pady=2, fill=tk.X)
self.tab_buttons.append(btn)
# 在Tab按钮下方添加退出按钮和日志按钮(底部对齐)
# 使用一个填充Frame将按钮推到底部
spacer = ttk.Frame(tab_bar_frame)
spacer.pack(side=tk.TOP, fill=tk.X, ipady=10)
# 清空日志按钮(底部第三个)
clear_log_btn = ttk.Button(
tab_bar_frame,
text="🗑", # 垃圾桶图标
command=self.clear_logs,
width=3
)
clear_log_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 日志显示按钮(退出按钮上方)
self.log_toggle_btn = ttk.Button(
tab_bar_frame,
text="📋", # 日志图标
command=self.toggle_log_panel,
width=3
)
self.log_toggle_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 退出按钮(最底部)
exit_btn = ttk.Button(
tab_bar_frame,
text="", # 电源图标
command=self.on_exit,
width=3
)
exit_btn.pack(side=tk.TOP, pady=2, fill=tk.X)
# 添加垂直分隔线
separator = ttk.Separator(content_area, orient='vertical')
separator.pack(side=tk.LEFT, fill=tk.Y, padx=1)
# 右侧内容区域容器(用于放置不同策略的Frame)
self.content_container = ttk.Frame(content_area) self.content_container = ttk.Frame(content_area)
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建各个策略Frame # 创建策略Frame
strategy_names = ["网格"]
self.create_strategy_frames(strategy_names) self.create_strategy_frames(strategy_names)
# 创建全局日志面板(默认隐藏) # 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container) self.create_global_log_panel(main_container)
# 默认显示第一个策略 # 默认显示第一个策略
self.switch_strategy_tab(0) self.show_strategy_frame(0)
def create_custom_styles(self):
"""创建自定义样式"""
style = ttk.Style()
# 创建书签样式
style.configure(
'Bookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
foreground='black',
background='#FFE599', # 浅黄色背景,类似便签纸
font=('Arial', 10, 'bold')
)
# 设置焦点样式(选中状态)
style.map(
'Bookmark.TButton',
background=[('active', '#F1C232'), ('pressed', '#F1C232')],
relief=[('pressed', 'sunken')]
)
# 创建选中状态的书签样式
style.configure(
'SelectedBookmark.TButton',
relief='flat',
borderwidth=1,
padding=(5, 10),
background='#3D85C6', # 蓝色背景表示选中状态
font=('Arial', 10, 'bold')
)
def create_global_log_panel(self, parent): def create_global_log_panel(self, parent):
"""创建全局日志面板""" """创建全局日志面板"""
@@ -204,62 +113,22 @@ class MainWindow:
def create_strategy_frames(self, strategy_names): def create_strategy_frames(self, strategy_names):
"""创建各个策略的Frame""" """创建各个策略的Frame"""
for idx, name in enumerate(strategy_names): frame = TradeTargetUI(self.content_container)
if idx == 0: self.strategy_frames[0] = frame
# 第一个Tab使用TradeTargetUI,传入main_window引用
frame = TradeTargetUI(self.content_container) def show_strategy_frame(self, index):
self.strategy_frames[idx] = frame """显示策略Frame"""
else:
# 其他策略使用占位Frame
frame = ttk.Frame(self.content_container)
self.strategy_frames[idx] = frame
# 添加占位内容
placeholder = ttk.Label(
frame,
text=f"{name} - 策略界面将在此实现",
font=('Arial', 14),
foreground='gray'
)
placeholder.pack(expand=True)
def switch_strategy_tab(self, index):
"""切换策略Tab"""
# 隐藏当前Frame
if self.current_strategy_index in self.strategy_frames:
self.strategy_frames[self.current_strategy_index].pack_forget()
# 更新当前索引
self.current_strategy_index = index
# 显示选中的Frame
if index in self.strategy_frames: if index in self.strategy_frames:
self.strategy_frames[index].pack(fill=tk.BOTH, expand=True) self.strategy_frames[index].pack(fill=tk.BOTH, expand=True)
# 更新Tab按钮样式(可选,用于视觉反馈)
self.update_tab_button_styles()
def update_tab_button_styles(self):
"""更新Tab按钮的样式以显示选中状态"""
# 重置所有按钮为普通书签样式
for i, btn in enumerate(self.tab_buttons):
if i == self.current_strategy_index:
btn.configure(style='SelectedBookmark.TButton') # 选中状态
else:
btn.configure(style='Bookmark.TButton') # 普通状态
def toggle_log_panel(self): def toggle_log_panel(self):
"""切换日志面板的显示/隐藏""" """切换日志面板的显示/隐藏"""
if self.log_visible: if self.log_visible:
# 隐藏日志面板
self.log_frame.pack_forget() self.log_frame.pack_forget()
self.log_visible = False self.log_visible = False
self.log_toggle_btn.config(text="📋") # 日志图标
else: else:
# 显示日志面板
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
self.log_visible = True self.log_visible = True
self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏
def on_exit(self): def on_exit(self):
"""退出程序""" """退出程序"""
+14
View File
@@ -107,6 +107,13 @@ class DummyQmtV:
} }
PrintLog(LogLevel.INFO, f'- [模拟] 已加载 {len(self._positions)} 个持仓') PrintLog(LogLevel.INFO, f'- [模拟] 已加载 {len(self._positions)} 个持仓')
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {stock_code: position_object}"""
result = {}
for code, pos_data in self._positions.items():
result[code] = type('DummyPos', (), pos_data)()
return result
def getStockPosition(self, stock_code: str): def getStockPosition(self, stock_code: str):
"""获取持仓 (模拟)""" """获取持仓 (模拟)"""
if stock_code in self._positions: if stock_code in self._positions:
@@ -207,6 +214,13 @@ class DummyQmtV:
"""获取跌停价 (模拟)""" """获取跌停价 (模拟)"""
return 0.0 return 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(模拟)"""
if stock_code in self._positions:
return float(self._positions[stock_code].get('open_cost', 10.0))
# 给一个合理模拟价
return 10.0 + hash(stock_code) % 100
def startMarketDataSubscription(self): def startMarketDataSubscription(self):
"""启动市场数据订阅 (模拟)""" """启动市场数据订阅 (模拟)"""
try: try:
+362 -19
View File
@@ -1,25 +1,368 @@
""" """
QMT 模块统一入口 QMT 真实交易实现 - 封装 xtquant SDK
根据环境自动选择真实 QMT 或模拟器
""" """
import sys import datetime
import threading
import time
import config
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
def _get_qmt():
"""获取 QMT 模块""" class RealQmtV:
if sys.platform == 'win32': """
真实 QMT 交易器
封装 xtquant 的 XtQuantTrader,提供与模拟器一致的接口
"""
@staticmethod
def _to_plain_code(stock_code: str) -> str:
"""将 xtquant 格式 '600519.SH' 转换为数据库格式 '600519'"""
return stock_code.split('.')[0] if '.' in stock_code else stock_code
@staticmethod
def _to_full_code(stock_code: str) -> str:
"""将数据库格式 '600519' 转换为 xtquant 格式 '600519.SH'"""
if '.' in stock_code:
return stock_code # already has suffix
code = stock_code
if code.startswith(('6', '5', '9')):
return f'{code}.SH'
elif code.startswith(('0', '3', '2')):
return f'{code}.SZ'
# fallback: try both, prefer SH
return f'{code}.SH'
@staticmethod
def _strip_code_suffixes(datas: dict) -> dict:
"""批量去除 xtquant 数据中的代码后缀"""
result = {}
for code, tick in datas.items():
result[code] = tick
if '.' in code:
result[code.split('.')[0]] = tick
return result
def __init__(self) -> None:
self.inited = False
self.connected = False
self.account = None
self.xt_trader = None
self.mini_qmt_path = ""
self._positions = {}
self._pending_orders = []
self._market_data_thread = None
self.isMarketActive = False
self.lastMarketDataUpdateTimestamp = time.time()
self.details = {}
def getTrader(self):
return self
def init_qmtv(self):
"""初始化 QMT 交易器"""
try: try:
# Windows 环境尝试导入真实 QMT from xtquant.xttrader import XtQuantTrader
import core.qmt_real as qmt_module from xtquant.xttype import StockAccount
return qmt_module.qmtv
except ImportError:
pass
# 非 Windows 或导入失败,使用模拟器 self.mini_qmt_path = config.miniQMTPath
try: self.account = StockAccount(config.account_no, 'STOCK')
import core.qmt_dummy as qmt_module
return qmt_module.qmtv
except ImportError:
raise ImportError("无法加载 QMT 模块")
# 导出单 # 创建 XtQuantTrader 实
qmtv = _get_qmt() session_id = int(time.time()) % 10000
self.xt_trader = XtQuantTrader(self.mini_qmt_path, session_id)
# 注册回调 — xtquant 只接受一个回调对象,会在上面调用 on_xxx 方法
self.xt_trader.register_callback(self)
self.inited = True
PrintLog(LogLevel.INFO, f'- [真实] QMT 交易器初始化成功')
except Exception as e:
self.inited = False
PrintLog(LogLevel.ERROR, f'- [失败] QMT 初始化: {e}')
def connect(self) -> bool:
"""连接 MiniQMT"""
if not self.inited:
PrintLog(LogLevel.ERROR, '- [失败] QMT 未初始化')
return False
try:
# 启动 trader 线程
self.xt_trader.start()
# 建立连接
connect_result = self.xt_trader.connect()
if connect_result == 0:
# 订阅账户 (传入 StockAccount 对象而不是 account_id 字符串)
self.xt_trader.subscribe(self.account)
# 等待回调
time.sleep(1)
self.connected = True
self.startMarketDataSubscription()
PrintLog(LogLevel.INFO, f'- [成功] 真实交易连接成功 (账号: {config.account_no})')
return True
else:
PrintLog(LogLevel.ERROR, f'- [失败] 连接失败, 返回码: {connect_result}')
return False
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [失败] 连接异常: {e}')
return False
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {plain_code: position_object}"""
if not self.connected:
return {}
try:
positions = self.xt_trader.query_stock_positions(self.account)
result = {}
for pos in positions:
code = self._to_plain_code(getattr(pos, 'stock_code', ''))
result[code] = pos
# 缓存以供 getStockPosition 使用
self._position_cache = result
return result
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [获取全部持仓失败]: {e}')
return {}
def getStockPosition(self, stock_code: str):
"""获取单只股票持仓(优先使用缓存)"""
if not self.connected:
return None
try:
# 优先查缓存
if hasattr(self, '_position_cache') and stock_code in self._position_cache:
return self._position_cache[stock_code]
# 回退查询
positions = self.xt_trader.query_stock_positions(self.account)
for pos in positions:
pos_code = self._to_plain_code(getattr(pos, 'stock_code', ''))
if pos_code == stock_code:
return pos
return None
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [持仓查询失败] {stock_code}: {e}')
return None
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
"""查询挂单"""
if not self.connected:
return []
try:
orders = self.xt_trader.query_stock_orders(self.account)
return [o for o in orders
if self._to_plain_code(getattr(o, 'stock_code', '')) == stock_code and
(tag is None or getattr(o, 'strategy_name', None) == tag)]
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [查询挂单失败] {e}')
return []
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
"""异步下单"""
if not self.connected:
PrintLog(LogLevel.ERROR, '- [下单失败] 未连接')
return -1
try:
seq = self.xt_trader.order_stock_async(
account=self.account,
stock_code=stock_code,
order_volume=orderVolume,
order_type=orderType,
price=orderPrice,
price_type=priceType,
order_remark=orderRemark,
strategy_name=strategy_name
)
PrintLog(LogLevel.INFO,
f'- [下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 类型:{orderType} seq:{seq}')
return 0
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [下单失败] {stock_code}: {e}')
return -1
def cacheStockDetail(self, stock_code: str):
"""获取股票详情"""
if stock_code not in self.details:
try:
from xtquant import xtdata
# xtquant 需要带后缀的完整代码
full_code = self._to_full_code(stock_code)
detail = xtdata.get_instrument_detail(full_code)
if detail:
# xtquant 返回 dict,使用 .get() 读取
self.details[stock_code] = {
'InstrumentName': detail.get('InstrumentName', stock_code) if isinstance(detail, dict) else getattr(detail, 'InstrumentName', stock_code),
'UpStopPrice': detail.get('UpStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'UpStopPrice', 0),
'DownStopPrice': detail.get('DownStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'DownStopPrice', 0)
}
else:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
except Exception:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
return self.details[stock_code]
def getInstrumentName(self, stock_code: str) -> str:
"""获取股票名称"""
return self.cacheStockDetail(stock_code)['InstrumentName']
def dailyUpStop(self, stock_code: str):
"""获取涨停价"""
detail = self.cacheStockDetail(stock_code)
up_stop = detail.get('UpStopPrice', 0)
PrintLog(LogLevel.DEBUG, f'- [详情] {stock_code} {detail["InstrumentName"]} 涨停价: {up_stop}')
return up_stop or 0.0
def dailyDownStop(self, stock_code: str):
"""获取跌停价"""
detail = self.cacheStockDetail(stock_code)
down_stop = detail.get('DownStopPrice', 0)
return down_stop or 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(拉取模式,作为推送的兜底)"""
try:
from xtquant import xtdata
import json
full_code = self._to_full_code(stock_code)
# 方式1: 尝试 get_full_tick(参数是 list[str],返回 dict {code: {...}}
raw = xtdata.get_full_tick([full_code])
if raw:
tick = json.loads(raw) if isinstance(raw, str) else raw
if isinstance(tick, dict):
# 格式: {'600519.SH': {'lastPrice': 8.97, ...}}
for code, info in tick.items():
if isinstance(info, dict) and info.get('lastPrice', 0) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → tick: {info["lastPrice"]:.3f}')
return float(info['lastPrice'])
# 方式2: get_market_data 取最新1分钟K线收盘价
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → kline: {float(vals[0]):.3f}')
return float(vals[0])
# 方式3: 下载历史数据后再试
xtdata.download_history_data(full_code, '1m', '')
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → download+kline: {float(vals[0]):.3f}')
return float(vals[0])
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 失败: 所有方式均无数据, raw={raw}')
except Exception as e:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 异常: {e}')
return 0.0
def startMarketDataSubscription(self):
"""启动市场数据订阅"""
try:
from xtquant import xtdata
# 订阅沪深全市场实时行情
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
# 启动行情活跃监控线程
self._market_data_thread = threading.Thread(
target=self._market_data_watchdog, daemon=True
)
self._market_data_thread.start()
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def _on_market_data(self, datas: dict):
"""xtquant 行情回调 — 将数据转换为事件总线格式"""
self.lastMarketDataUpdateTimestamp = time.time()
if not self.isMarketActive:
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
# xtquant 返回 "600519.SH" 格式 keyUI 使用纯代码 "600519"
# 构建同时包含两种 key 的数据确保匹配
eBus.event_bus.publish(eBus.MarketDataUpdate, self._strip_code_suffixes(datas))
def _market_data_watchdog(self):
"""行情活跃监控 — 超过 30 秒无数据则标记市场不活跃"""
while True:
time.sleep(10)
if self.isMarketActive:
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
if elapsed > 30:
self.isMarketActive = False
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
PrintLog(LogLevel.WARNING, f'- [行情] 超过 {elapsed:.0f} 秒无更新,市场标记为不活跃')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
self.isMarketActive = False
PrintLog(LogLevel.INFO, '- [市场数据订阅已停止]')
# ---- xtquant 回调处理 (xtquant 通过回调对象调用 on_xxx 方法) ----
def on_connected(self):
print(datetime.datetime.now(), '真实 QMT 连接成功')
def on_disconnected(self):
print(datetime.datetime.now(), '真实 QMT 连接断开')
def on_stock_order(self, order):
self._pending_orders.append(order)
def on_stock_trade(self, trade):
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
def on_order_stock_async_response(self, response):
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
def on_order_error(self, order_error):
print(f"\n真实委托报错回调 {order_error}")
def on_account_status(self, status):
print(datetime.datetime.now(), status)
qmtv = RealQmtV()
+19 -3
View File
@@ -2,6 +2,10 @@ from peewee import CharField, IntegerField, FloatField, BooleanField
from core.database import BaseModel, db from core.database import BaseModel, db
# 策略类型常量
STRATEGY_TYPE_UNCLASSIFIED = 0 # 未分类持仓
STRATEGY_TYPE_GRID = 1 # 网格策略
# 定义Target类,对应targets表 # 定义Target类,对应targets表
class SFGridTradeTarget(BaseModel): class SFGridTradeTarget(BaseModel):
@@ -14,10 +18,11 @@ class SFGridTradeTarget(BaseModel):
grid_total_profit = FloatField(default=0.0) grid_total_profit = FloatField(default=0.0)
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中 status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
enabled = BooleanField(default=False) # 是否启动交易线程 enabled = BooleanField(default=False) # 是否启动交易线程
strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略
grid_start_price = FloatField(default=10.0) # 基线价格 grid_start_price = FloatField(default=10.0) # 基线价格
grid_size = FloatField(default=0.1) # 网格价位差 grid_size = FloatField(default=1.0) # 网格价位差
grid_volume = IntegerField(default=100) # 网格交易量 grid_volume = IntegerField(default=200) # 网格交易量
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数 grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数 grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
@@ -41,4 +46,15 @@ class SFGridTradeTarget(BaseModel):
return self.priceGrid return self.priceGrid
db.create_tables([SFGridTradeTarget]) db.create_tables([SFGridTradeTarget])
# 数据库迁移: 为已有表添加 strategy_type 字段(如果不存在)
try:
from playhouse.migrate import migrate, SqliteMigrator
migrator = SqliteMigrator(db)
migrate(
migrator.add_column('sfgridtradetarget', 'strategy_type', SFGridTradeTarget.strategy_type),
)
except Exception:
# 字段已存在或迁移失败 — 静默跳过
pass
+10 -3
View File
@@ -20,11 +20,12 @@ class SFGridStrategy:
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade) event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
self.todayUpStopPrice=qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore self.todayUpStopPrice=qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
self.todayDownStopPrice=qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore self.todayDownStopPrice=qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}初始化: 停涨价 {self.todayUpStopPrice:.3f}, 停跌价 {self.todayDownStopPrice:.3f}') PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
self.orderGrid = {} # grid index, order_seq | order_id self.orderGrid = {} # grid index, order_seq | order_id
self.loadExistOrders() self.loadExistOrders()
self.enabledTrading(tradeTarget.enabled) # type: ignore self.enabledTrading(tradeTarget.enabled) # type: ignore
self.dataUpdateLock = threading.Lock() self.dataUpdateLock = threading.Lock()
PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: grid_index={self.tradeTarget.grid_index}')
def loadExistOrders(self): def loadExistOrders(self):
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
@@ -123,13 +124,17 @@ class SFGridStrategy:
self.dataUpdateLock.release() self.dataUpdateLock.release()
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget: def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
PrintLog(LogLevel.INFO, f" |- [DEBUG] enabledTrading({enabled}) 调用前: grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}")
self.tradeTarget.enabled = enabled # type: ignore self.tradeTarget.enabled = enabled # type: ignore
if enabled: if enabled:
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}") PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}")
if self.tradeTarget.status == 0: # 未建仓 if self.tradeTarget.status == 0: # 未建仓
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,") if self.tradeTarget.grid_index == 0:
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue] self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
else:
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 保留网格序号 {self.tradeTarget.grid_index},")
else: # 已建仓 else: # 已建仓
# 交易阶段,检查仓位,检查现有订单 # 交易阶段,检查仓位,检查现有订单
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}") PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
@@ -186,6 +191,7 @@ class SFGridStrategy:
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交') PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交')
self.tradeTarget.status = 1 # type: ignore self.tradeTarget.status = 1 # type: ignore
self.tradeTarget.init_price = trade.traded_price # type: ignore self.tradeTarget.init_price = trade.traded_price # type: ignore
PrintLog(LogLevel.INFO, f'|- [DEBUG] 建仓单成交: grid_index {self.tradeTarget.grid_index} → 1')
self.tradeTarget.grid_index = 1 # type: ignore self.tradeTarget.grid_index = 1 # type: ignore
type = "建仓单" type = "建仓单"
else: else:
@@ -218,6 +224,7 @@ class SFGridStrategy:
return "SFGRID" return "SFGRID"
def saveProxy(self): def saveProxy(self):
PrintLog(LogLevel.DEBUG, f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}')
rc = self.tradeTarget.save() rc = self.tradeTarget.save()
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget) event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
return rc return rc
+470 -165
View File
@@ -24,6 +24,8 @@ class TradeTargetUI(ttk.Frame):
self.listening_stock = [] self.listening_stock = []
# 监控价格,默认值为10 # 监控价格,默认值为10
self.monitor_price = 10.0 self.monitor_price = 10.0
# 追踪最后点击的表格 (0=网格, 1=未分类)
self._active_table = 0
self.init_trade_target_pool() self.init_trade_target_pool()
@@ -32,20 +34,66 @@ class TradeTargetUI(ttk.Frame):
# 市场监控窗口显示状态 # 市场监控窗口显示状态
self.market_monitor_visible = True self.market_monitor_visible = True
# 市场活跃状态 + 刷新控制
self._market_active = qmtv.isMarketActive # type: ignore
self._refresh_event = threading.Event()
self._prices_pulled_after_close = False # 收盘后是否已拉取过
# 创建界面 # 创建界面
self.create_ui() self.create_ui()
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated) eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
eBus.event_bus.subscribe(eBus.EventMarketActiveSwitch, self._on_market_active_switch)
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate) eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted) eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
def init_trade_target_pool(self): def init_trade_target_pool(self):
# 一次性迁移: 已配置过的标的 (status >= 0) → 网格策略
from core.sfgrid.model import STRATEGY_TYPE_GRID
migrated = SFGridTradeTarget.update(strategy_type=STRATEGY_TYPE_GRID).where(
SFGridTradeTarget.status >= 0
).execute()
if migrated:
PrintLog(LogLevel.INFO, f'- [迁移] {migrated} 个已配置标的标记为网格策略')
# 一次性从 QMT 获取全部持仓
all_positions = qmtv.getAllPositions()
PrintLog(LogLevel.INFO, f'- [持仓] 从 QMT 获取到 {len(all_positions)} 个持仓')
# 自动将 QMT 持仓导入到数据库(持仓但未在交易池中的标的)
imported_count = 0
for code, pos in all_positions.items():
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
if existing is None:
name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code)
volume = int(pos.volume)
avg_price = float(pos.avg_price) if pos.avg_price else 0.0
SFGridTradeTarget.create(
stock_code=code,
stock_name=name,
current_position=volume,
grid_index=0,
init_price=avg_price,
grid_match_count=0,
grid_total_profit=0.0,
status=-1,
enabled=False,
grid_start_price=avg_price if avg_price > 0 else 10.0,
grid_size=1.0,
grid_volume=200,
grid_upper_count=1,
grid_lower_count=10
)
imported_count += 1
PrintLog(LogLevel.INFO, f'- [导入] QMT持仓 → 交易池: {code} {name} 持仓:{volume} 成本:{avg_price:.4f}')
if imported_count:
PrintLog(LogLevel.INFO, f'- [导入] 共新增 {imported_count} 个标的到交易池')
results = SFGridTradeTarget.select() results = SFGridTradeTarget.select()
for temp in results: for temp in results:
tradeTarget:SFGridTradeTarget = temp tradeTarget:SFGridTradeTarget = temp
pos = qmtv.getStockPosition(tradeTarget.stock_code) pos = all_positions.get(tradeTarget.stock_code)
tradeTarget.current_position = 0 if pos is None else pos.volume # type: ignore tradeTarget.current_position = 0 if pos is None else pos.volume # type: ignore
if pos is None: if pos is None:
self.targetAvgPrice[tradeTarget.get_id()] = 0.0 self.targetAvgPrice[tradeTarget.get_id()] = 0.0
@@ -53,7 +101,9 @@ class TradeTargetUI(ttk.Frame):
self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price self.targetAvgPrice[tradeTarget.get_id()] = pos.avg_price
PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}') PrintLog(LogLevel.INFO, f'- [成功]获取持仓信息: {tradeTarget.stock_code} {tradeTarget.targetName()} {tradeTarget.current_position} {pos.avg_price}')
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 前: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
self.updateTradeTarget(tradeTarget, True) # 初始化的时候 self.updateTradeTarget(tradeTarget, True) # 初始化的时候
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 后: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}')
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的') PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
@@ -99,13 +149,19 @@ class TradeTargetUI(ttk.Frame):
target.save() target.save()
id = target.get_id() id = target.get_id()
# PrintLog(LogLevel.INFO, f' [序号-{id}] 股票代码: {target.stock_code}-{target.stock_name}: {target.plan_buy_price} {target.plan_sell_price}') # type: ignore
# 更新或添加数据到本地缓存 # 更新或添加数据到本地缓存
self.tradeTargetData[id] = target self.tradeTargetData[id] = target
if id not in self.strategy_ctrl: # 注册到 stockCodeIdMap(所有标的都需要行情数据)
if id not in self.stockCodeIdMap.values() and target.stock_code not in self.stockCodeIdMap:
self.stockCodeIdMap[target.stock_code] = id # type: ignore self.stockCodeIdMap[target.stock_code] = id # type: ignore
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
# 只有网格策略标的才创建策略控制器
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type == STRATEGY_TYPE_GRID: # type: ignore
if id not in self.strategy_ctrl:
self.strategy_ctrl[id] = SFGridStrategy(target) # pyright: ignore[reportArgumentType]
if id in self.targetAvgPrice: if id in self.targetAvgPrice:
pos = qmtv.getStockPosition(target.stock_code) pos = qmtv.getStockPosition(target.stock_code)
if pos is not None: if pos is not None:
@@ -118,35 +174,7 @@ class TradeTargetUI(ttk.Frame):
main_frame = ttk.Frame(self) main_frame = ttk.Frame(self)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建工具栏 # 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页)
toolbar_frame = ttk.Frame(main_frame)
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
# 工具栏按钮
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🛠 交易设置",
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▣ 边栏",
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
# 添加价格监控输入字段和确认按钮
ttk.Button(toolbar_frame, text="确认",
command=self.btnHandlerSetMonitorPrice, width=8).pack(side=tk.RIGHT, padx=2)
self.monitor_price_entry = ttk.Entry(toolbar_frame, width=8)
self.monitor_price_entry.insert(0, str(self.monitor_price))
self.monitor_price_entry.pack(side=tk.RIGHT, padx=2)
ttk.Label(toolbar_frame, text="价格").pack(side=tk.RIGHT, padx=(20, 2))
ttk.Label(toolbar_frame, text="监控配置").pack(side=tk.RIGHT, padx=(20, 2))
# 表格区域
self.create_tables_area(main_frame) self.create_tables_area(main_frame)
# 启动刷新线程 # 启动刷新线程
@@ -154,76 +182,203 @@ class TradeTargetUI(ttk.Frame):
self.refresh_thread.start() self.refresh_thread.start()
def _on_market_active_switch(self, is_active: bool):
"""市场活跃状态变更回调"""
self._market_active = is_active
if is_active:
self._prices_pulled_after_close = False
self._refresh_event.set() # 唤醒 refresh_loop
def refresh_loop(self): def refresh_loop(self):
"""刷新循环""" """刷新循环(后台线程:拉取市价 + 调度UI刷新)"""
while True: while True:
self.after(0, self.refresh_table) if self._market_active:
self.after(0, self.populate_market_table) # 盘中:每5s拉取缺失的市价
time.sleep(0.5) # 每0.5秒刷新一次 prices = {} # id -> price
for id, target in list(self.tradeTargetData.items()):
if id not in self.targetMarketPrice or self.targetMarketPrice[id] == 0:
price = qmtv.getLastPrice(target.stock_code) # type: ignore
if price > 0:
prices[id] = price
if prices:
self.after(0, lambda p=prices: self._apply_prices(p))
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
self._refresh_event.wait(timeout=5)
self._refresh_event.clear()
else:
# 收盘后:只拉取一次收盘价,之后等待市场重新活跃
if not self._prices_pulled_after_close:
prices = {}
for id, target in list(self.tradeTargetData.items()):
if id not in self.targetMarketPrice or self.targetMarketPrice[id] == 0:
price = qmtv.getLastPrice(target.stock_code) # type: ignore
if price > 0:
prices[id] = price
if prices:
self.after(0, lambda p=prices: self._apply_prices(p))
self.after(0, self.refresh_table)
self.after(0, self.populate_market_table)
self._prices_pulled_after_close = True
PrintLog(LogLevel.INFO, '[刷新] 收盘后已拉取收盘价,进入休眠等待开盘')
# 休眠直到市场重新活跃(或每60s检查一次以防漏信号)
self._refresh_event.wait(timeout=60)
self._refresh_event.clear()
def _apply_prices(self, prices: dict):
"""主线程回调:将后台拉取的市价写入缓存"""
for id, price in prices.items():
self.targetMarketPrice[id] = price
if id in self.tradeTargetData:
self.tradeTargetData[id].market_price = price # type: ignore
def create_tables_area(self, parent): def create_tables_area(self, parent):
"""创建表格区域""" """创建表格区域 — 左侧工具栏+持仓表格,右侧Notebook"""
# 创建主表格框架(水平排列) # 创建主表格框架(水平排列)
tables_frame = ttk.Frame(parent) tables_frame = ttk.Frame(parent)
tables_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5)) tables_frame.pack(fill=tk.BOTH, expand=True)
# 左侧交易标的区域 # ========== 左侧:工具栏 + 持仓表格 ==========
trade_frame = ttk.LabelFrame(tables_frame, text="交易标的详情", padding=10) left_frame = ttk.Frame(tables_frame)
trade_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
# 创建交易标的表格 # 工具栏(在左侧顶部)
self.create_trade_target_table(trade_frame) toolbar_frame = ttk.Frame(left_frame)
toolbar_frame.pack(fill=tk.X, pady=(0, 5))
# 右侧市场监控区域
self.market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10) ttk.Button(toolbar_frame, text=" 添加标的",
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) command=self.btnHandlerAddTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
# 创建市场监控表格 command=self.btnHandlerDelSelectedTradeTarget, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.btnHandlerStartSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🛠 交易设置",
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="▣ 边栏",
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
# 上半部分: 网格策略持仓
grid_frame = ttk.LabelFrame(left_frame, text="网格策略持仓", padding=5)
grid_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 3))
self.create_grid_table(grid_frame)
# 下半部分: 未分类持仓
unclassified_frame = ttk.LabelFrame(left_frame, text="未分类持仓", padding=5)
unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0))
self.create_unclassified_table(unclassified_frame)
# 右侧: Notebook 标签页容器
self.right_notebook = ttk.Notebook(tables_frame)
self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
# Tab 1: 实时价格监控
self.market_frame = ttk.Frame(self.right_notebook)
self.right_notebook.add(self.market_frame, text="实时价格监控")
# 价格监控控件(tab 内顶部)
monitor_control_frame = ttk.Frame(self.market_frame)
monitor_control_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(monitor_control_frame, text="监控配置").pack(side=tk.LEFT, padx=(0, 5))
ttk.Label(monitor_control_frame, text="价格").pack(side=tk.LEFT, padx=(0, 2))
self.monitor_price_entry = ttk.Entry(monitor_control_frame, width=8)
self.monitor_price_entry.insert(0, str(self.monitor_price))
self.monitor_price_entry.pack(side=tk.LEFT, padx=2)
ttk.Button(monitor_control_frame, text="确认",
command=self.btnHandlerSetMonitorPrice, width=6).pack(side=tk.LEFT, padx=2)
self.create_market_monitor_table(self.market_frame) self.create_market_monitor_table(self.market_frame)
# Tab 2: 订单记录 (占位)
model_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(model_tab, text="订单记录")
ttk.Label(model_tab, text="订单记录 - 待实现", font=('Arial', 12),
foreground='gray').pack(expand=True)
# Tab 3: 成交记录 (占位)
dataset_tab = ttk.Frame(self.right_notebook)
self.right_notebook.add(dataset_tab, text="成交记录")
ttk.Label(dataset_tab, text="成交记录 - 待实现", font=('Arial', 12),
foreground='gray').pack(expand=True)
def create_trade_target_table(self, parent): def create_grid_table(self, parent):
"""创建交易标的表格""" """创建网格策略表格"""
columns = ("ID", columns = ("ID",
"股票代码", "股票名称", "市场价", "当前持仓", "建仓成本", "股票", "市场价", "当前持仓",
"平均成本", "网格匹配次数", "网格收益", "交易状态" "平均成本", "当前网格基准价", "交易状态"
) )
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15) self.grid_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 专业化的列配置
column_configs = { column_configs = {
"ID": (50, tk.CENTER), "ID": (50, tk.CENTER),
"股票代码": (80, tk.CENTER), "股票": (120, tk.CENTER),
"股票名称": (80, tk.E), "市场价": (60, tk.E),
"市场价": (70, tk.E), "当前持仓": (70, tk.E),
"当前持仓": (80, tk.E),
"建仓成本": (60, tk.E),
"平均成本": (60, tk.E), "平均成本": (60, tk.E),
"网格匹配次数": (60, tk.E), "当前网格基准价": (100, tk.E),
"网格收益": (60, tk.E), "交易状态": (100, tk.CENTER)
"交易状态": (80, tk.CENTER)
} }
for col in columns: for col in columns:
width, anchor = column_configs[col] width, anchor = column_configs[col]
self.trade_table.heading(col, text=col) self.grid_table.heading(col, text=col)
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore self.grid_table.column(col, width=width, anchor=anchor) # type: ignore
# 填充数据 self.populate_grid_table()
self.populate_trade_table()
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.grid_table.yview)
# 滚动条 self.grid_table.configure(yscrollcommand=scrollbar.set)
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
self.trade_table.configure(yscrollcommand=scrollbar.set) self.grid_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件 self.grid_table.bind("<Double-1>", self.on_grid_table_double_click)
self.trade_table.bind("<Double-1>", self.on_table_double_click) self.grid_table.bind("<Button-3>", self.on_grid_table_right_click)
self.grid_table.bind("<Button-1>", lambda e: setattr(self, '_active_table', 0))
def create_unclassified_table(self, parent):
"""创建未分类持仓表格"""
columns = ("ID",
"股票", "市场价", "当前持仓",
"平均成本"
)
self.unclassified_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
column_configs = {
"ID": (50, tk.CENTER),
"股票": (120, tk.CENTER),
"市场价": (60, tk.E),
"当前持仓": (70, tk.E),
"平均成本": (60, tk.E),
}
for col in columns:
width, anchor = column_configs[col]
self.unclassified_table.heading(col, text=col)
self.unclassified_table.column(col, width=width, anchor=anchor) # type: ignore
self.populate_unclassified_table()
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.unclassified_table.yview)
self.unclassified_table.configure(yscrollcommand=scrollbar.set)
self.unclassified_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.unclassified_table.bind("<Double-1>", self.on_unclassified_table_double_click)
self.unclassified_table.bind("<Button-3>", self.on_unclassified_table_right_click)
self.unclassified_table.bind("<Button-1>", lambda e: setattr(self, '_active_table', 1))
def create_market_monitor_table(self, parent): def create_market_monitor_table(self, parent):
"""创建市场监控表格""" """创建市场监控表格"""
@@ -343,80 +498,175 @@ class TradeTargetUI(ttk.Frame):
else: else:
return "⏸ 已停止" return "⏸ 已停止"
# ---- 右键菜单 ----
def populate_trade_table(self):
"""填充交易标的表格数据""" def on_grid_table_right_click(self, event):
"""网格策略表格右键菜单"""
item = self.grid_table.identify_row(event.y)
if item:
self.grid_table.selection_set(item)
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="移回未分类", command=self.move_to_unclassified)
menu.post(event.x_root, event.y_root)
def on_unclassified_table_right_click(self, event):
"""未分类持仓表格右键菜单"""
item = self.unclassified_table.identify_row(event.y)
if item:
self.unclassified_table.selection_set(item)
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="添加到网格策略", command=self.move_to_grid)
menu.post(event.x_root, event.y_root)
def move_to_grid(self):
"""打开网格配置窗口,保存后自动标记为网格策略"""
target = self.get_selected_target()
if not target:
return
# 对未分类标的应用新默认值(仅内存,不存库)
if target.strategy_type == 0: # type: ignore
target.grid_size = 1.0 # type: ignore
target.grid_volume = 200 # type: ignore
# 不提前标记策略类型,等用户配置保存后由 save_config 自动设置
self.create_grid_config_window(target)
def move_to_unclassified(self):
"""将网格策略标的移回未分类"""
target = self.get_selected_target()
if not target:
return
result = messagebox.askyesno("确认", f"{target.targetName()} 移回未分类?\n\n网格配置将保留但交易将停止。")
if not result:
return
from core.sfgrid.model import STRATEGY_TYPE_UNCLASSIFIED
# 如果正在交易,先停止
if target.enabled and target.get_id() in self.strategy_ctrl: # type: ignore
self.strategy_ctrl[target.get_id()].enabledTrading(False)
target.strategy_type = STRATEGY_TYPE_UNCLASSIFIED # type: ignore
target.enabled = False # type: ignore
target.save()
self.refresh_table()
PrintLog(LogLevel.INFO, f'- [分类] {target.targetName()} → 未分类')
def populate_grid_table(self):
"""填充网格策略表格数据"""
for id, target in self.tradeTargetData.items(): for id, target in self.tradeTargetData.items():
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
continue
grid_info = '-'
if target.status >= 0:
grid_idx = target.grid_index
price_grid = target.getPriceGrid()
if 0 <= grid_idx < len(price_grid):
grid_info = f'{grid_idx}({price_grid[grid_idx]:.2f}元)'
else:
grid_info = f'{grid_idx}(?)'
values = [ values = [
id, id,
target.stock_code, # "股票代码" f"{target.stock_code} {target.stock_name}",
target.stock_name, # "股票名称" f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-',
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-', # "市场价" target.current_position,
target.current_position, # "当前持仓" f"{self.targetAvgPrice[id]:.3f}",
'-' if target.init_price is None else f"{target.init_price:.3f}", # "建仓成本" grid_info,
f"{self.targetAvgPrice[id]:.3f}", # "平均成本" self.get_trade_enabled_indicator(target)
target.grid_match_count, # "网格匹配次数"
f"{target.grid_total_profit:.3f}", # "网格收益"
self.get_trade_enabled_indicator(target) # type: ignore
] ]
self.trade_table.insert('', tk.END, values=values)
self.grid_table.insert('', tk.END, values=values)
def on_table_double_click(self, event):
"""表格双击事件""" def populate_unclassified_table(self):
selected = self.trade_table.selection() """填充未分类持仓表格数据"""
for id, target in self.tradeTargetData.items():
from core.sfgrid.model import STRATEGY_TYPE_UNCLASSIFIED
if target.strategy_type != STRATEGY_TYPE_UNCLASSIFIED: # type: ignore
continue
values = [
id,
f"{target.stock_code} {target.stock_name}",
f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-',
target.current_position,
f"{self.targetAvgPrice[id]:.3f}",
]
self.unclassified_table.insert('', tk.END, values=values)
def on_grid_table_double_click(self, event):
"""网格表格双击事件"""
selected = self.grid_table.selection()
if selected: if selected:
item = selected[0] item = selected[0]
values = self.trade_table.item(item)['values'] values = self.grid_table.item(item)['values']
ctrl = self.strategy_ctrl[values[0]] target_id = int(values[0])
PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}") if target_id in self.strategy_ctrl:
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格") ctrl = self.strategy_ctrl[target_id]
ctrl.printPendingOrder() PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}")
PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格")
ctrl.printPendingOrder()
def on_unclassified_table_double_click(self, event):
"""未分类表格双击 — 添加到网格策略"""
self.move_to_grid()
def get_selected_target(self): def get_selected_target(self):
"""获取选中的交易标的""" """获取选中的交易标的(根据最后点击的表格优先)"""
selected = self.trade_table.selection() # 根据最后点击的表格决定检查顺序
if not selected: if self._active_table == 1:
messagebox.showwarning("未选中", "请先选择一个交易标的") primary, secondary = self.unclassified_table, self.grid_table
return None else:
primary, secondary = self.grid_table, self.unclassified_table
# 获取选中行的ID
item = selected[0] for table in (primary, secondary):
values = self.trade_table.item(item)['values'] selected = table.selection()
target_id = values[0] if selected:
item = selected[0]
# 从列表中找到对应的target对象 values = table.item(item)['values']
for id in self.tradeTargetData: target_id = values[0]
if int(target_id) == id: # type: ignore return self.tradeTargetData.get(int(target_id))
return self.tradeTargetData[id]
messagebox.showwarning("未选中", "请先选择一个交易标的")
return None return None
def refresh_table(self): def refresh_table(self):
"""刷新表格数据""" """刷新表格数据(纯UI操作,在主线程执行)"""
# 保存当前选中的项 # 刷新网格策略表格
selected_items = self.trade_table.selection() selected_items = self.grid_table.selection()
selected_values = [] selected_values = []
for item in selected_items: for item in selected_items:
values = self.trade_table.item(item)['values'] values = self.grid_table.item(item)['values']
if values: if values:
selected_values.append(values[0]) # 保存ID selected_values.append(values[0])
# 清空表格 for item in self.grid_table.get_children():
for item in self.trade_table.get_children(): self.grid_table.delete(item)
self.trade_table.delete(item) self.populate_grid_table()
# 重新填充
self.populate_trade_table()
# 恢复之前选中的项
if selected_values: if selected_values:
for item in self.trade_table.get_children(): for item in self.grid_table.get_children():
values = self.trade_table.item(item)['values'] values = self.grid_table.item(item)['values']
if values and values[0] in selected_values: if values and values[0] in selected_values:
self.trade_table.selection_add(item) self.grid_table.selection_add(item)
# 刷新未分类表格
unselected_items = self.unclassified_table.selection()
unselected_values = []
for item in unselected_items:
values = self.unclassified_table.item(item)['values']
if values:
unselected_values.append(values[0])
for item in self.unclassified_table.get_children():
self.unclassified_table.delete(item)
self.populate_unclassified_table()
if unselected_values:
for item in self.unclassified_table.get_children():
values = self.unclassified_table.item(item)['values']
if values and values[0] in unselected_values:
self.unclassified_table.selection_add(item)
# 刷新市场监控表格 # 刷新市场监控表格
self.populate_market_table() self.populate_market_table()
@@ -451,7 +701,32 @@ class TradeTargetUI(ttk.Frame):
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2) ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2) ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 建仓状态(可变更)
status_text = "已建仓" if target.status >= 1 else "未建仓"
status_color = "green" if target.status >= 1 else "orange"
status_label = ttk.Label(info_frame, text=f"建仓状态: {status_text}", foreground=status_color)
status_label.grid(row=1, column=0, sticky=tk.W, pady=2)
def toggle_position_status():
new_status = 0 if target.status >= 1 else 1
setattr(target, 'status', new_status)
target.save()
new_text = "已建仓" if new_status >= 1 else "未建仓"
new_color = "green" if new_status >= 1 else "orange"
status_label.config(text=f"建仓状态: {new_text}", foreground=new_color)
toggle_btn.config(text="标记为未建仓" if new_status >= 1 else "标记为已建仓")
self.updateTradeTarget(target, False)
PrintLog(LogLevel.INFO, f"建仓状态变更: {target.stock_code}{new_text}")
toggle_btn = ttk.Button(
info_frame,
text="标记为未建仓" if target.status >= 1 else "标记为已建仓",
command=toggle_position_status
)
toggle_btn.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 已建初始仓(仅查看模式)").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置查看框架 # 创建网格配置查看框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10) config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=10)
@@ -550,7 +825,17 @@ class TradeTargetUI(ttk.Frame):
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2) ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2) ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# 建仓状态选择
ttk.Label(info_frame, text="建仓状态:", width=12).grid(row=1, column=0, sticky=tk.W, pady=2)
position_status_var = tk.StringVar(value="未建仓" if target.status < 1 else "已建仓")
position_status_combo = ttk.Combobox(
info_frame, textvariable=position_status_var,
values=["未建仓", "已建仓"], state="readonly", width=10
)
position_status_combo.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
# 创建网格配置框架 # 创建网格配置框架
config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15) config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15)
@@ -692,14 +977,23 @@ class TradeTargetUI(ttk.Frame):
setattr(target, 'grid_volume', grid_volume) setattr(target, 'grid_volume', grid_volume)
setattr(target, 'grid_upper_count', grid_upper_count) setattr(target, 'grid_upper_count', grid_upper_count)
setattr(target, 'grid_lower_count', grid_lower_count) setattr(target, 'grid_lower_count', grid_lower_count)
setattr(target, 'status', 0) # 建仓状态: "已建仓" → 1, "未建仓" → 0
setattr(target, 'status', 1 if position_status_var.get() == "已建仓" else 0)
# grid_index 设为基准价在网格中的位置 (grid_upper_count)
setattr(target, 'grid_index', grid_upper_count)
# 自动标记为网格策略
from core.sfgrid.model import STRATEGY_TYPE_GRID
setattr(target, 'strategy_type', STRATEGY_TYPE_GRID)
# 更新策略控制器 # 更新策略控制器
self.updateTradeTarget(target, True) # 网格配置变更 self.updateTradeTarget(target, True) # 网格配置变更
# 关闭窗口 # 关闭窗口
config_window.destroy() config_window.destroy()
# 立即刷新表格确保数据同步
self.refresh_table()
# 添加日志 # 添加日志
PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}") PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}")
messagebox.showinfo("成功", "网格配置已保存!") messagebox.showinfo("成功", "网格配置已保存!")
@@ -771,10 +1065,11 @@ class TradeTargetUI(ttk.Frame):
new_target = SFGridTradeTarget.create( new_target = SFGridTradeTarget.create(
stock_name=stock_name, stock_name=stock_name,
stock_code=stock_code, stock_code=stock_code,
current_position="0" if pos is None else str(pos.volume), current_position=0 if pos is None else int(pos.volume),
grid_index=gridIndex, grid_index=gridIndex,
init_price=0.0, init_price=0.0,
status=-1 status=-1,
strategy_type=0 # 默认为未分类
) )
# 更新标的池 # 更新标的池
self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化 self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化
@@ -792,14 +1087,12 @@ class TradeTargetUI(ttk.Frame):
def btnHandlerToggleMarketMonitor(self): def btnHandlerToggleMarketMonitor(self):
"""切换市场监控窗口显示/隐藏""" """切换右侧面板显示/隐藏"""
if self.market_monitor_visible: if self.market_monitor_visible:
# 隐藏市场监控窗口 self.right_notebook.pack_forget()
self.market_frame.pack_forget()
self.market_monitor_visible = False self.market_monitor_visible = False
else: else:
# 显示市场监控窗口 self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
self.market_monitor_visible = True self.market_monitor_visible = True
def btnHandlerTradeSettings(self): def btnHandlerTradeSettings(self):
@@ -807,9 +1100,9 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target() target = self.get_selected_target()
if not target: if not target:
return return
# 检查标的的状态,status为1时仅可查看 # 只要暂停交易就可以修改参数,运行中则仅可查看
if target.status == -1 or target.status == 0: if not target.enabled: # type: ignore
self.create_grid_config_window(target) self.create_grid_config_window(target)
else: else:
# 创建只读的网格配置查看窗口 # 创建只读的网格配置查看窗口
@@ -820,7 +1113,12 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target() target = self.get_selected_target()
if not target: if not target:
return return
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
messagebox.showinfo("提示", "该标的不属于网格策略,请先转为网格策略后再启动交易。")
return
if target.status < 0: if target.status < 0:
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。") messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
return return
@@ -853,7 +1151,12 @@ class TradeTargetUI(ttk.Frame):
target = self.get_selected_target() target = self.get_selected_target()
if not target: if not target:
return return
from core.sfgrid.model import STRATEGY_TYPE_GRID
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
messagebox.showinfo("提示", "该标的不属于网格策略。")
return
if not target.enabled: # type: ignore if not target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态") messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
return return
@@ -902,8 +1205,10 @@ class TradeTargetUI(ttk.Frame):
def onTradeTargetDeleted(self, target: SFGridTradeTarget): def onTradeTargetDeleted(self, target: SFGridTradeTarget):
id = target.get_id() id = target.get_id()
del self.tradeTargetData[id] del self.tradeTargetData[id]
del self.strategy_ctrl[id] if id in self.strategy_ctrl:
del self.stockCodeIdMap[target.stock_code] # type: ignore del self.strategy_ctrl[id]
if target.stock_code in self.stockCodeIdMap: # type: ignore
del self.stockCodeIdMap[target.stock_code] # type: ignore
def btnHandlerAddTradeTarget(self): def btnHandlerAddTradeTarget(self):
"""添加新的交易标的""" """添加新的交易标的"""