""" 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 = [] _CANCELED = {54, 57} for o in self._orders: if _plain(getattr(o, 'stock_code', '')) != stock_code: continue if getattr(o, 'order_status', 0) in _CANCELED: 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 去重(保留最后一条即最新状态) _CANCELED = {54, 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 _CANCELED: 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)