From 363efd6d2d6f284b09093c7711f05fbdd4c277fe Mon Sep 17 00:00:00 2001 From: "WIN-ALB39P9B6BU\\Docker" Date: Wed, 24 Jun 2026 14:02:08 +0800 Subject: [PATCH] update --- .gitignore | 1 + core/ui/flet/app.py | 836 --------------- core/ui/flet/app_v2.py | 95 +- core/ui/tkinter/__init__.py | 0 core/ui/tkinter/main_window.py | 142 --- core/ui/tkinter/sfgrid_view.py | 1850 -------------------------------- core/ui/tkinter/splash.py | 112 -- grid_strage.py | 259 ----- starter.py | 25 +- 9 files changed, 99 insertions(+), 3221 deletions(-) delete mode 100644 core/ui/flet/app.py delete mode 100644 core/ui/tkinter/__init__.py delete mode 100644 core/ui/tkinter/main_window.py delete mode 100644 core/ui/tkinter/sfgrid_view.py delete mode 100644 core/ui/tkinter/splash.py delete mode 100644 grid_strage.py diff --git a/.gitignore b/.gitignore index 1571093..955fc65 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ example.db.bak venv/ flet_desktop/ .flet/ +sfgrid.log diff --git a/core/ui/flet/app.py b/core/ui/flet/app.py deleted file mode 100644 index d81d81a..0000000 --- a/core/ui/flet/app.py +++ /dev/null @@ -1,836 +0,0 @@ -""" -Flet UI — 完整对齐 Tkinter 版布局、数据流、刷新机制。 -""" -import asyncio -import time -import threading -import flet as ft - -from core.qmt_real import RealQmtV, qmtv -from core.logger import LogLevel, PrintLog -from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID, STRATEGY_TYPE_UNCLASSIFIED -from core.sfgrid.sfgrid_strategy import SFGridStrategy -from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch -from core.sfgrid.bus_events import EventTradeTargetUpdate - - -# ── 委托状态 / 方向映射 ── -_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤', - 53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'} - - -def _fmt_time(t) -> str: - """格式化 QMT 时间为 HH:MM:SS(北京时间,Unix timestamp → 本地时间)""" - if not t: - return '' - import datetime - try: - ts = int(t) - if ts > 1e12: # 毫秒级 - ts //= 1000 - return datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S') - except (ValueError, OSError): - return str(t) - - -def _direction(ot: int) -> str: - return '买' if ot == 23 else '卖' if ot == 24 else str(ot) - - -def _plain(code: str) -> str: - return code.split('.')[0] if '.' in code else code - - -# ══════════════════════════════════════════════════════════════════════ -# QmtApp -# ══════════════════════════════════════════════════════════════════════ - -class QmtApp: - """Flet 版 QMT 交易界面,布局、数据流对齐 core/ui/tkinter/sfgrid_view.py""" - - def __init__(self, page: ft.Page): - self.page = page - self.page.title = "神之一手" - self.page.window.width = 1400 - self.page.window.height = 800 - self.page.padding = 0 - - # ── 状态(对齐 Tkinter TradeTargetUI) ── - self.tradeTargetData: dict[int, SFGridTradeTarget] = {} - self.stockCodeIdMap: dict[str, int] = {} - self.strategy_ctrl: dict[int, SFGridStrategy] = {} - self.targetMarketPrice: dict[int, float] = {} - self.targetPreClose: dict[int, float] = {} # 昨收 - self.targetAvgPrice: dict[int, float] = {} - self.marketData: dict[str, dict] = {} # stock_code → {stock_name, last_price, time} - self.listening_stock: list = [] - self.monitor_price: float = 10.0 - self._market_active: bool = qmtv.isMarketActive - self._refresh_cycle: int = 0 - self._drawer_open: bool = False - self._selected_target = None - self._prices_loaded: bool = False - self._orders: list = [] - self._trades: list = [] - - self._run_startup() - - # ══════════════════════════════════════════════════════════════ - # 启动流程(对齐 tkinter/splash.py) - # ══════════════════════════════════════════════════════════════ - - def _run_startup(self): - """启动进度 — 先渲染 splash,再异步执行启动步骤""" - bar = ft.ProgressBar(width=340, value=0, color='#0078d4') - self._splash_status = ft.Text("正在初始化...", size=13) - self._splash_bar = bar - splash = ft.Container( - ft.Column([ - ft.Text("神之一手", size=22, weight=ft.FontWeight.BOLD, color='#0078d4'), - ft.Text("交易系统", size=14, color='#666666'), - ft.Container(height=20), - self._splash_status, - ft.Container(height=8), - bar, - ], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER), - width=380, height=200, - bgcolor=ft.Colors.SURFACE, - border_radius=12, - shadow=ft.BoxShadow(blur_radius=20, color='#20000000'), - alignment=ft.Alignment.CENTER, - ) - self.page.add(ft.Container( - content=splash, - alignment=ft.Alignment.CENTER, expand=True, - bgcolor='#F5F5F5', - )) - self.page.update() - - # 异步执行启动,确保 splash 先渲染 - asyncio.ensure_future(self._do_startup()) - - async def _do_startup(self): - """异步启动流程 — splash 已渲染,逐步执行并更新进度""" - # 给渲染一帧的时间 - await asyncio.sleep(0.05) - - steps = [ - ("正在检查 QMT 环境...", 0.10, lambda: RealQmtV._discover_qmt_port() or True), - ("正在初始化交易器...", 0.35, lambda: qmtv.init_qmtv()), - ("正在连接 QMT...", 0.55, lambda: qmtv.connect() or True), - ("正在加载持仓数据...", 0.75, lambda: self._init_data()), - ("正在构建界面...", 0.85, lambda: None), - ("正在初始化策略...", 0.92, lambda: self._init_strategies()), - ] - for text, pct, action in steps: - self._splash_status.value = text - self._splash_bar.value = pct - self.page.update() - try: - result = action() - if result is False: - self._show_error(f"启动失败: {text}") - return - except Exception as e: - self._show_error(f"启动异常: {text}\n{e}") - return - - self._splash_status.value = "启动完成" - self._splash_bar.value = 1.0 - self.page.update() - await asyncio.sleep(0.3) - - self.page.clean() - self._build_main_ui() - self.page.update() - - # 主动拉取市价(不等行情推送) - self._pull_prices() - # 加载委托/成交数据 - self._refresh_orders() - self._refresh_trades() - self._rebuild_tables() - self.page.update() - - # 订阅事件 + 后台刷新 - event_bus.subscribe(MarketDataUpdate, self._on_market_data) - event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active_switch) - event_bus.subscribe(EventTradeTargetUpdate, self._on_strategy_update) - threading.Thread(target=self._refresh_loop, daemon=True).start() - - def _show_error(self, msg: str): - self.page.clean() - self.page.add(ft.Container( - content=ft.Column([ - ft.Icon(ft.Icons.ERROR_OUTLINE, size=48, color=ft.Colors.RED), - ft.Text(msg, size=16), - ft.ElevatedButton("重试", on_click=lambda e: self._retry()), - ], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER), - alignment=ft.Alignment.CENTER, expand=True, - )) - self.page.update() - - def _retry(self): - self.page.clean() - self._run_startup() - - # ══════════════════════════════════════════════════════════════ - # 数据初始化(对齐 Tkinter init_trade_target_pool) - # ══════════════════════════════════════════════════════════════ - - def _init_data(self): - positions = qmtv.getAllPositions() - PrintLog(LogLevel.INFO, f'[Flet] 持仓: {len(positions)} 个') - - for code, pos in positions.items(): - existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code) - if existing is None: - name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code) - SFGridTradeTarget.create( - stock_code=code, stock_name=name, - current_position=int(pos.volume), - init_price=float(getattr(pos, 'avg_price', 0) or 0), - grid_index=0, enabled=False, - grid_start_price=float(getattr(pos, 'avg_price', 0) or 0) or 10.0, - grid_size=1.0, grid_volume=200, grid_upper_count=1, grid_lower_count=10, - ) - - # 获取昨收价(需要带后缀的完整代码) - try: - from xtquant import xtdata - for stock_code, pos in positions.items(): - full_code = stock_code - if '.' not in stock_code: - c = stock_code - full_code = f'{c}.SH' if c.startswith(('6', '5', '9')) else f'{c}.SZ' - detail = xtdata.get_instrument_detail(full_code) - if detail: - pre_close = detail.get('PreClose', 0) if isinstance(detail, dict) else getattr(detail, 'PreClose', 0) - if pre_close > 0: - self.targetPreClose[stock_code] = float(pre_close) - PrintLog(LogLevel.INFO, f'[Flet] 已获取 {len(self.targetPreClose)} 个标的昨收价') - except Exception as e: - PrintLog(LogLevel.DEBUG, f'[Flet] 昨收价获取异常: {e}') - - results = list(SFGridTradeTarget.select()) - for t in results: - pos = positions.get(t.stock_code) - t.current_position = 0 if pos is None else int(pos.volume) - tid = t.get_id() - self.tradeTargetData[tid] = t - self.stockCodeIdMap[t.stock_code] = tid - if pos is not None: - self.targetAvgPrice[tid] = float(getattr(pos, 'avg_price', 0) or 0) - - def _init_strategies(self): - from core.sfgrid.model import STRATEGY_TYPE_GRID - for tid, t in self.tradeTargetData.items(): - if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled: - self.strategy_ctrl[tid] = SFGridStrategy(t) - - # ══════════════════════════════════════════════════════════════ - # 主界面构建(对齐 Tkinter create_tables_area) - # ══════════════════════════════════════════════════════════════ - - def _build_main_ui(self): - # ── 右侧面板内容 ── - self._tab_orders = ft.Tab(label="当前委托") - self._tab_trades = ft.Tab(label="当日成交") - right_bar = ft.TabBar(tabs=[ - ft.Tab(label="实时价格监控"), - self._tab_orders, - self._tab_trades, - ft.Tab(label="未分类持仓"), - ]) - self._uncl_list = ft.ListView([self._build_unclassified_table()], expand=True) - self._right_view = ft.TabBarView(controls=[ - self._build_market_view(), - self._build_order_view(), - self._build_trade_view(), - self._uncl_list, - ], expand=True) - panel_content = ft.Container( - content=ft.Column([ - ft.Container(ft.Text("监控面板", size=14, weight=ft.FontWeight.BOLD), padding=ft.Padding(10, 10, 10, 5)), - ft.Tabs(ft.Column([right_bar, self._right_view], expand=True), length=4, expand=True), - ], expand=True), - width=700, bgcolor=ft.Colors.SURFACE, - ) - - # ── 遮罩层(点击关闭) ── - backdrop = ft.Container( - bgcolor='#44000000', expand=True, - on_click=lambda e: self._hide_overlay(), - ) - - # ── overlay 行:遮罩 + 面板 ── - self._overlay = ft.Container( - ft.Row([backdrop, panel_content], spacing=0), - visible=False, expand=True, - ) - - # ── 标题栏(始终可见,选中行后显示操作按钮) ── - self._sidebar_icon = _PanelIcon('sidebar', active=False, on_click=lambda e: self._toggle_overlay()) - self._sel_actions = ft.Row([], spacing=4) # 动态操作按钮 - self._sel_info = ft.Text("", size=12, color='#666666') - grid_title = ft.Container( - ft.Row([ - ft.Row([ - ft.Text("网格策略持仓", size=13, weight=ft.FontWeight.BOLD), - self._sel_info, - self._sel_actions, - ]), - ft.Row([ - ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18, - on_click=lambda e: self._manual_refresh()), - self._sidebar_icon, - ], spacing=0), - ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), - padding=ft.Padding(10, 10, 10, 5), - ) - - # ── 表格(Stack 内,可被 overlay 覆盖) ── - self._grid_list = self._build_grid_table() # 回到 DataTable - grid_body = ft.Container( - content=self._grid_list, expand=True, - padding=ft.Padding(10, 0, 10, 10), - ) - - self.page.add(ft.Column([ - grid_title, - ft.Stack([grid_body, self._overlay], expand=True), - ], expand=True)) - - # ── 表格工具 ── - - def _dt(self, cols: list[str], rows: list[list[str]], col_widths: list = None) -> ft.Control: - """构建 DataTable""" - data_cols = [ft.DataColumn(ft.Text(h)) for h in cols] - data_rows = [] - for r in rows: - cells = [] - for i, c in enumerate(r): - w = col_widths[i] if col_widths and i < len(col_widths) else None - cells.append(ft.DataCell(ft.Text(str(c), overflow=ft.TextOverflow.ELLIPSIS, - max_lines=1, width=w))) - data_rows.append(ft.DataRow(cells=cells)) - if not data_rows: - data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols])) - return ft.ListView([ft.DataTable( - columns=data_cols, rows=data_rows, - width=float('inf'), - heading_row_height=36, data_row_min_height=32, - )], expand=True) - - # ── 各表格 ── - - def _pending_tags(self, stock_code: str) -> list: - """返回该标的下挂单的方向标签列表:'多'(买单) / '空'(卖单)""" - tags = [] - _TERMINAL = {54, 56, 57} - for o in self._orders: - if _plain(getattr(o, 'stock_code', '')) != stock_code: - continue - if getattr(o, 'order_status', 0) in _TERMINAL: - continue - ot = getattr(o, 'order_type', 0) - if ot == 23 and '多' not in tags: - tags.append('多') - elif ot == 24 and '空' not in tags: - tags.append('空') - return tags - - def _tag_badge(self, text: str, color: str) -> ft.Container: - return ft.Container( - ft.Text(text, size=10, color='white', weight=ft.FontWeight.BOLD), - bgcolor=color, border_radius=4, padding=ft.Padding(3, 1, 3, 1), - ) - - def _on_grid_row_select(self, target): - """DataRow 选中回调 — 在标题栏显示操作按钮""" - self._selected_target = target - name = f'{target.stock_code} {target.stock_name}' - self._sel_info.value = f" | 已选: {name}" - actions = [] - if target.enabled: - actions.append(ft.ElevatedButton("⏸ 暂停", on_click=lambda e, t=target: self._on_stop_trade(t), height=28)) - else: - actions.append(ft.ElevatedButton("▶ 启动", on_click=lambda e, t=target: self._on_start_trade(t), height=28)) - actions.append(ft.ElevatedButton("⚙ 设置", on_click=lambda e, t=target: self._open_grid_config(t), height=28)) - self._sel_actions.controls = actions - self.page.update() - - def _build_grid_table(self) -> ft.Control: - """网格表格 — DataTable + on_select_change""" - cols = ["ID", "股票", "市场价", "持仓", "成本", "网格基准", "状态"] - data_cols = [ft.DataColumn(ft.Text(h)) for h in cols] - data_rows = [] - is_sel = self._selected_target is not None - sel_id = self._selected_target.get_id() if self._selected_target else -1 - for tid, t in self.tradeTargetData.items(): - if t.strategy_type != 1: - continue - pg = t.getPriceGrid() - idx = t.grid_index - grid_base = pg[idx] if 0 <= idx < len(pg) else 0 - mp = self.targetMarketPrice.get(tid, 0) or 0 - pre_close = self.targetPreClose.get(t.stock_code, 0) or 0 - up = mp > pre_close and pre_close > 0 - down = mp < pre_close and mp > 0 and pre_close > 0 - pcolor = '#CC0000' if up else '#009900' if down else None - gtext = ft.Text(f'{grid_base:.2f}', weight=ft.FontWeight.BOLD) - gparts = [gtext] - if mp > grid_base > 0: - gparts.append(ft.Text(' ▲', color='#CC0000', weight=ft.FontWeight.BOLD)) - elif 0 < mp < grid_base: - gparts.append(ft.Text(' ▼', color='#009900', weight=ft.FontWeight.BOLD)) - for tag in self._pending_tags(t.stock_code): - gparts.append(self._tag_badge(tag, '#E67E22' if tag == '多' else '#3498DB')) - gcell = ft.Row(gparts, spacing=3) if len(gparts) > 1 else gtext - dr = ft.DataRow(cells=[ - ft.DataCell(ft.Text(str(tid))), - ft.DataCell(ft.Text(f'{t.stock_code} {t.stock_name}')), - ft.DataCell(ft.Text(f'{mp:.3f}', color=pcolor, weight=ft.FontWeight.BOLD)), - ft.DataCell(ft.Text(str(t.current_position))), - ft.DataCell(ft.Text(f'{self.targetAvgPrice.get(tid, 0):.3f}')), - ft.DataCell(gcell), - ft.DataCell(ft.Text('▶运行中' if t.enabled else '⏸已暂停')), - ], selected=(is_sel and tid == sel_id)) - dr.on_select_change = lambda e, t=t: self._on_grid_row_select(t) - data_rows.append(dr) - if not data_rows: - data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols])) - return ft.ListView([ft.DataTable(columns=data_cols, rows=data_rows, - width=float('inf'), - heading_row_height=36, data_row_min_height=32)], expand=True) - - def _build_unclassified_table(self) -> ft.Control: - cols = ["ID", "股票", "市场价", "当前持仓", "平均成本"] - rows = [] - for tid, t in self.tradeTargetData.items(): - if t.strategy_type == STRATEGY_TYPE_GRID: - continue - mp = self.targetMarketPrice.get(tid, 0) or 0 - rows.append([ - str(tid), - f'{t.stock_code} {t.stock_name}', - f'{mp:.3f}', - str(t.current_position), - f'{self.targetAvgPrice.get(tid, 0):.3f}元', - ]) - return self._dt(cols, rows) - - def _build_market_view(self) -> ft.Control: - """实时价格监控 — 监控配置 + 表格""" - price_input = ft.TextField(value=str(self.monitor_price), width=80, height=32, - text_size=13, content_padding=ft.Padding(4, 0, 4, 0)) - confirm_btn = ft.ElevatedButton("确认", on_click=lambda e: self._set_monitor_price(price_input.value), height=32) - - self._market_table = self._dt(["时间", "股票名称", "最新价格"], []) - return ft.Column([ - ft.Row([ - ft.Text("监控配置", size=13), ft.Text("价格", size=13), - price_input, confirm_btn, - ]), - ft.Container(content=self._market_table, expand=True), - ], expand=True) - - def _build_order_view(self) -> ft.Control: - self._order_table = self._dt( - ["时间", "代码", "名称", "方向", "委托价", "委托量", "已成交", "均价", "状态"], [], - col_widths=[65, 55, 70, 35, 60, 80, 55, 50]) - return self._order_table - - def _build_trade_view(self) -> ft.Control: - self._trade_table = self._dt( - ["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], [], - col_widths=[65, 55, 70, 35, 65, 60, 70, 55]) - return self._trade_table - - # ══════════════════════════════════════════════════════════════ - # 事件回调(对齐 Tkinter onMarketDataUpdated) - # ══════════════════════════════════════════════════════════════ - - def _on_market_data(self, data: dict): - """行情数据回调 — 来自 QMT 推送""" - need_rebuild = not self._prices_loaded - updated_count = 0 - for stock_code, tick in data.items(): - plain = _plain(stock_code) - tid = self.stockCodeIdMap.get(plain) - lp = tick.get('lastPrice', 0) - - if tid is not None and tid in self.tradeTargetData: - self.targetMarketPrice[tid] = lp - self.tradeTargetData[tid].market_price = lp - updated_count += 1 - else: - # 非目标标的:监控价格触发时记录 - if lp == self.monitor_price or stock_code in self.listening_stock: - if stock_code not in self.listening_stock: - self.listening_stock.append(stock_code) - t_str = time.strftime("%H:%M:%S") - name = qmtv.getInstrumentName(stock_code) - self.marketData[stock_code] = {'stock_name': name, 'last_price': lp, 'time': t_str} - - if need_rebuild and not self._prices_loaded and updated_count > 0: - self._prices_loaded = True - self._rebuild_tables() - self.page.update() - - def _on_market_active_switch(self, is_active: bool): - self._market_active = is_active - - def _on_strategy_update(self, target): - """策略数据变更 — 成交后立即刷新表格""" - self._rebuild_tables() - self.page.update() - - # ══════════════════════════════════════════════════════════════ - # 刷新循环(对齐 Tkinter refresh_loop) - # ══════════════════════════════════════════════════════════════ - - def _pull_prices(self): - """主动拉取缺失的市价(对齐 Tkinter refresh_loop)""" - for tid, t in self.tradeTargetData.items(): - if tid not in self.targetMarketPrice or self.targetMarketPrice[tid] == 0: - price = qmtv.getLastPrice(t.stock_code) - if price > 0: - self.targetMarketPrice[tid] = price - t.market_price = price - - def _manual_refresh(self): - self._pull_prices() - self._refresh_positions() - self._refresh_orders() - self._refresh_trades() - self._rebuild_tables() - self.page.update() - - def _refresh_positions(self): - positions = qmtv.getAllPositions() - for t in self.tradeTargetData.values(): - pos = positions.get(t.stock_code) - t.current_position = 0 if pos is None else int(pos.volume) - - def _refresh_orders(self): - try: - self._orders = list(qmtv.queryTodayOrders()) - except Exception: - pass - - def _refresh_trades(self): - try: - self._trades = list(qmtv.queryTodayTrades()) - except Exception: - pass - - def _rebuild_tables(self): - """重建所有表格数据""" - self._grid_list.controls = [self._build_grid_table()] - if not self._selected_target: - self._sel_info.value = "" - self._sel_actions.controls = [] - self._uncl_list.controls = [self._build_unclassified_table()] - - # 委托 — 过滤已终结订单(已撤/已成/废单),按 order_id 去重 - _TERMINAL = {54, 56, 57} - o_map = {} # order_id → latest order - for o in self._orders: - oid = str(getattr(o, 'order_id', '')) - if not oid: - continue - o_map[oid] = o # 后面的覆盖前面的 - o_rows = [] - for o in o_map.values(): - st = getattr(o, 'order_status', 0) - if st in _TERMINAL: - continue - tv = getattr(o, 'traded_volume', 0) or 0 - ov = getattr(o, 'order_volume', 0) or 0 - o_rows.append([ - _fmt_time(getattr(o, 'order_time', 0)), - _plain(getattr(o, 'stock_code', '')), - getattr(o, 'instrument_name', '') or '', - _direction(getattr(o, 'order_type', 0)), - f"{getattr(o, 'price', 0):.3f}", - f"{tv}/{ov}", - f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-', - _ORDER_STATUS.get(st, '未知'), - ]) - self._tab_orders.label = f"当前委托 ({len(o_rows)})" if o_rows else "当前委托" - self._order_table.controls = [self._dt( - ["时间", "代码", "名称", "方向", "委托价", "已成交/委托量", "均价", "状态"], o_rows, - col_widths=[65, 55, 70, 35, 60, 80, 55, 50])] - - # 成交 — 按 traded_id 去重(保留最后一条) - t_map = {} - for t in self._trades: - tid = str(getattr(t, 'traded_id', '')) - if not tid: - continue - t_map[tid] = t - t_rows = [] - for t in t_map.values(): - t_rows.append([ - _fmt_time(getattr(t, 'traded_time', 0)), - _plain(getattr(t, 'stock_code', '')), - getattr(t, 'instrument_name', '') or '', - _direction(getattr(t, 'order_type', 0)), - f"{getattr(t, 'traded_price', 0):.3f}", - str(getattr(t, 'traded_volume', 0)), - f"{getattr(t, 'traded_amount', 0):.2f}", - f"{getattr(t, 'commission', 0):.2f}", - ]) - self._tab_trades.label = f"当日成交 ({len(t_rows)})" if t_rows else "当日成交" - self._trade_table.controls = [self._dt( - ["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], t_rows, - col_widths=[65, 55, 70, 35, 65, 60, 70, 55])] - - # 市场监控 - m_rows = [] - for sc, d in self.marketData.items(): - m_rows.append([d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"]) - self._market_table.controls = [self._dt(["时间", "股票名称", "最新价格"], m_rows)] - - def _refresh_loop(self): - """后台定时刷新 — 对齐 Tkinter: 5s 拉价 + 30s 委托/成交""" - while True: - time.sleep(5) - self._refresh_cycle += 1 - try: - self._pull_prices() - self._refresh_positions() - if self._refresh_cycle % 6 == 0: - self._refresh_orders() - self._refresh_trades() - self._rebuild_tables() - self.page.update() - except Exception: - pass - - # ══════════════════════════════════════════════════════════════ - # 工具栏按钮 - # ══════════════════════════════════════════════════════════════ - - def _on_start_trade(self, target): - PrintLog(LogLevel.INFO, f'[Flet-按钮] 启动按钮被点击: {target.stock_code}') - if target.enabled: - self._show_toast("该标的正运行中") - return - name = f'{target.stock_code} {target.stock_name}' - dlg = ft.AlertDialog( - title=ft.Text("确认启动"), - content=ft.Text(f"确定要启动交易吗?\n\n{name}"), - actions=[ - ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)), - ft.TextButton("确定", on_click=lambda e, t=target: self._do_start(t)), - ], - ) - self.page.show_dialog(dlg) - - def _do_start(self, target): - self.page.pop_dialog() - target.enabled = True - target.save() - from core.sfgrid.sfgrid_strategy import SFGridStrategy - self.strategy_ctrl[target.get_id()] = SFGridStrategy(target) - self._rebuild_tables() - self.page.update() - PrintLog(LogLevel.INFO, f'[Flet] 启动交易: {target.targetName()}') - - def _on_stop_trade(self, target): - PrintLog(LogLevel.INFO, f'[Flet-按钮] 暂停按钮被点击: {target.stock_code}') - if not target.enabled: - self._show_toast("该标的已暂停") - return - name = f'{target.stock_code} {target.stock_name}' - dlg = ft.AlertDialog( - title=ft.Text("确认暂停"), - content=ft.Text(f"确定要暂停交易吗?\n\n{name}"), - actions=[ - ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)), - ft.TextButton("确定", on_click=lambda e, t=target: self._do_stop(t)), - ], - ) - self.page.show_dialog(dlg) - - def _do_stop(self, target): - self.page.pop_dialog() - target.enabled = False - target.save() - ctrl = self.strategy_ctrl.pop(target.get_id(), None) - if ctrl: - ctrl.enabledTrading(False) - self._rebuild_tables() - self.page.update() - PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}') - - def _open_grid_config(self, target): - """网格配置对话框 — 对齐 Tkinter create_grid_config_window""" - PrintLog(LogLevel.INFO, f'[Flet-按钮] 设置按钮被点击: {target.stock_code}') - base = ft.TextField(label="基准价格", value=str(target.grid_start_price), width=120, text_size=13) - gsize = ft.TextField(label="网格大小", value=str(target.grid_size), width=120, text_size=13) - gvol = ft.TextField(label="网格交易量(手)", value=str(target.grid_volume), width=120, text_size=13) - gupper = ft.TextField(label="上方网格数", value=str(target.grid_upper_count), width=120, text_size=13) - glower = ft.TextField(label="下方网格数", value=str(target.grid_lower_count), width=120, text_size=13) - gidx = ft.TextField(label="当前网格层级", value=str(target.grid_index), width=120, text_size=13) - - col1 = ft.Column([base, gsize, gvol], spacing=8) - col2 = ft.Column([gupper, glower, gidx], spacing=8) - grid_preview = ft.Text("", size=11, italic=True) - - def _preview(e): - try: - bp = float(base.value) - gs = float(gsize.value) - up = int(gupper.value) - lo = int(glower.value) - prices = [] - for i in range(up, 0, -1): - prices.append(f"{bp + gs * i:.2f}(卖{up - i + 1})") - prices.append(f"→{bp:.2f}←(基准)") - for i in range(1, lo + 1): - p = bp - gs * i - if p > 0: - prices.append(f"{p:.2f}(买{i})") - grid_preview.value = " ".join(prices) - grid_preview.update() - except ValueError: - grid_preview.value = "请输入有效数字" - grid_preview.update() - - def _save(e): - try: - target.grid_start_price = float(base.value) - target.grid_size = float(gsize.value) - target.grid_volume = int(gvol.value) - target.grid_upper_count = int(gupper.value) - target.grid_lower_count = int(glower.value) - target.grid_index = int(gidx.value) - target.save() - self._close_dialog() - self._rebuild_tables() - self.page.update() - PrintLog(LogLevel.INFO, f'[Flet] 网格配置已保存: {target.targetName()}') - except ValueError: - self._show_toast("请输入有效的数值") - - dlg = ft.AlertDialog( - title=ft.Text(f"网格配置 - {target.stock_code} {target.stock_name}"), - content=ft.Column([ - ft.Row([col1, col2], spacing=20), - ft.ElevatedButton("预览网格序列", on_click=_preview), - grid_preview, - ], spacing=10, tight=True, height=320), - actions=[ - ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)), - ft.ElevatedButton("保存", on_click=_save), - ], - ) - self.page.show_dialog(dlg) - - def _show_toast(self, msg: str): - dlg = ft.AlertDialog(title=ft.Text("提示"), content=ft.Text(msg), - actions=[ft.TextButton("确定", on_click=lambda e: self._close_dialog(dlg))]) - self.page.show_dialog(dlg) - - def _close_dialog(self, dlg=None): - self.page.pop_dialog() - self.page.update() - - def _toggle_overlay(self): - self._drawer_open = not self._drawer_open - self._overlay.visible = self._drawer_open - self._sidebar_icon.set_active(self._drawer_open) - self.page.update() - - def _hide_overlay(self): - self._drawer_open = False - self._overlay.visible = False - self._sidebar_icon.set_active(False) - self.page.update() - - def _set_monitor_price(self, val: str): - try: - self.monitor_price = float(val) - self.marketData.clear() - self.listening_stock.clear() - self._rebuild_tables() - self.page.update() - except ValueError: - pass - - -# ══════════════════════════════════════════════════════════════════════ -# PanelIcon — 对齐 Tkinter 版 Canvas 手绘图标 -# ══════════════════════════════════════════════════════════════════════ - -class _PanelIcon(ft.Container): - """VSCode 风格面板切换图标 — 两个色块拼成的分栏图标""" - - _SIZE = 22 - _M = 3 - _COLORS = { - 'light': {'bg': '#f0f0f0', 'hover': '#d4d4d4', 'off': '#b0b0b0', 'on': '#808080', 'active': '#0078d4'}, - 'dark': {'bg': '#3c3c3c', 'hover': '#505050', 'off': '#6a6a6a', 'on': '#a0a0a0', 'active': '#ffffff'}, - } - - def __init__(self, kind: str, active: bool = True, on_click=None): - self._kind = kind - self._active = active - c = self._COLORS['light'] # 默认亮色,后续可扩展暗色检测 - self._bg = c['bg'] - self._hover_bg = c['hover'] - self._off = c['off'] - self._on = c['on'] - self._active_color = c['active'] - - rects = self._build_rects() - super().__init__( - content=rects, - width=self._SIZE, height=self._SIZE, - bgcolor=self._bg, border_radius=3, - ink=True, on_click=on_click, - padding=ft.Padding(self._M, self._M, self._M, self._M), - ) - - def _build_rects(self): - off, on, act = self._off, self._on, self._active_color - bar_w, bar_h = 6, self._SIZE - self._M * 2 - 2 - - if self._kind == 'sidebar': - c1 = on if self._active else off - c2 = act if self._active else off - return ft.Row([ - ft.Container(width=bar_w, height=bar_h, bgcolor=c1, border_radius=1), - ft.Container(width=2), # gap - ft.Container(width=bar_w, height=bar_h, bgcolor=c2, border_radius=1), - ], spacing=0) - else: - c1 = on if self._active else off - c2 = act if self._active else off - return ft.Column([ - ft.Container(width=bar_h, height=bar_w, bgcolor=c1, border_radius=1), - ft.Container(height=2), # gap - ft.Container(width=bar_h, height=bar_w, bgcolor=c2, border_radius=1), - ], spacing=0) - - def set_active(self, active: bool): - self._active = active - self.content = self._build_rects() - - -# ══════════════════════════════════════════════════════════════════════ -# 入口 -# ══════════════════════════════════════════════════════════════════════ - -def main(page: ft.Page): - QmtApp(page) - - -def run(): - ft.app(target=main) - -def run_web(): - ft.app(target=main, view=ft.AppView.WEB_BROWSER, port=8550) diff --git a/core/ui/flet/app_v2.py b/core/ui/flet/app_v2.py index f52372a..df18717 100644 --- a/core/ui/flet/app_v2.py +++ b/core/ui/flet/app_v2.py @@ -474,6 +474,7 @@ class _DrawerPanel: self._dataset_table = ft.ListView(expand=True) self._dataset_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True) self._scoring_table = ft.ListView(expand=True) + self._backtest_col = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True) bar = ft.TabBar(tabs=[ ft.Tab(label="实时价格监控"), @@ -666,10 +667,15 @@ class _DrawerPanel: def _build_dataset_tab(self) -> ft.Control: self._dataset_status = ft.Text("就绪", size=12, color='#888888') + self._backtest_label = ft.Text("v6.7r3 回测曲线", size=11, color='#aaaaaa') + self._backtest_img = ft.Container(visible=False) # 回测图表占位,运行时替换为 Image return ft.Column([ self._dataset_status, self._dataset_col, - ], expand=True, spacing=6) + ft.Divider(height=2, color='#444444'), + self._backtest_label, + self._backtest_img, + ], expand=True, spacing=6, scroll=ft.ScrollMode.AUTO) def _run_sync(self, targets: list): # 线程锁 @@ -712,6 +718,92 @@ class _DrawerPanel: threading.Thread(target=_do, daemon=True).start() + def _refresh_backtest(self): + """加载 v6.7r3 回测图表,嵌入数据集 Tab""" + import pandas as pd + import io, base64, json + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + import matplotlib.dates as mdates + from pathlib import Path + + model_dir = Path(__file__).parent.parent.parent.parent / 'models' + daily_csv = model_dir / 'backtest_v67r3_daily.csv' + summary_json = model_dir / 'backtest_v67r3_summary.json' + + if not daily_csv.exists(): + self._backtest_label.value = "回测数据不存在" + self._backtest_label.update() + return + + try: + df_daily = pd.read_csv(daily_csv, parse_dates=['date']) + summary = json.loads(summary_json.read_text()) if summary_json.exists() else {} + + # 更新标题行 + start_val = summary.get('initial_cash', 60000) + final_val = summary.get('final_total_value', 0) + self._backtest_label.value = (f"v6.7r3 回测 初始:{start_val:,.0f} → 终值:{final_val:,.0f} " + f"收益:{summary.get('total_return_pct',0):.1f}% " + f"夏普:{summary.get('annual_sharpe',0):.2f} " + f"回撤:{summary.get('max_drawdown_pct',0):.1f}%") + self._backtest_label.update() + + # 渲染总资产曲线 + import matplotlib.font_manager as mfont + # 查找中文字体 + cjk_names = ['Microsoft YaHei', 'SimHei', 'Noto Sans SC', 'WenQuanYi'] + cjk_font = next((f.fname for f in mfont.fontManager.ttflist + if any(n in f.name for n in cjk_names)), None) + if cjk_font: + prop = mfont.FontProperties(fname=cjk_font) + plt.rcParams['font.family'] = prop.get_name() + plt.rcParams['axes.unicode_minus'] = False + fig, ax = plt.subplots(figsize=(7, 3.5), dpi=100) + fig.patch.set_facecolor('#1e1e1e') + ax.set_facecolor('#2d2d2d') + ax.plot(df_daily['date'], df_daily['total_asset'], color='#4CAF50', linewidth=1.5) + ax.fill_between(df_daily['date'], df_daily['total_asset'], alpha=0.1, color='#4CAF50') + ax.set_title('总资产曲线 (v6.7r3)', color='#ffffff', fontsize=10) + ax.set_ylabel('元', color='#cccccc', fontsize=9) + ax.tick_params(colors='#cccccc', labelsize=8) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) + ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3)) + plt.setp(ax.xaxis.get_majorticklabels(), rotation=30, ha='right') + for spine in ax.spines.values(): + spine.set_color('#555555') + ax.grid(True, alpha=0.15, color='#888888') + # 标注起点终点 + ax.annotate(f'{start_val:,.0f}', xy=(df_daily['date'].iloc[0], start_val), + xytext=(3, 5), textcoords='offset points', color='#aaaaaa', fontsize=8) + ax.annotate(f'{final_val:,.0f}', xy=(df_daily['date'].iloc[-1], final_val), + xytext=(3, 5), textcoords='offset points', color='#4CAF50', fontsize=8, fontweight='bold') + plt.tight_layout() + buf = io.BytesIO() + fig.savefig(buf, format='png', bbox_inches='tight', facecolor=fig.get_facecolor()) + buf.seek(0) + img_b64 = base64.b64encode(buf.read()).decode() + plt.close(fig) + + # 用实际 Image 替换占位的 Container + new_img = ft.Image( + src=f'data:image/png;base64,{img_b64}', + fit=ft.BoxFit.CONTAIN, + height=240, + visible=True, + ) + # 找到 placeholder Container 在 Column 中的位置,替换 + idx = self._backtest_label.parent.controls.index(self._backtest_img) + self._backtest_label.parent.controls[idx] = new_img + self._backtest_img = new_img + self._backtest_label.parent.update() + + except Exception as ex: + self._backtest_label.value = f"图表加载失败: {ex}" + self._backtest_label.color = '#F44336' + self._backtest_label.update() + def _set_sync_btns_disabled(self, disabled: bool): for btn in self._sync_btns: btn.disabled = disabled @@ -776,6 +868,7 @@ class _DrawerPanel: rows.append(ft.Divider(height=1, color='#f0f0f0')) self._dataset_col.controls = rows + self._refresh_backtest() # ── Tab 6: 每日评分 ── def _build_scoring_tab(self) -> ft.Control: diff --git a/core/ui/tkinter/__init__.py b/core/ui/tkinter/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/ui/tkinter/main_window.py b/core/ui/tkinter/main_window.py deleted file mode 100644 index 2a09504..0000000 --- a/core/ui/tkinter/main_window.py +++ /dev/null @@ -1,142 +0,0 @@ -import tkinter as tk -from tkinter import ttk -from core.logger import LogLevel, LogData, PrintLog -from core.ui.tkinter.sfgrid_view import TradeTargetUI - -# 检测运行环境,决定使用真实或模拟 QMT -def get_qmt_module(): - try: - # 尝试导入真实 QMT,如果失败则使用模拟 - from core.qmt import qmtv - return qmtv - except ImportError: - from core.qmt_dummy import qmtv - return qmtv - -qmtv = get_qmt_module() - -from core.eventbus import EventPrintLog -from core.eventbus import event_bus as eBus - - -class MainWindow: - def __init__(self, configLogLevel:str, progress=None): - self.root = tk.Tk() - self.root.title("神之一手 - 交易系统") - self.root.geometry("1400x700") - - self.logLevel = LogLevel[configLogLevel] - PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}") - # 存储各个Frame的引用 - self.strategy_frames = {} - # 日志面板可见性标志 - self.log_visible = False - self.create_ui(progress) - - eBus.subscribe(EventPrintLog, self.on_log_event) - - - def create_ui(self, progress=None): - """创建UI界面""" - # 主容器 - main_container = ttk.Frame(self.root) - main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 中间主体区域 - content_area = ttk.Frame(main_container) - content_area.pack(fill=tk.BOTH, expand=True) - - # 右侧内容区域容器 - self.content_container = ttk.Frame(content_area) - self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # 创建策略Frame - strategy_names = ["网格"] - self.create_strategy_frames(strategy_names, progress) - - # 创建全局日志面板(默认隐藏) - self.create_global_log_panel(main_container) - - # 默认显示第一个策略 - self.show_strategy_frame(0) - - def create_global_log_panel(self, parent): - """创建全局日志面板""" - # 日志区域(默认隐藏) - self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10) - # 默认不显示,通过工具栏按钮控制 - - # 创建日志表格 - columns = ("timestamp", "level", "message") - - self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8) - - log_column_configs = { - "timestamp": ("时间", 100), - "level": ("级别", 50), - "message": ("消息", 1150) # 调整宽度适应全局布局 - } - - for col in columns: - title, width = log_column_configs[col] - self.log_table.heading(col, text=title) - self.log_table.column(col, width=width, anchor=tk.W) - - # 添加初始日志 - from datetime import datetime - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功")) - - # 滚动条 - scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview) - self.log_table.configure(yscrollcommand=scrollbar.set) - - self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - def on_log_event(self, event:LogData): - if self.logLevel.value <= event.level.value: - self.add_log(event.level, event.message) - - - def add_log(self, level:LogLevel, message): - """添加日志记录 - 全局方法""" - from datetime import datetime - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.log_table.insert('', 0, values=(timestamp, level.name, message)) - - def clear_logs(self): - """清空日志记录""" - # 删除所有日志项 - for item in self.log_table.get_children(): - self.log_table.delete(item) - - def create_strategy_frames(self, strategy_names, progress=None): - """创建各个策略的Frame""" - frame = TradeTargetUI(self.content_container, progress=progress) - 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) - - def toggle_log_panel(self): - """切换日志面板的显示/隐藏""" - if self.log_visible: - self.log_frame.pack_forget() - self.log_visible = False - else: - self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0)) - self.log_visible = True - - def on_exit(self): - """退出程序""" - from tkinter import messagebox - result = messagebox.askyesno("确认退出", "确定要退出系统吗?") - if result: - self.root.destroy() - - def run(self): - """运行程序""" - self.root.mainloop() \ No newline at end of file diff --git a/core/ui/tkinter/sfgrid_view.py b/core/ui/tkinter/sfgrid_view.py deleted file mode 100644 index abbc83f..0000000 --- a/core/ui/tkinter/sfgrid_view.py +++ /dev/null @@ -1,1850 +0,0 @@ -from typing import Any - -import tkinter as tk -from tkinter import ttk, messagebox -from datetime import datetime -import threading -import time -import core.eventbus as eBus -from core.logger import LogLevel, PrintLog -from core.sfgrid import bus_events -from core.sfgrid.model import SFGridTradeTarget -from core.qmt import qmtv -from core.sfgrid.sfgrid_strategy import SFGridStrategy - - -class TradeTargetUI(ttk.Frame): - def __init__(self, parent, progress=None): - import time as _time - _t0 = _time.time() - super().__init__(parent) - self.tradeTargetData:dict[int, SFGridTradeTarget] = {} # id->trade_target - self.stockCodeIdMap:dict[str, int] = {} - self.strategy_ctrl:dict[int, SFGridStrategy] = {} # stock_code->trade_target - self.targetMarketPrice: dict[int, float] = {} - self.targetAvgPrice: dict[int, float] = {} - self.listening_stock = [] - # 监控价格,默认值为10 - self.monitor_price = 10.0 - # 追踪最后点击的表格 (0=网格, 1=未分类) - self._active_table = 0 - - if progress: - progress("正在加载持仓数据...", 0.3) - self.init_trade_target_pool() - - # 市场监控数据 - self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}} - - # 面板显示状态 - self.market_monitor_visible = True - self.bottom_panel_visible = True - # 市场活跃状态 + 刷新控制 - self._market_active = qmtv.isMarketActive # type: ignore - self._refresh_event = threading.Event() - self._refresh_cycle = 0 # 后台刷新周期计数 - self._prices_pulled_after_close = False # 收盘后是否已拉取过 - - if progress: - progress("正在构建界面...", 0.6) - # 创建界面 - self.create_ui() - - if progress: - progress("正在初始化策略...", 0.85) - 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) - print(f'[计时] TradeTargetUI.__init__ 总计: {_time.time() - _t0:.2f}s') - - - def init_trade_target_pool(self): - import time as _time - _t = _time.time() - - # 一次性从 QMT 获取全部持仓 - all_positions = qmtv.getAllPositions() - PrintLog(LogLevel.INFO, f'- [持仓] 从 QMT 获取到 {len(all_positions)} 个持仓') - print(f'[计时] └─ getAllPositions: {_time.time() - _t:.2f}s') - - # 自动将 QMT 持仓导入到数据库(持仓但未在交易池中的标的) - _t2 = _time.time() - 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, - 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} 个标的到交易池') - print(f'[计时] └─ 持仓导入DB: {_time.time() - _t2:.2f}s ({imported_count} 个新增)') - - _t3 = _time.time() - results = SFGridTradeTarget.select() - for temp in results: - tradeTarget:SFGridTradeTarget = temp - 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 - else: - 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}') - print(f'[计时] └─ 策略初始化: {_time.time() - _t3:.2f}s ({len(results)} 个标的)') - print(f'[计时] └─ init_trade_target_pool 总计: {_time.time() - _t:.2f}s') - - PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的') - - - # 收集所有市场数据用于市场监控 - def onMarketDataUpdated(self, data): - for stock_code, tickData in data.items(): - # 统一去掉后缀用于内部查找 - plain_code = stock_code.split('.')[0] if '.' in stock_code else stock_code - if plain_code in self.stockCodeIdMap: - id:int = self.stockCodeIdMap[plain_code] - self.targetMarketPrice[id] = tickData['lastPrice'] - tradeTarget = self.tradeTargetData[id] - lastPrice = float("{:.3f}".format(tickData['lastPrice'])) - tradeTarget.market_price = lastPrice # type: ignore - self.updateTradeTarget(tradeTarget, False) # 市价更新 - else: - # 非目标交易,发布市场数据更新事件用于市场监控 - lastPrice = tickData['lastPrice'] - # 使用用户设置的监控价格替代硬编码的10 - if lastPrice == self.monitor_price or stock_code in self.listening_stock: - # 发布市场数据更新事件用于市场监控 - if stock_code not in self.listening_stock: - self.listening_stock.append(stock_code) - # 更新市场监控数据用于UI显示 - current_time = datetime.now().strftime("%H:%M:%S") - self.marketData[str(stock_code)] = { - 'stock_name': qmtv.getInstrumentName(stock_code), - 'last_price': tickData['lastPrice'], - 'time': current_time - } - - - # 来自策略的数据更新 - def onStrategyUpdate(self, target: SFGridTradeTarget): - id = target.get_id() - self.tradeTargetData[id] = target - # 唤醒 refresh_loop 立即刷新表格,统一由 refresh_table 处理 - self._refresh_event.set() - - - # priceChange 用于控制是否对更新价格数据,进行交易判断 - def updateTradeTarget(self, target: SFGridTradeTarget, save: bool = True): - if save: - target.save() - - id = target.get_id() - # 更新或添加数据到本地缓存 - self.tradeTargetData[id] = target - - # 注册到 stockCodeIdMap(所有标的都需要行情数据) - if id not in self.stockCodeIdMap.values() and target.stock_code not in self.stockCodeIdMap: - self.stockCodeIdMap[target.stock_code] = id # type: ignore - - # 只有网格策略标的才创建策略控制器 - 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: - self.targetAvgPrice[id] = pos.avg_price - - # UI CREATE - def create_ui(self): - """创建UI界面""" - import time as _time - _t = _time.time() - - # 主框架(使用self作为父容器) - main_frame = ttk.Frame(self) - main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - # 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页) - self.create_tables_area(main_frame) - print(f'[计时] └─ create_tables_area: {_time.time() - _t:.2f}s') - - # 启动刷新线程 - self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True) - self.refresh_thread.start() - print(f'[计时] └─ create_ui 总计: {_time.time() - _t:.2f}s') - - - 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: - 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) - - # 每 6 个周期 (30s) 自动刷新委托/成交 - self._refresh_cycle += 1 - if self._refresh_cycle % 6 == 0: - self.after(0, self._refresh_orders) - self.after(0, self._refresh_trades) - - 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) - - # ========== 左侧:工具栏 + 持仓表格 ========== - 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) - - # 右上角 VSCode 风格图标按钮 - sidebar_btn = PanelIcon(toolbar_frame, 'sidebar', self.btnHandlerToggleMarketMonitor, - active=self.market_monitor_visible) - sidebar_btn.pack(side=tk.RIGHT, padx=1, pady=2) - bottom_btn = PanelIcon(toolbar_frame, 'bottom', self._toggle_bottom_panel, - active=self.bottom_panel_visible) - bottom_btn.pack(side=tk.RIGHT, padx=1, pady=2) - self._sidebar_icon = sidebar_btn - self._bottom_icon = bottom_btn - - # 上半部分: 网格策略持仓 - 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) - - # 下半部分: 未分类持仓 - self.unclassified_frame = ttk.LabelFrame(left_frame, text="未分类持仓", padding=5) - self.unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0)) - self.create_unclassified_table(self.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: 当日委托 - self.order_tab = ttk.Frame(self.right_notebook) - self.right_notebook.add(self.order_tab, text="当日委托") - self._create_order_table(self.order_tab) - - # Tab 3: 当日成交 - self.trade_tab = ttk.Frame(self.right_notebook) - self.right_notebook.add(self.trade_tab, text="当日成交") - self._create_trade_table(self.trade_tab) - - # Tab 4: 数据集管理 - self.dataset_tab = ttk.Frame(self.right_notebook) - self.right_notebook.add(self.dataset_tab, text="数据集管理") - self._create_dataset_tab(self.dataset_tab) - - # Tab 5: 每日评分 - self.scoring_tab = ttk.Frame(self.right_notebook) - self.right_notebook.add(self.scoring_tab, text="每日评分") - self._create_scoring_tab(self.scoring_tab) - - # 评分数据缓存 - self._scoring_data: dict = {} # {stock_code: {score, rank, ...}} - - # Tab 切换时自动刷新 - self.right_notebook.bind("<>", self._on_tab_changed) - - - def create_grid_table(self, parent): - """创建网格策略表格""" - columns = ("ID", - "股票", "市场价", "当前持仓", - "平均成本", "当前网格基准价", "交易状态" - ) - - self.grid_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), - "当前网格基准价": (100, tk.E), - "交易状态": (100, tk.CENTER) - } - - for col in columns: - width, anchor = column_configs[col] - 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.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): - """创建市场监控表格""" - columns = ("时间", "股票名称", "最新价格") - - self.market_table = ttk.Treeview(parent, columns=columns, show='headings', height=15) - - # 列配置 - column_configs = { - "时间": (50, tk.CENTER), - "股票名称": (80, tk.CENTER), - "最新价格": (50, tk.CENTER) - } - - for col in columns: - width, anchor = column_configs[col] - self.market_table.heading(col, text=col) - self.market_table.column(col, width=width, anchor=anchor) # type: ignore - - # 滚动条 - scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.market_table.yview) - self.market_table.configure(yscrollcommand=scrollbar.set) - - self.market_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - # 绑定双击事件 - self.market_table.bind("", self.on_market_table_double_click) - - # 填充初始数据 - self.populate_market_table() - - # ---- 委托/成交 Tab 页 ---- - - _ORDER_STATUS_MAP = { - 48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', - 52: '部成待撤', 53: '部撤', 54: '已撤', 55: '部成', - 56: '已成', 57: '废单', - } - - @staticmethod - def _order_direction(ot: int) -> str: - return '买' if ot in (23,) else '卖' if ot in (24,) else str(ot) - - def _create_order_table(self, parent): - """创建当日委托表格""" - toolbar = ttk.Frame(parent) - toolbar.pack(fill=tk.X, pady=(0, 3)) - ttk.Button(toolbar, text="刷新", command=self._refresh_orders, width=6).pack(side=tk.RIGHT) - ttk.Label(toolbar, text="当日委托", font=('', 10, 'bold')).pack(side=tk.LEFT) - - cols = ("时间", "代码", "名称", "方向", "委托价", "委托量", "已成交", "均价", "状态") - self.order_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14) - widths = {"时间": 70, "代码": 60, "名称": 70, "方向": 30, "委托价": 55, "委托量": 55, "已成交": 55, "均价": 55, "状态": 55} - for c in cols: - self.order_tree.heading(c, text=c) - self.order_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER) - - sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.order_tree.yview) - self.order_tree.configure(yscrollcommand=sb.set) - self.order_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - sb.pack(side=tk.RIGHT, fill=tk.Y) - - def _create_trade_table(self, parent): - """创建当日成交表格""" - toolbar = ttk.Frame(parent) - toolbar.pack(fill=tk.X, pady=(0, 3)) - ttk.Button(toolbar, text="刷新", command=self._refresh_trades, width=6).pack(side=tk.RIGHT) - ttk.Label(toolbar, text="当日成交", font=('', 10, 'bold')).pack(side=tk.LEFT) - - cols = ("时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费") - self.trade_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14) - widths = {"时间": 70, "代码": 60, "名称": 70, "方向": 30, "成交价": 55, "成交量": 55, "成交金额": 65, "手续费": 50} - for c in cols: - self.trade_tree.heading(c, text=c) - self.trade_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER) - - sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_tree.yview) - self.trade_tree.configure(yscrollcommand=sb.set) - self.trade_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - sb.pack(side=tk.RIGHT, fill=tk.Y) - - def _on_tab_changed(self, event): - """Tab 切换时自动刷新""" - nb = self.right_notebook - try: - tab_id = nb.select() - tab_text = nb.tab(tab_id, "text") - if tab_text == "当日委托": - self._refresh_orders() - elif tab_text == "当日成交": - self._refresh_trades() - elif tab_text == "数据集管理": - self._refresh_dataset_status() - elif tab_text == "每日评分": - self._refresh_scoring_table() - except Exception: - pass - - # ================================================================ - # Tab 4: 数据集管理 - # ================================================================ - def _create_dataset_tab(self, parent): - """创建数据集管理 Tab""" - # 顶部按钮栏 - btn_frame = ttk.Frame(parent) - btn_frame.pack(fill=tk.X, pady=(0, 5)) - - syncs = [ - ("同步K线(个股+指数)", lambda: self._run_dataset_sync(['kline'])), - ("同步股票信息", lambda: self._run_dataset_sync(['stocks'])), - ("同步行业映射", lambda: self._run_dataset_sync(['industry'])), - ("计算市场状态", lambda: self._run_dataset_sync(['market'])), - ("计算行业指数", lambda: self._run_dataset_sync(['sector'])), - ("同步全部", lambda: self._run_dataset_sync(['all'])), - ] - for label, cmd in syncs: - ttk.Button(btn_frame, text=label, command=cmd, width=18).pack( - side=tk.LEFT, padx=2) - - # 数据表状态表格 - cols = ("表名", "记录数", "最新日期", "数据覆盖") - self.dataset_tree = ttk.Treeview(parent, columns=cols, show='headings', height=12) - widths = {"表名": 180, "记录数": 80, "最新日期": 100, "数据覆盖": 200} - for c in cols: - self.dataset_tree.heading(c, text=c) - self.dataset_tree.column(c, width=widths.get(c, 80), anchor=tk.W) - - sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.dataset_tree.yview) - self.dataset_tree.configure(yscrollcommand=sb.set) - self.dataset_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - sb.pack(side=tk.RIGHT, fill=tk.Y) - - # 状态栏 - self.dataset_status = ttk.Label(parent, text='就绪', foreground='gray') - self.dataset_status.pack(fill=tk.X, pady=(5, 0)) - - def _refresh_dataset_status(self): - """刷新数据集状态""" - from core.scoring.models import ( - KlineStock, KlineIndex, StockInfo, IndustryMapping, - MarketRegimeDaily, SectorFeaturesDaily, ScoringResult, - ) - # (显示名, 模型类, 日期字段名) - tables = [ - ('kline_stock', KlineStock, 'trade_date'), - ('kline_index', KlineIndex, 'trade_date'), - ('stocks', StockInfo, 'share_updated_at'), - ('industry', IndustryMapping, 'update_date'), - ('market_regime', MarketRegimeDaily, 'trade_date'), - ('sector_features', SectorFeaturesDaily, 'trade_date'), - ('ScoringResult', ScoringResult, 'trade_date'), - ] - - self.dataset_tree.delete(*self.dataset_tree.get_children()) - for name, model, date_col in tables: - try: - count = model.select().count() - df = getattr(model, date_col, None) - if df is not None: - last = (model.select(df) - .order_by(df.desc()) - .first()) - last_date = str(getattr(last, date_col, '')) if last else '' - else: - last_date = '' - self.dataset_tree.insert('', tk.END, values=(name, count, last_date, '')) - except Exception as e: - self.dataset_tree.insert('', tk.END, values=(name, 'ERR', str(e), '')) - - def _run_dataset_sync(self, targets: list): - """后台线程执行数据同步""" - import threading - self.dataset_status.config(text='同步中...', foreground='orange') - - def _do_sync(): - from core.scoring.sync import ( - KlineStockSync, KlineIndexSync, StocksSync, - IndustrySync, MarketRegimeSync, SectorFeaturesSync, - ) - from core.eventbus import event_bus - from core.sfgrid.bus_events import EventSyncProgress - - all_targets = { - 'kline': [('K线-个股', KlineStockSync(count=300)), - ('K线-指数', KlineIndexSync(count=300))], - 'stocks': [('股票信息', StocksSync())], - 'industry': [('行业映射', IndustrySync())], - 'market': [('市场状态', MarketRegimeSync())], - 'sector': [('行业指数', SectorFeaturesSync())], - } - - if 'all' in targets: - order = (all_targets['kline'] + all_targets['stocks'] + - all_targets['industry'] + all_targets['market'] + - all_targets['sector']) - else: - order = [] - for t in targets: - order.extend(all_targets.get(t, [])) - - for label, sync in order: - self.after(0, lambda l=label: self.dataset_status.config( - text=f'同步中: {l}...', foreground='orange')) - try: - stats = sync.run() - event_bus.publish(EventSyncProgress, - {'source': label, 'status': 'ok', 'stats': stats}) - except Exception as e: - event_bus.publish(EventSyncProgress, - {'source': label, 'status': 'error', 'stats': str(e)}) - - self.after(0, lambda: self.dataset_status.config( - text='同步完成', foreground='green')) - self.after(100, self._refresh_dataset_status) - - threading.Thread(target=_do_sync, daemon=True).start() - - # ================================================================ - # Tab 5: 每日评分 - # ================================================================ - def _create_scoring_tab(self, parent): - """创建每日评分 Tab""" - # 顶部控制栏 - ctrl = ttk.Frame(parent) - ctrl.pack(fill=tk.X, pady=(0, 5)) - - ttk.Label(ctrl, text="评分日期:").pack(side=tk.LEFT, padx=(0, 5)) - self.score_date_var = tk.StringVar(value='') - self.score_date_entry = ttk.Entry(ctrl, textvariable=self.score_date_var, width=10) - self.score_date_entry.pack(side=tk.LEFT, padx=2) - - ttk.Button(ctrl, text="执行评分", command=self._run_scoring, width=10).pack( - side=tk.LEFT, padx=5) - ttk.Button(ctrl, text="刷新", command=self._refresh_scoring_table, width=6).pack( - side=tk.LEFT, padx=2) - ttk.Button(ctrl, text="加入网格", - command=self._add_scored_to_grid, width=10).pack( - side=tk.LEFT, padx=5) - - self.scoring_status = ttk.Label(ctrl, text='', foreground='gray') - self.scoring_status.pack(side=tk.LEFT, padx=10) - - # 评分结果表格 - cols = ("排名", "代码", "名称", "预测利润", "预测轮数", "堆叠概率") - self.scoring_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14) - widths = {"排名": 50, "代码": 70, "名称": 80, "预测利润": 80, "预测轮数": 80, "堆叠概率": 80} - for c in cols: - self.scoring_tree.heading(c, text=c) - self.scoring_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER) - - sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.scoring_tree.yview) - self.scoring_tree.configure(yscrollcommand=sb.set) - self.scoring_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - sb.pack(side=tk.RIGHT, fill=tk.Y) - - # 双击加入网格 - self.scoring_tree.bind("", self._on_scoring_double_click) - - def _refresh_scoring_table(self): - """从 ScoringResult 表加载最新评分""" - from core.scoring.models import ScoringResult - from core.qmt import qmtv - - self.scoring_tree.delete(*self.scoring_tree.get_children()) - self._scoring_data.clear() - - try: - # 取最新评分日 - latest = (ScoringResult - .select(ScoringResult.trade_date) - .distinct() - .order_by(ScoringResult.trade_date.desc()) - .first()) - if not latest: - self.scoring_status.config(text='无评分数据', foreground='gray') - return - - trade_date = latest.trade_date - self.score_date_var.set(str(trade_date)) - - rows = (ScoringResult - .select() - .where(ScoringResult.trade_date == trade_date) - .order_by(ScoringResult.score_rank) - .limit(200) - .dicts()) - - for r in rows: - code = r['stock_code'] - # 尝试从 QMT 获取名称 - name = '' - try: - name = qmtv.getInstrumentName(code) - except Exception: - pass - - values = ( - r.get('score_rank', ''), - code, - name, - f'{r.get("predicted_profit", 0):.4f}', - f'{r.get("rank_predicted_rounds", 0) or 0:.2f}', - f'{r.get("stacking_probability", 0) or 0:.4f}', - ) - self.scoring_tree.insert('', tk.END, values=values) - self._scoring_data[code] = {'name': name, **r} - - self.scoring_status.config( - text=f'{trade_date} — {len(rows)} 只候选股', - foreground='green') - - except Exception as e: - self.scoring_status.config(text=f'加载失败: {e}', foreground='red') - - def _run_scoring(self): - """后台线程执行评分管道""" - import threading - from datetime import date, datetime - - date_str = self.score_date_var.get().strip() - if date_str: - try: - trade_date = datetime.strptime(date_str, '%Y-%m-%d').date() - except ValueError: - self.scoring_status.config(text='日期格式错误 (YYYY-MM-DD)', foreground='red') - return - else: - trade_date = date.today() - - self.scoring_status.config(text='评分中...', foreground='orange') - - def _do_score(): - from core.scoring.inference.scorer import GridSeekerPipeline - from core.eventbus import event_bus - from core.sfgrid.bus_events import EventScoringCompleted - - try: - engine = GridSeekerPipeline() - rankings = engine.run(trade_date) - if not rankings.empty: - engine.persist(rankings, trade_date) - event_bus.publish(EventScoringCompleted, - {'date': str(trade_date), 'count': len(rankings)}) - except FileNotFoundError as e: - self.after(0, lambda: self.scoring_status.config( - text=f'模型文件缺失: {e}', foreground='red')) - return - except Exception as e: - self.after(0, lambda: self.scoring_status.config( - text=f'评分失败: {e}', foreground='red')) - return - - self.after(100, self._refresh_scoring_table) - self.after(0, lambda: self.scoring_status.config( - text=f'{trade_date} — 评分完成, {len(rankings)} 只', foreground='green')) - - threading.Thread(target=_do_score, daemon=True).start() - - def _on_scoring_double_click(self, event): - """双击评分行 → 加入网格策略""" - self._add_scored_to_grid() - - def _add_scored_to_grid(self): - """将选中的评分股票加入网格策略""" - selected = self.scoring_tree.selection() - if not selected: - return - - for item in selected: - values = self.scoring_tree.item(item)['values'] - if not values: - continue - stock_code = str(values[1]) - stock_name = str(values[2]) if values[2] else stock_code - - # 检查是否已存在 - from core.sfgrid.model import SFGridTradeTarget - existing = SFGridTradeTarget.get_or_none( - SFGridTradeTarget.stock_code == stock_code) - if existing: - continue - - # 添加到未分类持仓 - from core.qmt import qmtv - pos = qmtv.getStockPosition(stock_code) - position = int(pos.volume) if pos else 0 - - target = SFGridTradeTarget.create( - stock_code=stock_code, - stock_name=stock_name, - current_position=position, - strategy_type=0, # 未分类 - enabled=False, - ) - self.tradeTargetData[target.id] = target - self.stockCodeIdMap[stock_code] = target.id - self.refresh_table() - - # ---- 原有方法继续 ---- - """从 QMT 读取当日委托并刷新表格""" - from core.qmt import qmtv - try: - orders = qmtv.queryTodayOrders() - except Exception: - orders = [] - - for item in self.order_tree.get_children(): - self.order_tree.delete(item) - - _TERMINAL = {54, 56, 57} - for o in orders: - if getattr(o, 'order_status', 0) in _TERMINAL: - continue - code = getattr(o, 'stock_code', '') - if '.' in code: - code = code.split('.')[0] - self.order_tree.insert('', tk.END, values=( - str(getattr(o, 'order_time', '')), - code, - getattr(o, 'instrument_name', '') or code, - self._order_direction(getattr(o, 'order_type', 0)), - f"{getattr(o, 'price', 0):.3f}", - getattr(o, 'order_volume', 0), - getattr(o, 'traded_volume', 0), - f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-', - self._ORDER_STATUS_MAP.get(getattr(o, 'order_status', 255), '未知'), - )) - - def _refresh_trades(self): - """从 QMT 读取当日成交并刷新表格""" - from core.qmt import qmtv - try: - trades = qmtv.queryTodayTrades() - except Exception: - trades = [] - - for item in self.trade_tree.get_children(): - self.trade_tree.delete(item) - - for t in trades: - code = getattr(t, 'stock_code', '') - if '.' in code: - code = code.split('.')[0] - self.trade_tree.insert('', tk.END, values=( - str(getattr(t, 'traded_time', '')), - code, - getattr(t, 'instrument_name', '') or code, - self._order_direction(getattr(t, 'order_type', 0)), - f"{getattr(t, 'traded_price', 0):.3f}", - getattr(t, 'traded_volume', 0), - f"{getattr(t, 'traded_amount', 0):.2f}", - f"{getattr(t, 'commission', 0):.2f}", - )) - - def populate_market_table(self): - """填充市场监控表格数据""" - # 保存当前选中的项 - selected_items = self.market_table.selection() - selected_values = [] - for item in selected_items: - values = self.market_table.item(item)['values'] - if values: - selected_values.append(values[1]) # 保存股票代码 - - # 清空现有数据 - for item in self.market_table.get_children(): - self.market_table.delete(item) - - # 填充市场数据 - tmp = self.marketData.copy() - for stock_code, data in tmp.items(): - # 处理时间格式,仅显示 hh:mm:ss - time_str = data['time'] - # 如果时间字符串包含空格,说明包含日期和时间,只取时间部分 - if ' ' in time_str: - time_str = time_str.split(' ')[1] - - # 确保时间格式为 hh:mm:ss,如果只有 hh:mm 则补充 :00 - if ':' in time_str: - time_components = time_str.split(':') - if len(time_components) == 2: - # 只有小时和分钟,补充秒 - time_str = f"{time_components[0]}:{time_components[1]}:00" - elif len(time_components) >= 3: - # 有小时、分钟和秒,只取前三个部分 - time_str = f"{time_components[0]}:{time_components[1]}:{time_components[2]}" - - values = [ - time_str, - data['stock_name']+f"-{stock_code}", - f"{data['last_price']:.3f}", - stock_code - ] - self.market_table.insert('', tk.END, values=values) - - # 恢复之前选中的项 - if selected_values: - for item in self.market_table.get_children(): - values = self.market_table.item(item)['values'] - if values and values[1] in selected_values: # 比较股票代码 - self.market_table.selection_add(item) - - def on_market_table_double_click(self, event): - """市场监控表格双击事件""" - selected = self.market_table.selection() - if selected: - item = selected[0] - values = self.market_table.item(item)['values'] - print(values) - stock_name = values[1] - last_price = values[2] - stock_code = values[3] - - # 检查是否已在交易池中 - is_in_trade_pool = any(target.stock_code == stock_code for target in self.tradeTargetData.values()) - - if is_in_trade_pool: - messagebox.showinfo("提示", f"{stock_code} ({stock_name}) 已在交易池中") - else: - result = messagebox.askyesno( - "添加交易标的", - f"确定要将以下股票添加到交易池吗?\n\n" - f"股票代码: {stock_code}\n" - f"股票名称: {stock_name}\n" - f"最新价格: {last_price}" - ) - - if result: - # 发布事件通知主控制器添加标的 - self.addTradeTarget(stock_code) - - def get_trade_enabled_indicator(self, target: SFGridTradeTarget) -> str: - """获取交易状态指示器""" - if target.strategy_type == 0: # 未分类 = 未配置 - return "请做交易设置" - else: - if target.enabled: - return "▶ 运行中" - else: - return "⏸ 已停止" - - # ---- 右键菜单 ---- - - 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): - """填充网格策略表格数据""" - # 先清空所有行再重建,防止重复 - self.grid_table.delete(*self.grid_table.get_children()) - 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_idx = target.grid_index - price_grid = target.getPriceGrid() - grid_info = '-' - 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, - 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.grid_table.insert('', tk.END, values=values) - - def populate_unclassified_table(self): - """填充未分类持仓表格数据""" - # 先清空所有行再重建,防止重复 - self.unclassified_table.delete(*self.unclassified_table.get_children()) - 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.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): - """获取选中的交易标的(根据最后点击的表格优先)""" - # 根据最后点击的表格决定检查顺序 - 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 get_selected_targets(self): - """获取所有选中的交易标的(支持多选)""" - targets = [] - for table in (self.grid_table, self.unclassified_table): - for item in table.selection(): - values = table.item(item)['values'] - if values: - target = self.tradeTargetData.get(int(values[0])) - if target: - targets.append(target) - return targets - - def refresh_table(self): - """刷新表格数据(纯UI操作,在主线程执行)""" - # 刷新网格策略表格 - selected_items = self.grid_table.selection() - selected_values = [] - for item in selected_items: - values = self.grid_table.item(item)['values'] - if values: - 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.grid_table.get_children(): - values = self.grid_table.item(item)['values'] - if values and values[0] in selected_values: - 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() - - def create_grid_view_window(self, target: SFGridTradeTarget): - """创建网格配置查看窗口(只读)""" - # 获取顶层窗口 - root = self.winfo_toplevel() - - # 创建顶层窗口 - view_window = tk.Toplevel(root) - view_window.title(f"网格配置查看 - {target.stock_code} ({target.stock_name})") - view_window.geometry("500x450") - view_window.resizable(False, False) - - # 设置窗口模态 - view_window.transient(root) - view_window.grab_set() - - # 居中显示 - root.update_idletasks() - x = root.winfo_x() + (root.winfo_width() // 2) - 250 - y = root.winfo_y() + (root.winfo_height() // 2) - 225 - view_window.geometry(f"500x450+{x}+{y}") - - # 创建主框架 - main_frame = ttk.Frame(view_window, padding=20) - main_frame.pack(fill=tk.BOTH, expand=True) - - # 显示股票信息 - info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10) - info_frame.pack(fill=tk.X, pady=(0, 10)) - - 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) - - # 建仓状态(可变更) - status_text = "已建仓" if target.grid_index > 0 else "未建仓" - status_color = "green" if target.grid_index > 0 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_idx = 0 if target.grid_index > 0 else 1 - setattr(target, 'grid_index', new_idx) - target.save() - new_text = "已建仓" if new_idx > 0 else "未建仓" - new_color = "green" if new_idx > 0 else "orange" - status_label.config(text=f"建仓状态: {new_text}", foreground=new_color) - toggle_btn.config(text="标记为未建仓" if new_idx > 0 else "标记为已建仓") - self.updateTradeTarget(target, False) - PrintLog(LogLevel.INFO, f"建仓状态变更: {target.stock_code} → {new_text}") - - toggle_btn = ttk.Button( - info_frame, - text="标记为未建仓" if target.grid_index > 0 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.pack(fill=tk.X, pady=(0, 10)) - - # 基准价格 - base_price_frame = ttk.Frame(config_frame) - base_price_frame.pack(fill=tk.X, pady=5) - ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT) - ttk.Label(base_price_frame, text=f"{target.grid_start_price:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5) - ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT) - - # 网格大小 - grid_size_frame = ttk.Frame(config_frame) - grid_size_frame.pack(fill=tk.X, pady=5) - ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT) - ttk.Label(grid_size_frame, text=f"{target.grid_size:.3f}", width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5) - ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT) - - # 网格交易量 - grid_volume_frame = ttk.Frame(config_frame) - grid_volume_frame.pack(fill=tk.X, pady=5) - ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT) - ttk.Label(grid_volume_frame, text=str(target.grid_volume), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5) - ttk.Label(grid_volume_frame, text="股", foreground='gray').pack(side=tk.LEFT) - - # 上方网格数量 - upper_count_frame = ttk.Frame(config_frame) - upper_count_frame.pack(fill=tk.X, pady=5) - ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT) - ttk.Label(upper_count_frame, text=str(target.grid_upper_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5) - ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT) - - # 下方网格数量 - lower_count_frame = ttk.Frame(config_frame) - lower_count_frame.pack(fill=tk.X, pady=5) - ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT) - ttk.Label(lower_count_frame, text=str(target.grid_lower_count), width=15, anchor=tk.W).pack(side=tk.LEFT, padx=5) - ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT) - - # 生成网格价格序列 - price_grid_frame = ttk.LabelFrame(main_frame, text="网格价格序列", padding=10) - price_grid_frame.pack(fill=tk.X, pady=(0, 10)) - - # 计算并显示网格价格序列 - price_list = target.getPriceGrid() - price_text = ", ".join([f"{price:.3f}" for price in price_list]) - - # 创建文本框显示网格价格序列 - text_frame = ttk.Frame(price_grid_frame) - text_frame.pack(fill=tk.BOTH, expand=True) - - text_widget = tk.Text(text_frame, height=4, wrap=tk.WORD) - text_widget.insert(tk.END, price_text) - text_widget.config(state=tk.DISABLED) # 只读 - - scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview) - text_widget.configure(yscrollcommand=scrollbar.set) - - text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - # 关闭按钮 - button_frame = ttk.Frame(main_frame) - button_frame.pack(fill=tk.X, pady=(10, 0)) - ttk.Button(button_frame, text="关闭", command=view_window.destroy).pack(side=tk.RIGHT, padx=5) - - def create_grid_config_window(self, target: SFGridTradeTarget): - """创建网格配置窗口(可编辑)""" - # 获取顶层窗口 - root = self.winfo_toplevel() - - # 创建顶层窗口 - config_window = tk.Toplevel(root) - config_window.title(f"网格配置 - {target.stock_code} ({target.stock_name})") - config_window.geometry("550x550") - config_window.resizable(False, False) - - # 设置窗口模态 - config_window.transient(root) - config_window.grab_set() - - # 居中显示 - root.update_idletasks() - x = root.winfo_x() + (root.winfo_width() // 2) - 275 - y = root.winfo_y() + (root.winfo_height() // 2) - 275 - config_window.geometry(f"550x550+{x}+{y}") - - # 创建主框架 - main_frame = ttk.Frame(config_window, padding=20) - main_frame.pack(fill=tk.BOTH, expand=True) - - # 显示股票信息 - info_frame = ttk.LabelFrame(main_frame, text="标的详情", padding=10) - info_frame.pack(fill=tk.X, pady=(0, 10)) - - 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=2, column=0, columnspan=2, sticky=tk.W, pady=2) - - # 创建网格配置框架 - config_frame = ttk.LabelFrame(main_frame, text="网格配置", padding=15) - config_frame.pack(fill=tk.X, pady=(0, 10)) - - # 创建输入框字典用于保存引用 - entries = {} - - # 基准价格 - base_price_frame = ttk.Frame(config_frame) - base_price_frame.pack(fill=tk.X, pady=5) - ttk.Label(base_price_frame, text="基准价格:", width=15).pack(side=tk.LEFT) - base_price_entry = ttk.Entry(base_price_frame, width=15) - base_price_entry.insert(0, str(target.grid_start_price)) - base_price_entry.pack(side=tk.LEFT, padx=5) - ttk.Label(base_price_frame, text="元", foreground='gray').pack(side=tk.LEFT) - entries['grid_start_price'] = base_price_entry - - # 网格大小 - grid_size_frame = ttk.Frame(config_frame) - grid_size_frame.pack(fill=tk.X, pady=5) - ttk.Label(grid_size_frame, text="网格大小:", width=15).pack(side=tk.LEFT) - grid_size_entry = ttk.Entry(grid_size_frame, width=15) - grid_size_entry.insert(0, str(target.grid_size)) - grid_size_entry.pack(side=tk.LEFT, padx=5) - ttk.Label(grid_size_frame, text="元", foreground='gray').pack(side=tk.LEFT) - entries['grid_size'] = grid_size_entry - - # 网格交易量 - grid_volume_frame = ttk.Frame(config_frame) - grid_volume_frame.pack(fill=tk.X, pady=5) - ttk.Label(grid_volume_frame, text="网格交易量:", width=15).pack(side=tk.LEFT) - grid_volume_entry = ttk.Entry(grid_volume_frame, width=15) - grid_volume_entry.insert(0, str(target.grid_volume)) - grid_volume_entry.pack(side=tk.LEFT, padx=5) - ttk.Label(grid_volume_frame, text="手", foreground='gray').pack(side=tk.LEFT) - entries['grid_volume'] = grid_volume_entry - - # 上方网格数量 - upper_count_frame = ttk.Frame(config_frame) - upper_count_frame.pack(fill=tk.X, pady=5) - ttk.Label(upper_count_frame, text="上方网格数量:", width=15).pack(side=tk.LEFT) - upper_count_entry = ttk.Entry(upper_count_frame, width=15) - upper_count_entry.insert(0, str(target.grid_upper_count)) - upper_count_entry.pack(side=tk.LEFT, padx=5) - ttk.Label(upper_count_frame, text="格", foreground='gray').pack(side=tk.LEFT) - entries['grid_upper_count'] = upper_count_entry - - # 下方网格数量 - lower_count_frame = ttk.Frame(config_frame) - lower_count_frame.pack(fill=tk.X, pady=5) - ttk.Label(lower_count_frame, text="下方网格数量:", width=15).pack(side=tk.LEFT) - lower_count_entry = ttk.Entry(lower_count_frame, width=15) - lower_count_entry.insert(0, str(target.grid_lower_count)) - lower_count_entry.pack(side=tk.LEFT, padx=5) - ttk.Label(lower_count_frame, text="格", foreground='gray').pack(side=tk.LEFT) - entries['grid_lower_count'] = lower_count_entry - - # 预览按钮和结果显示 - preview_frame = ttk.LabelFrame(main_frame, text="网格价格序列预览", padding=10) - preview_frame.pack(fill=tk.X, pady=(0, 10)) - - preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列") - - def calculate_grid_prices(): - """计算网格价格序列""" - try: - base_price = float(base_price_entry.get()) - grid_size = float(grid_size_entry.get()) - upper_count = int(upper_count_entry.get()) - lower_count = int(lower_count_entry.get()) - - prices = [] - - # 计算上方网格价格 - for i in range(upper_count, 0, -1): - price = base_price + grid_size * i - prices.append(round(price, 3)) - - # 添加基准价格 - prices.append(base_price) - - # 计算下方网格价格 - for i in range(1, lower_count + 1): - price = base_price - grid_size * i - # 确保价格不为负 - if price >= 0: - prices.append(round(price, 3)) - else: - break - - return prices - except ValueError: - return None - - def update_preview(): - """更新网格价格序列预览""" - prices = calculate_grid_prices() - if prices: - price_str = ", ".join([str(p) for p in prices]) - preview_result.set(f"网格价格序列: {price_str}") - else: - preview_result.set("参数错误,请检查输入!") - - # 绑定输入变化自动预览 - for entry_widget in entries.values(): - entry_widget.bind("", lambda e: update_preview()) - entry_widget.bind("", lambda e: update_preview()) - - # 预览按钮 - preview_button_frame = ttk.Frame(preview_frame) - preview_button_frame.pack(fill=tk.X, pady=5) - # ttk.Button(preview_button_frame, text="预览", command=update_preview).pack(side=tk.LEFT) - - # 预览结果显示 - preview_label = ttk.Label(preview_button_frame, textvariable=preview_result, foreground='blue') - preview_label.pack(side=tk.LEFT, padx=10) - - # 初始预览 - update_preview() - - # 按钮框架 - button_frame = ttk.Frame(main_frame) - button_frame.pack(fill=tk.X, pady=(10, 0)) - - def save_config(): - """保存配置""" - try: - # 获取输入值 - grid_start_price = float(base_price_entry.get()) - grid_size = float(grid_size_entry.get()) - grid_volume = int(grid_volume_entry.get()) - grid_upper_count = int(upper_count_entry.get()) - grid_lower_count = int(lower_count_entry.get()) - - # 更新target对象(使用setattr来正确设置Peewee字段的值) - setattr(target, 'grid_start_price', grid_start_price) - setattr(target, 'grid_size', grid_size) - setattr(target, 'grid_volume', grid_volume) - setattr(target, 'grid_upper_count', grid_upper_count) - setattr(target, 'grid_lower_count', grid_lower_count) - # 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("成功", "网格配置已保存!") - - except ValueError: - messagebox.showerror("错误", "输入参数有误,请检查!") - except Exception as e: - messagebox.showerror("错误", f"保存配置失败:{str(e)}") - PrintLog(LogLevel.ERROR, f"保存网格配置失败: {str(e)}") - - # 保存和取消按钮 - ttk.Button(button_frame, text="保存", command=save_config).pack(side=tk.RIGHT, padx=5) - ttk.Button(button_frame, text="取消", command=config_window.destroy).pack(side=tk.RIGHT, padx=5) - - def decrease_grid_index(self, grid_index_var: tk.IntVar, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label): - """减少网格序号""" - current_value = grid_index_var.get() - if current_value > 0: - grid_index_var.set(current_value - 1) - # 同步更新需求持仓量和持仓状态 - self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label) - - def increase_grid_index(self, grid_index_var: tk.IntVar, max_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label): - """增加网格序号""" - current_value = grid_index_var.get() - if current_value < max_index: - grid_index_var.set(current_value + 1) - # 同步更新需求持仓量和持仓状态 - self.update_required_position_and_status(grid_index_var.get(), target, required_position_label, position_status_label) - - def update_position_status(self, current_position: int, required_position: int, status_label: ttk.Label): - """更新持仓量状态提示""" - if current_position >= required_position: - status_label.config(text="持仓量充足", foreground="green") - else: - shortage = required_position - current_position - status_label.config(text=f"还需补充 {shortage} 手仓位", foreground="red") - - - def update_required_position_and_status(self, grid_index: int, target: SFGridTradeTarget, required_position_label: ttk.Label, position_status_label: ttk.Label): - """更新需求持仓量和持仓状态""" - # 计算需求持仓量 - required_position:int = grid_index * target.grid_volume # type: ignore - required_position_label.config(text=str(required_position)) - - # 更新持仓量状态 - current_position = getattr(target, 'current_position') - self.update_position_status(current_position, required_position, position_status_label) - - - # 交易池管理 - def addTradeTarget(self, stock_code: str, gridIndex: int = 1): # 新增 - """处理添加交易标的事件""" - try: - stock_name = qmtv.getInstrumentName(stock_code) - if not stock_name: - PrintLog(LogLevel.ERROR, f'无法获取股票代码 {stock_code} 的名称,请检查代码是否正确') - return - PrintLog(LogLevel.DEBUG, f'添加交易标的: {stock_code} {stock_name}') - - # 检查是否已存在该标的 - existing_target = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == stock_code) - if existing_target: - PrintLog(LogLevel.INFO, f'交易标的 {stock_code} {stock_name} 已存在') - return - - # 刷新标的持仓 - pos = qmtv.getStockPosition(stock_code) # type: ignore - new_target = SFGridTradeTarget.create( - stock_name=stock_name, - stock_code=stock_code, - current_position=0 if pos is None else int(pos.volume), - grid_index=gridIndex, - init_price=0.0, - strategy_type=0 # 默认为未分类 - ) - # 更新标的池 - self.updateTradeTarget(new_target, True) # 新增标的,相当于也是初始化 - - except Exception as e: - PrintLog(LogLevel.ERROR, f'新增交易标的失败 {stock_code} {e}') - - # button handlers ============================================================================================= - def btnHandlerGridCorrect(self): - - target = self.get_selected_target() - if not target: - return - self.create_grid_correction_window(target) - - - def btnHandlerToggleMarketMonitor(self): - """切换右侧面板显示/隐藏""" - if self.market_monitor_visible: - self.right_notebook.pack_forget() - self.market_monitor_visible = False - else: - self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) - self.market_monitor_visible = True - if hasattr(self, '_sidebar_icon'): - self._sidebar_icon.set_active(self.market_monitor_visible) - - def _toggle_bottom_panel(self): - """切换底部未分类持仓面板显示/隐藏""" - if self.bottom_panel_visible: - self.unclassified_frame.pack_forget() - self.bottom_panel_visible = False - else: - self.unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0)) - self.bottom_panel_visible = True - if hasattr(self, '_bottom_icon'): - self._bottom_icon.set_active(self.bottom_panel_visible) - - def btnHandlerTradeSettings(self): - """网格配置功能""" - target = self.get_selected_target() - if not target: - return - - # 只要暂停交易就可以修改参数,运行中则仅可查看 - if not target.enabled: # type: ignore - self.create_grid_config_window(target) - else: - # 创建只读的网格配置查看窗口 - self.create_grid_view_window(target) - - def btnHandlerStartSelectedTrade(self): - """启动选中的交易(支持多选)""" - from core.sfgrid.model import STRATEGY_TYPE_GRID - targets = self.get_selected_targets() - if not targets: - messagebox.showwarning("未选中", "请先选择交易标的") - return - - # 过滤:只处理已暂停的网格策略标的 - to_start = [t for t in targets - if t.strategy_type == STRATEGY_TYPE_GRID and not t.enabled] # type: ignore - if not to_start: - messagebox.showinfo("提示", "选中的标的中没有可启动的(已在运行中或非网格策略)") - return - - # 确认对话框 - names = "\n".join(f"{t.stock_code} {t.stock_name}" for t in to_start) - result = messagebox.askyesno("确认启动", f"确定要启动以下 {len(to_start)} 个交易标的吗?\n\n{names}") - if not result: - return - - for target in to_start: - PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}') - target.enabled = True # type: ignore - ctrl = self.strategy_ctrl.get(target.get_id()) - if ctrl: - ctrl.enabledTrading(True) - else: - PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {target.targetName()}") - - def btnHandlerStopSelectedTrade(self): - """暂停选中的交易(支持多选)""" - from core.sfgrid.model import STRATEGY_TYPE_GRID - targets = self.get_selected_targets() - if not targets: - messagebox.showwarning("未选中", "请先选择交易标的") - return - - # 过滤:只处理已启用的网格策略标的 - to_stop = [t for t in targets - if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled] # type: ignore - if not to_stop: - messagebox.showinfo("提示", "选中的标的中没有可暂停的(已是暂停状态或非网格策略)") - return - - # 确认对话框 - names = "\n".join(f"{t.stock_code} {t.stock_name}" for t in to_stop) - result = messagebox.askyesno("确认暂停", f"确定要暂停以下 {len(to_stop)} 个交易标的吗?\n\n{names}") - if not result: - return - - for target in to_stop: - PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}') - ctrl = self.strategy_ctrl.get(target.get_id()) - if ctrl: - ctrl.enabledTrading(False) - else: - print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n") - - def btnHandlerDelSelectedTradeTarget(self): - """删除选中的交易标的""" - target = self.get_selected_target() - if not target: - return - - result = messagebox.askyesno( - "确认删除", - f"确定要删除以下交易标的吗?\n\n" - f"股票代码: {target.stock_code}\n" - f"股票名称: {target.stock_name}\n\n" - f"⚠️ 此操作不可恢复!", - icon='warning' - ) - - if result: - id = target.get_id() - # try: - if id in self.strategy_ctrl: - ctrl = self.strategy_ctrl[id] - ctrl.deleteTradeTarget(target) - else: - self.onTradeTargetDeleted(target) - PrintLog(LogLevel.INFO, f"已发送删除请求: {target.stock_code} - {target.stock_name}") - - def onTradeTargetDeleted(self, target: SFGridTradeTarget): - id = target.get_id() - del self.tradeTargetData[id] - 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): - """添加新的交易标的""" - # 获取顶层窗口 - root = self.winfo_toplevel() - - # 创建顶层窗口 - add_window = tk.Toplevel(root) - add_window.title("添加交易标的") - add_window.geometry("400x150") - add_window.resizable(False, False) - - # 设置窗口模态 - add_window.transient(root) - add_window.grab_set() - - # 居中显示 - root.update_idletasks() - x = root.winfo_x() + (root.winfo_width() // 2) - 200 - y = root.winfo_y() + (root.winfo_height() // 2) - 75 - add_window.geometry(f"400x150+{x}+{y}") - - # 创建输入框架 - input_frame = ttk.Frame(add_window, padding=20) - input_frame.pack(fill=tk.BOTH, expand=True) - - # 股票代码输入 - ttk.Label(input_frame, text="股票代码:").grid(row=0, column=0, sticky=tk.W, pady=5) - stock_code_entry = ttk.Entry(input_frame, width=30) - stock_code_entry.grid(row=0, column=1, pady=5, padx=(10, 0)) - stock_code_entry.focus() - - # 按钮框架 - button_frame = ttk.Frame(input_frame) - button_frame.grid(row=1, column=0, columnspan=2, pady=20) - - def confirm_add(): - stock_code = stock_code_entry.get().strip() - if not stock_code: - messagebox.showwarning("输入错误", "请输入股票代码") - return - - # 发布事件通知主控制器添加标的 - self.addTradeTarget(stock_code) - add_window.destroy() - - def cancel_add(): - add_window.destroy() - - # 确认和取消按钮 - ttk.Button(button_frame, text="确认", command=confirm_add, width=10).pack(side=tk.LEFT, padx=5) - ttk.Button(button_frame, text="取消", command=cancel_add, width=10).pack(side=tk.LEFT, padx=5) - - # 绑定回车键确认 - stock_code_entry.bind('', lambda event: confirm_add()) - - PrintLog(LogLevel.INFO, "点击添加交易标的按钮") - - def btnHandlerSetMonitorPrice(self): - """设置监控价格""" - try: - # 获取输入的价格 - price_str = self.monitor_price_entry.get() - new_price = float(price_str) - - # 更新监控价格 - self.monitor_price = new_price - - # 清空当前监控的数据 - self.marketData.clear() - self.listening_stock.clear() - - # 清空市场监控表格 - for item in self.market_table.get_children(): - self.market_table.delete(item) - - PrintLog(LogLevel.INFO, f"监控价格已更新为: {new_price}") - except ValueError: - messagebox.showerror("错误", "请输入有效的数字") - - -class PanelIcon(tk.Canvas): - """VSCode 风格面板切换图标,颜色自适应系统主题""" - - _SIZE = 22 - - def __init__(self, parent, kind: str, command, active: bool = True): - super().__init__(parent, width=self._SIZE, height=self._SIZE, - bd=0, highlightthickness=0, cursor='hand2') - self._command = command - self._kind = kind - self._active = active - self.bind('', self._on_click) - self.bind('', self._on_enter) - self.bind('', self._on_leave) - self.bind('', lambda e: self._update_colors()) - self._update_colors() - - def _update_colors(self): - """从父容器读取实际背景色,推导图标配色""" - try: - bg = self.master.cget('background') or 'SystemButtonFace' - # 转 RGB 判断明暗 - rgb = self.winfo_rgb(bg) if bg.startswith('#') else None - if rgb: - r, g, b = rgb[0] >> 8, rgb[1] >> 8, rgb[2] >> 8 - bright = (r * 299 + g * 587 + b * 114) / 1000 - else: - bright = 240 # 系统颜色默认当作亮色 - except Exception: - bright = 240 - - if bright > 128: - # 亮色主题 - self._BG = '#e8e8e8' if bright < 220 else '#f0f0f0' - self._HOVER = '#d4d4d4' - self._OFF = '#b0b0b0' - self._ON = '#808080' - self._ACTIVE = '#0078d4' # Windows 蓝色 - else: - # 暗色主题 - self._BG = '#3c3c3c' - self._HOVER = '#505050' - self._OFF = '#6a6a6a' - self._ON = '#a0a0a0' - self._ACTIVE = '#ffffff' - - self.configure(bg=self._BG) - self._draw() - - def _draw(self): - self.delete('all') - m = 3 - s = self._SIZE - m * 2 - off = self._OFF - on = self._ON - - if self._kind == 'sidebar': - # 左右分栏图标,右边面板高亮 - l = self._ON if self._active else off - r = self._ACTIVE if self._active else off - self.create_rectangle(m + 1, m + 1, m + 6, m + s - 2, fill=l, outline='', width=0) - self.create_rectangle(m + 8, m + 1, m + 13, m + s - 2, fill=r, outline='', width=0) - else: - # 上下分栏图标,下面板高亮 - t = self._ON if self._active else off - b = self._ACTIVE if self._active else off - self.create_rectangle(m + 1, m + 1, m + s - 2, m + 6, fill=t, outline='', width=0) - self.create_rectangle(m + 1, m + 8, m + s - 2, m + 13, fill=b, outline='', width=0) - - def set_active(self, active: bool): - self._active = active - self._draw() - - def _on_click(self, event): - self._command() - - def _on_enter(self, event): - self.configure(bg=self._HOVER) - - def _on_leave(self, event): - self.configure(bg=self._BG) diff --git a/core/ui/tkinter/splash.py b/core/ui/tkinter/splash.py deleted file mode 100644 index 86f4c12..0000000 --- a/core/ui/tkinter/splash.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -启动进度窗口 — 无边框小窗口,负责整个初始化流程。 -""" -import time -import tkinter as tk -from tkinter import ttk, messagebox - - -class SplashWindow: - """初始化进度窗口,所有者启动逻辑""" - - def __init__(self): - self.root = tk.Tk() - self.root.title("神之一手") - self.root.geometry("380x120") - self.root.resizable(False, False) - self.root.overrideredirect(True) - - self.root.update_idletasks() - sw = self.root.winfo_screenwidth() - sh = self.root.winfo_screenheight() - w, h = 380, 120 - self.root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") - - frame = ttk.Frame(self.root, padding=20) - frame.pack(fill=tk.BOTH, expand=True) - - ttk.Label(frame, text="神之一手", font=('Microsoft YaHei', 14, 'bold')).pack(pady=(0, 5)) - self._status = ttk.Label(frame, text="正在初始化...", font=('Microsoft YaHei', 9)) - self._status.pack(pady=(0, 10)) - - self._bar = ttk.Progressbar(frame, mode='determinate', length=340) - self._bar.pack() - self.root.update() - - def progress(self, text: str, pct: float): - self._status.configure(text=text) - self._bar.configure(value=pct) - self.root.update() - - def _destroy(self): - self.root.destroy() - - def run(self): - """执行完整启动流程,成功返回主窗口,失败返回 None""" - from core.qmt_real import RealQmtV, qmtv as selected_qmtv - - while True: - _t_total = time.time() - - # 步骤1: 探测 QMT 环境 - self.progress("正在检查 QMT 环境...", 10) - _t = time.time() - try: - discovered = RealQmtV._discover_qmt_port() - except Exception: - discovered = 0 - print(f'[计时] 步骤1-探测QMT环境: {time.time() - _t:.2f}s') - - if not discovered: - self._destroy() - messagebox.showerror( - "启动失败", - "未能自动探测到 QMT 环境。\n\n" - "请确认:\n" - "1. 极简QMT(GJQMT)已启动并登录\n" - "2. XtMiniQmt.exe 和 miniquote.exe 进程在运行" - ) - return None - - # 步骤2: 初始化交易器 - self.progress("正在初始化交易器...", 35) - _t = time.time() - selected_qmtv.init_qmtv() - print(f'[计时] 步骤2-初始化交易器: {time.time() - _t:.2f}s') - - # 步骤3: 连接 QMT - self.progress("正在连接 QMT...", 55) - _t = time.time() - connected = selected_qmtv.connect() - print(f'[计时] 步骤3-连接QMT: {time.time() - _t:.2f}s') - - if not connected: - self._destroy() - option = messagebox.askokcancel( - "连接失败", - "QMT 连接失败。\n\n" - "请确认极简QMT 已启动并登录交易账号。\n" - "点击「确定」重试,或「取消」退出。" - ) - if not option: - return None - # 重试:重新创建进度窗口 - self.__init__() - continue - - # 步骤4: 加载主界面 - self.progress("正在加载持仓与策略...", 75) - _t = time.time() - from core.ui.tkinter.main_window import MainWindow - window = MainWindow('INFO', progress=lambda t, p: self.progress(t, 75 + p * 0.2)) - print(f'[计时] 步骤4-主界面加载: {time.time() - _t:.2f}s') - - window.root.update() - - # 步骤5: 完成 - self.progress("启动完成", 100) - self.root.update() - self.root.after(300, self._destroy) - print(f'[计时] 总启动耗时: {time.time() - _t_total:.2f}s') - - return window diff --git a/grid_strage.py b/grid_strage.py deleted file mode 100644 index 24a7572..0000000 --- a/grid_strage.py +++ /dev/null @@ -1,259 +0,0 @@ -from kuanke.wizard import * -from jqdata import * -import pandas as pd -import numpy as np - -# ==================== 初始化 ==================== -def initialize(context): - set_params(context) - # 开启防未来函数 - set_option('avoid_future_data', True) - # 用真实价格交易 - set_option('use_real_price', True) - # 过滤order中低于error级别的日志 - log.set_level('order', 'error') - log.set_level('system', 'error') - log.set_level('strategy', 'debug') - - set_benchmark('000001.XSHG') - set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0002, close_commission=0.0002, min_commission=5), type='stock') - set_slippage(FixedSlippage(0.01)) - - run_daily(before_trading, '9:30') - -# -------------------- 参数设置 -------------------- -def set_params(context): - context.max_price = 6 - context.min_price = 5.01 - context.grid_base_min = 1 # 最小价格 - context.grid_base_max = 5 # 建仓价格 - context.grid_interval = 0.5 # 下跌n元加仓 - context.profit_target = 0.5 # 上涨n元清仓 - - context.min_stocks = 10 - context.max_stocks = 25 - context.base_max_stocks = 25 - context.max_layers = 7 - - context.base_position_pct = 0.15 - context.max_position_pct = 0.15 - context.target_usage = 0.98 - context.reserve_ratio = 0.02 - - context.first_round_max = 10 - context.add_batch_size = 3 - context.add_cash_threshold = 0.4 - - g.stock_pool = [] - g.grid_info = {} - g.monitoring_stocks = set() - g.first_round_done = False - - - -# ==================== 盘前 ==================== -def before_trading(context): - january_clear(context) - if context.current_dt.month == 1: - return - stock_pool = get_stock_pool(context) - g.stock_pool = stock_pool - g.monitoring_stocks.update([s for s in stock_pool if s not in g.grid_info]) - g.first_round_done = len(g.grid_info) >= context.first_round_max - -# -------------------- 股票池 -------------------- -def get_stock_pool(context): - # 1. 全部 A 股(不含退市) - df_sec = get_all_securities(types=['stock'], date=context.previous_date) - codes = list(df_sec.index) - - # 2. 过滤 ST、科创板、北交所 - def is_valid(code): - name = df_sec.loc[code, 'display_name'] - if 'ST' in name or '退' in name or 'st' in name: - return False - if code.startswith('688'): # 科创板 - return False - if code.startswith('83') or code.startswith('87') or code.startswith('9'): # 北交所 - return False - return True - - codes = [c for c in codes if is_valid(c)] - - if not codes: - return [] - - # 3. 过滤停牌 & 价格区间 - try: - price_df = get_price(codes, - end_date=context.current_dt, - count=1, - fields=['pre_close'], - panel=False) - - if price_df is None or price_df.empty: - return [] - - # 过滤价格区间 - price_df = price_df[ - (price_df['pre_close'].notna()) & - (price_df['pre_close'] >= context.min_price) & - (price_df['pre_close'] <= context.max_price) - ] - - valid_codes = price_df['code'].tolist() - - except Exception as e: - log.error(f"获取价格数据失败: {e}") - return [] - - if not valid_codes: - return [] - - # 4. 过滤停牌(开盘价缺失) - try: - open_df = get_price(valid_codes, - end_date=context.current_dt, - count=1, - fields=['open'], - panel=False) - - if open_df is None or open_df.empty: - return [] - - # 过滤掉开盘价为空的股票 - open_df = open_df[open_df['open'].notna()] - final_codes = open_df['code'].tolist() - - except Exception as e: - log.error(f"获取开盘价数据失败: {e}") - return [] - - return final_codes - -# -------------------- 一月清仓 -------------------- -def january_clear(context): - if context.current_dt.month == 1: - log.info("进入1月,执行年度清仓...") - for stock in list(context.portfolio.positions.keys()): - order_target(stock, 0) - if stock in g.grid_info: - del g.grid_info[stock] - g.monitoring_stocks.add(stock) - -# ==================== 盘中 ==================== -def handle_data(context, data): - if context.current_dt.month == 1: - return - manage_positions(context, data) - usage = (context.portfolio.total_value - context.portfolio.available_cash) / context.portfolio.total_value - dynamic_max = get_dynamic_max_stocks(context) - if len(g.grid_info) < dynamic_max and usage < context.target_usage: - try_build_new(context, data) - -# -------------------- 动态上限 -------------------- -def get_dynamic_max_stocks(context): - return context.max_stocks if g.first_round_done else context.first_round_max - -# -------------------- 建仓 -------------------- -def try_build_new(context, data): - position_pct = context.max_position_pct - dynamic_max = get_dynamic_max_stocks(context) - count = 0 - for stock in list(g.monitoring_stocks): - if len(g.grid_info) >= dynamic_max or count >= 3: - break - - price = data[stock].close - if context.grid_base_min <= price <= context.grid_base_max: - total_value = context.portfolio.total_value - stock_amount = total_value * position_pct - grid = GridInfo(price, stock_amount, context.max_layers, context.grid_interval, context.profit_target) - layer_amount = grid.get_layer_amount(0) - buy_amount = int(layer_amount / price / 100) * 100 - if buy_amount > 0: - order(stock, buy_amount) - grid.add_position(price, buy_amount, 0) - g.grid_info[stock] = grid - g.monitoring_stocks.discard(stock) - count += 1 - log.info(f"[建仓] {stock} 价格{price:.2f} 数量{buy_amount}") - -# -------------------- 管理持仓 -------------------- -def manage_positions(context, data): - for stock, grid in list(g.grid_info.items()): - - price = data[stock].close - - # 止盈 - sellable = grid.get_sellable_positions(price) - if sellable: - for idx, pos in reversed(sellable): - order(stock, -pos['amount']) - grid.remove_position(idx) - profit = (price - pos['price']) * pos['amount'] - log.info(f"[止盈] {stock} 盈利{profit:.2f}") - - # 加仓 - layer = grid.should_add_layer(price) - if layer is not None: - layer_amount = grid.get_layer_amount(layer) - buy_amount = int(layer_amount / price / 100) * 100 - if buy_amount > 0: - order(stock, buy_amount) - grid.add_position(price, buy_amount, layer) - log.info(f"[加仓] {stock} 层级{layer} 数量{buy_amount}") - else: - log.info(f"[加仓失败] {stock} 层级{layer} 金额不足") - - # 清仓 - if len(grid.positions) == 0: - del g.grid_info[stock] - g.monitoring_stocks.add(stock) - log.info(f"[清仓] {stock}") - -# ==================== 盘后 ==================== -def after_trading_end(context): - log.info(f"持仓数:{len(g.grid_info)},监控数:{len(g.monitoring_stocks)}") - -# ==================== 网格类 ==================== -class GridInfo: - def __init__(self, base_price, total_amount, max_layers, interval, profit_target): - self.base_price = float(base_price) - self.total_amount = float(total_amount) - self.max_layers = int(max_layers) - self.interval = float(interval) - self.profit_target = float(profit_target) - self.layer_prices = {i: base_price - i * interval for i in range(self.max_layers)} - self.layer_weights = self._calc_weights() - self.positions = [] - - def _calc_weights(self): - weights = {i: 1.0 + 0.05 * i for i in range(self.max_layers)} - total = sum(list(weights.values())) - return {k: v / total for k, v in weights.items()} - - def get_layer_amount(self, layer): - return self.total_amount * self.layer_weights[layer] - - def add_position(self, price, amount, layer): - self.positions.append({'price': price, 'amount': amount, 'layer': layer}) - - def get_sellable_positions(self, current_price): - return [(i, p) for i, p in enumerate(self.positions) if current_price >= p['price'] + self.profit_target] - - def remove_position(self, index): - return self.positions.pop(index) - - def should_add_layer(self, current_price): - for layer in range(self.max_layers): - target = self.layer_prices[layer] - diff = abs(current_price - target) - - # 获取该层级的所有持仓 - layer_positions = [p for p in self.positions if p['layer'] == layer] - has_position = len(layer_positions) > 0 - - if diff <= 0.1 and not has_position: - return layer - return None \ No newline at end of file diff --git a/starter.py b/starter.py index 86cfc38..9f0ea54 100644 --- a/starter.py +++ b/starter.py @@ -1,7 +1,6 @@ # coding:utf-8 """ -启动入口 — 默认使用 Flet2 UI。 -使用 --tk 参数切换到 Tkinter UI。 +启动入口 — Flet UI """ import sys import os @@ -10,7 +9,6 @@ import ssl import traceback # 修复 Windows 上 flet_desktop 子进程弹出控制台窗口的问题 -# 原始 Popen 不带 CREATE_NO_WINDOW 标志,会为每个子进程创建控制台窗口 _original_popen = subprocess.Popen class Popen(_original_popen): @@ -22,14 +20,11 @@ class Popen(_original_popen): subprocess.Popen = Popen # PyInstaller 打包后,设置 FLET_VIEW_PATH 指向打包内的 Flet 客户端 -# 避免从 GitHub 下载 if getattr(sys, 'frozen', False): - # 运行在打包后的 exe 中 - base_path = sys._MEIPASS # PyInstaller 解压到的临时目录 + base_path = sys._MEIPASS flet_client_path = os.path.join(base_path, '.flet', 'client', 'flet-desktop-full-0.85.3') flet_exe = os.path.join(flet_client_path, 'flet', 'flet.exe') - # 写入日志便于调试 log_file = os.path.join(os.path.dirname(sys.executable), 'startup_log.txt') with open(log_file, 'w') as f: f.write(f'base_path: {base_path}\n') @@ -41,12 +36,10 @@ if getattr(sys, 'frozen', False): os.environ['FLET_VIEW_PATH'] = flet_client_path f.write(f'FLET_VIEW_PATH: {os.environ.get("FLET_VIEW_PATH")}\n') - # 禁用 SSL 验证,避免证书问题 if hasattr(ssl, '_create_unverified_context'): ssl._create_default_https_context = ssl._create_unverified_context def excepthook(type, value, tb): - """捕获未处理的异常,写入日志""" log_file = os.path.join(os.path.dirname(sys.executable), 'error_log.txt') with open(log_file, 'w') as f: f.write(''.join(traceback.format_exception(type, value, tb))) @@ -55,15 +48,5 @@ def excepthook(type, value, tb): sys.excepthook = excepthook if __name__ == '__main__': - if '--tk' in sys.argv: - from core.ui.tkinter.splash import SplashWindow - from tkinter import messagebox - try: - window = SplashWindow().run() - if window: - window.run() - except Exception as e: - messagebox.showerror("错误", f"系统初始化失败: {str(e)}") - else: - from core.ui.flet.app_v2 import run - run() + from core.ui.flet.app_v2 import run + run()