diff --git a/config.ini b/config.ini index af7c380..b5694bc 100644 --- a/config.ini +++ b/config.ini @@ -1,5 +1,5 @@ [config] -miniqmtpath = D:/Programs/DTQMT/userdata_mini -account_no = 99082560 +miniqmtpath = C:/Programs/GJQMT/userdata_mini +account_no = 8882874667 log_level = INFO diff --git a/core/main_ui.py b/core/main_ui.py index 65ad0e5..d5e76b9 100644 --- a/core/main_ui.py +++ b/core/main_ui.py @@ -27,14 +27,12 @@ class MainWindow: self.logLevel = LogLevel[configLogLevel] PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}") - # 当前选中的策略Tab索引 - self.current_strategy_index = 0 # 存储各个Frame的引用 self.strategy_frames = {} # 日志面板可见性标志 self.log_visible = False self.create_ui() - + eBus.subscribe(EventPrintLog, self.on_log_event) @@ -43,113 +41,24 @@ class MainWindow: # 主容器 main_container = ttk.Frame(self.root) main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 中间主体区域(左右布局) + + # 中间主体区域 content_area = ttk.Frame(main_container) content_area.pack(fill=tk.BOTH, expand=True) - - # 左侧Tab按钮栏(垂直排列) - tab_bar_frame = ttk.Frame(content_area) - tab_bar_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) - - # 创建自定义样式 - 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.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 创建各个策略的Frame + + # 创建策略Frame + strategy_names = ["网格"] self.create_strategy_frames(strategy_names) - + # 创建全局日志面板(默认隐藏) self.create_global_log_panel(main_container) - + # 默认显示第一个策略 - self.switch_strategy_tab(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') - ) + self.show_strategy_frame(0) def create_global_log_panel(self, parent): """创建全局日志面板""" @@ -204,62 +113,22 @@ class MainWindow: def create_strategy_frames(self, strategy_names): """创建各个策略的Frame""" - for idx, name in enumerate(strategy_names): - if idx == 0: - # 第一个Tab使用TradeTargetUI,传入main_window引用 - frame = TradeTargetUI(self.content_container) - self.strategy_frames[idx] = frame - else: - # 其他策略使用占位Frame - frame = ttk.Frame(self.content_container) - self.strategy_frames[idx] = frame - - # 添加占位内容 - placeholder = ttk.Label( - frame, - text=f"{name} - 策略界面将在此实现", - font=('Arial', 14), - foreground='gray' - ) - placeholder.pack(expand=True) - - def switch_strategy_tab(self, index): - """切换策略Tab""" - # 隐藏当前Frame - if self.current_strategy_index in self.strategy_frames: - self.strategy_frames[self.current_strategy_index].pack_forget() - - # 更新当前索引 - self.current_strategy_index = index - - # 显示选中的Frame + frame = TradeTargetUI(self.content_container) + self.strategy_frames[0] = frame + + def show_strategy_frame(self, index): + """显示策略Frame""" if index in self.strategy_frames: self.strategy_frames[index].pack(fill=tk.BOTH, expand=True) - - # 更新Tab按钮样式(可选,用于视觉反馈) - self.update_tab_button_styles() - - def update_tab_button_styles(self): - """更新Tab按钮的样式以显示选中状态""" - # 重置所有按钮为普通书签样式 - 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): """切换日志面板的显示/隐藏""" if self.log_visible: - # 隐藏日志面板 self.log_frame.pack_forget() self.log_visible = False - self.log_toggle_btn.config(text="📋") # 日志图标 else: - # 显示日志面板 self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) self.log_visible = True - self.log_toggle_btn.config(text="🔽") # 使用不同图标表示隐藏 def on_exit(self): """退出程序""" diff --git a/core/qmt_dummy.py b/core/qmt_dummy.py index 55b0b84..2e3b9a2 100644 --- a/core/qmt_dummy.py +++ b/core/qmt_dummy.py @@ -107,6 +107,13 @@ class DummyQmtV: } 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): """获取持仓 (模拟)""" if stock_code in self._positions: @@ -207,6 +214,13 @@ class DummyQmtV: """获取跌停价 (模拟)""" 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): """启动市场数据订阅 (模拟)""" try: diff --git a/core/qmt_real.py b/core/qmt_real.py index 4d4bdf1..f569252 100644 --- a/core/qmt_real.py +++ b/core/qmt_real.py @@ -1,25 +1,368 @@ """ -QMT 模块统一入口 -根据环境自动选择真实 QMT 或模拟器 +QMT 真实交易实现 - 封装 xtquant SDK """ -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 模块""" - if sys.platform == 'win32': + +class RealQmtV: + """ + 真实 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: - # Windows 环境尝试导入真实 QMT - import core.qmt_real as qmt_module - return qmt_module.qmtv - except ImportError: - pass + from xtquant.xttrader import XtQuantTrader + from xtquant.xttype import StockAccount - # 非 Windows 或导入失败,使用模拟器 - try: - import core.qmt_dummy as qmt_module - return qmt_module.qmtv - except ImportError: - raise ImportError("无法加载 QMT 模块") + self.mini_qmt_path = config.miniQMTPath + self.account = StockAccount(config.account_no, 'STOCK') -# 导出单例 -qmtv = _get_qmt() \ No newline at end of file + # 创建 XtQuantTrader 实例 + 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" 格式 key,UI 使用纯代码 "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() diff --git a/core/sfgrid/model.py b/core/sfgrid/model.py index 5878143..b702581 100644 --- a/core/sfgrid/model.py +++ b/core/sfgrid/model.py @@ -2,6 +2,10 @@ from peewee import CharField, IntegerField, FloatField, BooleanField from core.database import BaseModel, db +# 策略类型常量 +STRATEGY_TYPE_UNCLASSIFIED = 0 # 未分类持仓 +STRATEGY_TYPE_GRID = 1 # 网格策略 + # 定义Target类,对应targets表 class SFGridTradeTarget(BaseModel): @@ -14,10 +18,11 @@ class SFGridTradeTarget(BaseModel): grid_total_profit = FloatField(default=0.0) status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中 enabled = BooleanField(default=False) # 是否启动交易线程 + strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略 grid_start_price = FloatField(default=10.0) # 基线价格 - grid_size = FloatField(default=0.1) # 网格价位差 - grid_volume = IntegerField(default=100) # 网格交易量 + grid_size = FloatField(default=1.0) # 网格价位差 + grid_volume = IntegerField(default=200) # 网格交易量 grid_upper_count = IntegerField(default=1) # 基线价格上方网格数 grid_lower_count = IntegerField(default=10) # 基线价格下方网格数 @@ -41,4 +46,15 @@ class SFGridTradeTarget(BaseModel): return self.priceGrid -db.create_tables([SFGridTradeTarget]) \ No newline at end of file +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 \ No newline at end of file diff --git a/core/sfgrid/sfgrid_strategy.py b/core/sfgrid/sfgrid_strategy.py index 2c785e9..c0dc327 100644 --- a/core/sfgrid/sfgrid_strategy.py +++ b/core/sfgrid/sfgrid_strategy.py @@ -20,11 +20,12 @@ class SFGridStrategy: event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade) self.todayUpStopPrice=qmtv.dailyUpStop(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.loadExistOrders() self.enabledTrading(tradeTarget.enabled) # type: ignore self.dataUpdateLock = threading.Lock() + PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: grid_index={self.tradeTarget.grid_index}') def loadExistOrders(self): orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore @@ -123,13 +124,17 @@ class SFGridStrategy: self.dataUpdateLock.release() 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 if enabled: PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}") if self.tradeTarget.status == 0: # 未建仓 - PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,") - self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue] + if self.tradeTarget.grid_index == 0: + 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: # 已建仓 # 交易阶段,检查仓位,检查现有订单 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}] - 建仓单成交') self.tradeTarget.status = 1 # 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 type = "建仓单" else: @@ -218,6 +224,7 @@ class SFGridStrategy: return "SFGRID" 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() event_bus.publish(EventTradeTargetUpdate, self.tradeTarget) return rc \ No newline at end of file diff --git a/core/sfgrid/sfgrid_ui.py b/core/sfgrid/sfgrid_ui.py index b2b6786..9840f31 100644 --- a/core/sfgrid/sfgrid_ui.py +++ b/core/sfgrid/sfgrid_ui.py @@ -24,6 +24,8 @@ class TradeTargetUI(ttk.Frame): self.listening_stock = [] # 监控价格,默认值为10 self.monitor_price = 10.0 + # 追踪最后点击的表格 (0=网格, 1=未分类) + self._active_table = 0 self.init_trade_target_pool() @@ -32,20 +34,66 @@ class TradeTargetUI(ttk.Frame): # 市场监控窗口显示状态 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() 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.EventTradeTargetDeleted, self.onTradeTargetDeleted) 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() for temp in results: 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 if pos is None: self.targetAvgPrice[tradeTarget.get_id()] = 0.0 @@ -53,7 +101,9 @@ class TradeTargetUI(ttk.Frame): 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.DEBUG, f'- [DEBUG] updateTradeTarget 前: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}') 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)} 个标的') @@ -99,13 +149,19 @@ class TradeTargetUI(ttk.Frame): target.save() 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 - - 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.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: pos = qmtv.getStockPosition(target.stock_code) if pos is not None: @@ -118,35 +174,7 @@ class TradeTargetUI(ttk.Frame): main_frame = ttk.Frame(self) 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.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)) - - - # 表格区域 + # 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页) self.create_tables_area(main_frame) # 启动刷新线程 @@ -154,76 +182,203 @@ class TradeTargetUI(ttk.Frame): 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): - """刷新循环""" + """刷新循环(后台线程:拉取市价 + 调度UI刷新)""" while True: - self.after(0, self.refresh_table) - self.after(0, self.populate_market_table) - time.sleep(0.5) # 每0.5秒刷新一次 + if self._market_active: + # 盘中:每5s拉取缺失的市价 + 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): - """创建表格区域""" + """创建表格区域 — 左侧工具栏+持仓表格,右侧Notebook""" # 创建主表格框架(水平排列) tables_frame = ttk.Frame(parent) - tables_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5)) - - # 左侧交易标的区域 - trade_frame = ttk.LabelFrame(tables_frame, text="交易标的详情", padding=10) - trade_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) - - # 创建交易标的表格 - self.create_trade_target_table(trade_frame) - - # 右侧市场监控区域 - self.market_frame = ttk.LabelFrame(tables_frame, text="市场监控", padding=10) - self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) - - # 创建市场监控表格 + tables_frame.pack(fill=tk.BOTH, expand=True) + + # ========== 左侧:工具栏 + 持仓表格 ========== + left_frame = ttk.Frame(tables_frame) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) + + # 工具栏(在左侧顶部) + toolbar_frame = ttk.Frame(left_frame) + toolbar_frame.pack(fill=tk.X, pady=(0, 5)) + + 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) + + # 上半部分: 网格策略持仓 + 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) + # 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", - "股票代码", "股票名称", "市场价", "当前持仓", "建仓成本", - "平均成本", "网格匹配次数", "网格收益", "交易状态" + "股票", "市场价", "当前持仓", + "平均成本", "当前网格基准价", "交易状态" ) - - 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 = { "ID": (50, tk.CENTER), - "股票代码": (80, tk.CENTER), - "股票名称": (80, tk.E), - "市场价": (70, tk.E), - "当前持仓": (80, tk.E), - "建仓成本": (60, tk.E), + "股票": (120, tk.CENTER), + "市场价": (60, tk.E), + "当前持仓": (70, tk.E), "平均成本": (60, tk.E), - "网格匹配次数": (60, tk.E), - "网格收益": (60, tk.E), - "交易状态": (80, tk.CENTER) + "当前网格基准价": (100, tk.E), + "交易状态": (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) + self.grid_table.heading(col, text=col) + self.grid_table.column(col, width=width, anchor=anchor) # type: ignore + + self.populate_grid_table() + + scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.grid_table.yview) + self.grid_table.configure(yscrollcommand=scrollbar.set) + + self.grid_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - # 绑定双击事件 - self.trade_table.bind("", self.on_table_double_click) + + self.grid_table.bind("", self.on_grid_table_double_click) + self.grid_table.bind("", self.on_grid_table_right_click) + self.grid_table.bind("", 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("", self.on_unclassified_table_double_click) + self.unclassified_table.bind("", self.on_unclassified_table_right_click) + self.unclassified_table.bind("", lambda e: setattr(self, '_active_table', 1)) def create_market_monitor_table(self, parent): """创建市场监控表格""" @@ -343,80 +498,175 @@ class TradeTargetUI(ttk.Frame): else: 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(): + 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 = [ id, - target.stock_code, # "股票代码" - target.stock_name, # "股票名称" - f"{self.targetMarketPrice[id]:.3f}" if id in self.targetMarketPrice else '-', # "市场价" - target.current_position, # "当前持仓" - '-' if target.init_price is None else f"{target.init_price:.3f}", # "建仓成本" - f"{self.targetAvgPrice[id]:.3f}", # "平均成本" - target.grid_match_count, # "网格匹配次数" - f"{target.grid_total_profit:.3f}", # "网格收益" - self.get_trade_enabled_indicator(target) # type: ignore + 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}元", + grid_info, + self.get_trade_enabled_indicator(target) ] - - self.trade_table.insert('', tk.END, values=values) - - def on_table_double_click(self, event): - """表格双击事件""" - selected = self.trade_table.selection() + self.grid_table.insert('', tk.END, values=values) + + def populate_unclassified_table(self): + """填充未分类持仓表格数据""" + 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: item = selected[0] - values = self.trade_table.item(item)['values'] - ctrl = self.strategy_ctrl[values[0]] - PrintLog(LogLevel.DEBUG, f"双击查看详情: {values[0]} - {values[1]}") - PrintLog(LogLevel.DEBUG, f"双击查看详情 - 订单网格") - ctrl.printPendingOrder() + values = self.grid_table.item(item)['values'] + target_id = int(values[0]) + if target_id in self.strategy_ctrl: + ctrl = self.strategy_ctrl[target_id] + 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): - """获取选中的交易标的""" - 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.tradeTargetData: - if int(target_id) == id: # type: ignore - return self.tradeTargetData[id] - + """获取选中的交易标的(根据最后点击的表格优先)""" + # 根据最后点击的表格决定检查顺序 + if self._active_table == 1: + primary, secondary = self.unclassified_table, self.grid_table + else: + primary, secondary = self.grid_table, self.unclassified_table + + for table in (primary, secondary): + selected = table.selection() + if selected: + item = selected[0] + values = table.item(item)['values'] + target_id = values[0] + return self.tradeTargetData.get(int(target_id)) + + messagebox.showwarning("未选中", "请先选择一个交易标的") return None - + def refresh_table(self): - """刷新表格数据""" - # 保存当前选中的项 - selected_items = self.trade_table.selection() + """刷新表格数据(纯UI操作,在主线程执行)""" + # 刷新网格策略表格 + selected_items = self.grid_table.selection() selected_values = [] for item in selected_items: - values = self.trade_table.item(item)['values'] + values = self.grid_table.item(item)['values'] if values: - selected_values.append(values[0]) # 保存ID - - # 清空表格 - for item in self.trade_table.get_children(): - self.trade_table.delete(item) - - # 重新填充 - self.populate_trade_table() - - # 恢复之前选中的项 + selected_values.append(values[0]) + + for item in self.grid_table.get_children(): + self.grid_table.delete(item) + self.populate_grid_table() + if selected_values: - for item in self.trade_table.get_children(): - values = self.trade_table.item(item)['values'] + for item in self.grid_table.get_children(): + values = self.grid_table.item(item)['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() @@ -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_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) @@ -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_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) @@ -692,14 +977,23 @@ class TradeTargetUI(ttk.Frame): setattr(target, 'grid_volume', grid_volume) setattr(target, 'grid_upper_count', grid_upper_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) # 网格配置变更 - + # 关闭窗口 config_window.destroy() - + + # 立即刷新表格确保数据同步 + self.refresh_table() + # 添加日志 PrintLog(LogLevel.INFO, f"网格配置已保存: {target.stock_code} - {target.stock_name}") messagebox.showinfo("成功", "网格配置已保存!") @@ -771,10 +1065,11 @@ class TradeTargetUI(ttk.Frame): new_target = SFGridTradeTarget.create( stock_name=stock_name, 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, init_price=0.0, - status=-1 + status=-1, + strategy_type=0 # 默认为未分类 ) # 更新标的池 self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化 @@ -792,14 +1087,12 @@ class TradeTargetUI(ttk.Frame): def btnHandlerToggleMarketMonitor(self): - """切换市场监控窗口显示/隐藏""" + """切换右侧面板显示/隐藏""" if self.market_monitor_visible: - # 隐藏市场监控窗口 - self.market_frame.pack_forget() + self.right_notebook.pack_forget() self.market_monitor_visible = False else: - # 显示市场监控窗口 - self.market_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) + self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) self.market_monitor_visible = True def btnHandlerTradeSettings(self): @@ -807,9 +1100,9 @@ class TradeTargetUI(ttk.Frame): target = self.get_selected_target() if not target: return - - # 检查标的的状态,status为1时仅可查看 - if target.status == -1 or target.status == 0: + + # 只要暂停交易就可以修改参数,运行中则仅可查看 + if not target.enabled: # type: ignore self.create_grid_config_window(target) else: # 创建只读的网格配置查看窗口 @@ -820,7 +1113,12 @@ class TradeTargetUI(ttk.Frame): target = self.get_selected_target() if not target: 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: messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。") return @@ -853,7 +1151,12 @@ class TradeTargetUI(ttk.Frame): target = self.get_selected_target() if not target: 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 messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态") return @@ -902,8 +1205,10 @@ class TradeTargetUI(ttk.Frame): def onTradeTargetDeleted(self, target: SFGridTradeTarget): id = target.get_id() del self.tradeTargetData[id] - del self.strategy_ctrl[id] - del self.stockCodeIdMap[target.stock_code] # type: ignore + if id in self.strategy_ctrl: + 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): """添加新的交易标的"""