flet v2
This commit is contained in:
+5
-5
@@ -327,11 +327,11 @@ class QmtApp:
|
||||
def _pending_tags(self, stock_code: str) -> list:
|
||||
"""返回该标的下挂单的方向标签列表:'多'(买单) / '空'(卖单)"""
|
||||
tags = []
|
||||
_CANCELED = {54, 57}
|
||||
_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 _CANCELED:
|
||||
if getattr(o, 'order_status', 0) in _TERMINAL:
|
||||
continue
|
||||
ot = getattr(o, 'order_type', 0)
|
||||
if ot == 23 and '多' not in tags:
|
||||
@@ -533,8 +533,8 @@ class QmtApp:
|
||||
self._sel_actions.controls = []
|
||||
self._uncl_list.controls = [self._build_unclassified_table()]
|
||||
|
||||
# 委托 — 过滤已撤/废单,按 order_id 去重(保留最后一条即最新状态)
|
||||
_CANCELED = {54, 57}
|
||||
# 委托 — 过滤已终结订单(已撤/已成/废单),按 order_id 去重
|
||||
_TERMINAL = {54, 56, 57}
|
||||
o_map = {} # order_id → latest order
|
||||
for o in self._orders:
|
||||
oid = str(getattr(o, 'order_id', ''))
|
||||
@@ -544,7 +544,7 @@ class QmtApp:
|
||||
o_rows = []
|
||||
for o in o_map.values():
|
||||
st = getattr(o, 'order_status', 0)
|
||||
if st in _CANCELED:
|
||||
if st in _TERMINAL:
|
||||
continue
|
||||
tv = getattr(o, 'traded_volume', 0) or 0
|
||||
ov = getattr(o, 'order_volume', 0) or 0
|
||||
|
||||
+211
-37
@@ -133,6 +133,31 @@ class _DataStore:
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled:
|
||||
self.strategyCtrl[tid] = SFGridStrategy(t)
|
||||
|
||||
def remove_target(self, tid: int) -> str:
|
||||
"""移除/降级网格策略标的。返回 'unclassified'(有持仓降级)或 'deleted'(无持仓删除)"""
|
||||
target = self.tradeTargets.get(tid)
|
||||
if target is None:
|
||||
return 'none'
|
||||
# 先停止策略
|
||||
ctrl = self.strategyCtrl.pop(tid, None)
|
||||
if ctrl:
|
||||
ctrl.enabledTrading(False)
|
||||
if target.current_position > 0:
|
||||
# 有持仓 → 降级为未分类
|
||||
target.strategy_type = 0 # STRATEGY_TYPE_UNCLASSIFIED
|
||||
target.enabled = False
|
||||
target.save()
|
||||
return 'unclassified'
|
||||
else:
|
||||
# 无持仓 → 直接删除
|
||||
target.delete_instance()
|
||||
self.tradeTargets.pop(tid, None)
|
||||
self.stockCodeIdMap.pop(target.stock_code, None)
|
||||
self.marketPrices.pop(tid, None)
|
||||
self.avgPrices.pop(tid, None)
|
||||
self.preClose.pop(tid, None)
|
||||
return 'deleted'
|
||||
|
||||
# ── 行情 ──
|
||||
|
||||
def apply_tick(self, data: dict) -> int:
|
||||
@@ -189,7 +214,8 @@ class _DataStore:
|
||||
|
||||
def active_orders(self) -> list:
|
||||
"""返回过滤+去重后的活跃委托行"""
|
||||
_CANCELED = {54, 57}
|
||||
# 过滤已终结的订单: 已撤(54) 已成(56) 废单(57)
|
||||
_TERMINAL = {54, 56, 57}
|
||||
o_map = {}
|
||||
for o in self.orders:
|
||||
oid = str(getattr(o, 'order_id', ''))
|
||||
@@ -198,7 +224,7 @@ class _DataStore:
|
||||
rows = []
|
||||
for o in o_map.values():
|
||||
st = getattr(o, 'order_status', 0)
|
||||
if st in _CANCELED: continue
|
||||
if st in _TERMINAL: continue
|
||||
tv = getattr(o, 'traded_volume', 0) or 0
|
||||
ov = getattr(o, 'order_volume', 0) or 0
|
||||
rows.append([
|
||||
@@ -234,6 +260,36 @@ class _DataStore:
|
||||
])
|
||||
return rows
|
||||
|
||||
def add_from_market(self, stock_code: str, stock_name: str) -> int:
|
||||
"""从市场监控添加标的(默认: grid_index=0, upper=0, lower=10),返回 tid"""
|
||||
plain = _plain(stock_code)
|
||||
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == plain)
|
||||
if existing:
|
||||
tid = existing.get_id()
|
||||
if tid not in self.tradeTargets:
|
||||
self.tradeTargets[tid] = existing
|
||||
self.stockCodeIdMap[plain] = tid
|
||||
return tid
|
||||
mp = self.marketLog.get(stock_code, {})
|
||||
price = mp.get('last_price', 10.0)
|
||||
target = SFGridTradeTarget.create(
|
||||
stock_code=plain, stock_name=stock_name,
|
||||
current_position=0, init_price=0.0,
|
||||
grid_index=0, enabled=False,
|
||||
strategy_type=STRATEGY_TYPE_GRID,
|
||||
grid_start_price=float(price) if price > 0 else 10.0,
|
||||
grid_size=1.0, grid_volume=200,
|
||||
grid_upper_count=0, grid_lower_count=10,
|
||||
)
|
||||
tid = target.get_id()
|
||||
self.tradeTargets[tid] = target
|
||||
self.stockCodeIdMap[plain] = tid
|
||||
# 从市场监控列表中移除(加入交易池后不再监控)
|
||||
self.marketLog.pop(stock_code, None)
|
||||
if stock_code in self.listeningStocks:
|
||||
self.listeningStocks.remove(stock_code)
|
||||
return tid
|
||||
|
||||
def pending_tags(self, stock_code: str) -> list:
|
||||
"""某标的挂单方向标签"""
|
||||
tags = []
|
||||
@@ -268,8 +324,8 @@ class _GridPanel:
|
||||
return self._col
|
||||
|
||||
def _rebuild(self):
|
||||
# 列: (宽度/expand, ...) expand为比重,无则固定宽
|
||||
_C = [(40, 0), (0, 2), (80, 2), (0, 2), (55, 1), (65, 1), (75, 1), (100, 0)]
|
||||
# (width, expand): 0=固定宽, >0=弹性比重
|
||||
_C = [(35, 0), (0, 2), (70, 1), (0, 2), (50, 1), (55, 1), (60, 1), (0, 1)]
|
||||
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "状态", "操作"]
|
||||
header = []
|
||||
for h, (w, e) in zip(H, _C):
|
||||
@@ -302,20 +358,28 @@ class _GridPanel:
|
||||
))
|
||||
gcell = ft.Row(gcells, spacing=3) if len(gcells) > 1 else gcells[0]
|
||||
|
||||
# 操作按钮 — IconButton 图标居中,无点击偏移
|
||||
# 操作按钮
|
||||
_ICON = 22 # 图标大小,比默认 18 好按
|
||||
if t.enabled:
|
||||
btns = [
|
||||
ft.IconButton(ft.Icons.PAUSE, icon_size=18, tooltip="暂停",
|
||||
ft.IconButton(ft.Icons.PAUSE, icon_size=_ICON, tooltip="暂停",
|
||||
on_click=lambda e, tt=t: self._dialogs.confirm_stop(tt)),
|
||||
ft.IconButton(ft.Icons.SETTINGS, icon_size=_ICON, tooltip="设置",
|
||||
icon_color='#BDBDBD',
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
|
||||
ft.IconButton(ft.Icons.REMOVE, icon_size=_ICON, tooltip="运行中不可移除",
|
||||
icon_color='#BDBDBD',
|
||||
on_click=None),
|
||||
]
|
||||
else:
|
||||
btns = [
|
||||
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=18, tooltip="启动",
|
||||
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=_ICON, tooltip="启动",
|
||||
on_click=lambda e, tt=t: self._dialogs.confirm_start(tt)),
|
||||
ft.IconButton(ft.Icons.SETTINGS, icon_size=_ICON, tooltip="设置",
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
|
||||
ft.IconButton(ft.Icons.REMOVE, icon_size=_ICON, tooltip="移除",
|
||||
on_click=lambda e, tt=t: self._dialogs.confirm_remove(tt)),
|
||||
]
|
||||
btns.append(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="设置",
|
||||
icon_color='#BDBDBD' if t.enabled else None,
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)))
|
||||
|
||||
cells_text = [str(tid), f'{t.stock_code} {t.stock_name}',
|
||||
f'{mp:.3f}', gcell,
|
||||
@@ -331,11 +395,11 @@ class _GridPanel:
|
||||
elif isinstance(content, str):
|
||||
content = _text(content)
|
||||
elif isinstance(content, list):
|
||||
content = ft.Row(content, spacing=2)
|
||||
content = ft.Row(content, spacing=6)
|
||||
if e > 0:
|
||||
row_cells.append(ft.Container(content, padding=4, expand=e))
|
||||
else:
|
||||
row_cells.append(ft.Container(content, width=w, padding=4 if i != 7 else 2))
|
||||
row_cells.append(ft.Container(content, width=w, padding=4))
|
||||
row = ft.Row(row_cells, spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
rows.append(ft.Divider(height=1, color='#f0f0f0'))
|
||||
@@ -382,7 +446,6 @@ class _DrawerPanel:
|
||||
|
||||
panel = ft.Container(
|
||||
ft.Column([
|
||||
ft.Container(_text("监控面板", bold=True, size=14), padding=ft.Padding(10, 10, 10, 5)),
|
||||
ft.Tabs(ft.Column([bar, view], expand=True), length=4, expand=True),
|
||||
], expand=True),
|
||||
width=700, bgcolor=ft.Colors.SURFACE,
|
||||
@@ -416,26 +479,32 @@ class _DrawerPanel:
|
||||
self._refresh_market()
|
||||
|
||||
def _refresh_grid(self):
|
||||
W = [35, 180, 80, 60, 70, 65] # ID, 股票, 市场价, 持仓, 成本, 操作
|
||||
# (width, expand): 0=固定宽, >0=弹性比重; 股票列 expand 自动填充剩余空间
|
||||
_C = [(35, 0), (0, 1), (80, 0), (60, 0), (70, 0), (65, 0)]
|
||||
H = ["ID", "股票", "市场价", "持仓", "成本", "操作"]
|
||||
rows = [
|
||||
ft.Row([ft.Container(_text(h, bold=True), width=w, padding=4)
|
||||
for h, w in zip(H, W)], spacing=0),
|
||||
ft.Divider(height=1, color='#e0e0e0'),
|
||||
]
|
||||
|
||||
def _cell(text, w, e, color=None):
|
||||
if e > 0:
|
||||
return ft.Container(_text(text, color=color), padding=4, expand=e)
|
||||
return ft.Container(_text(text, color=color), width=w, padding=4)
|
||||
|
||||
header = ft.Row([_cell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
|
||||
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
|
||||
|
||||
for tid, t in self._data.tradeTargets.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID: continue
|
||||
mp = self._data.marketPrices.get(tid, 0) or 0
|
||||
row = ft.Row([
|
||||
ft.Container(_text(str(tid)), width=W[0], padding=4),
|
||||
ft.Container(_text(f'{t.stock_code} {t.stock_name}'), width=W[1], padding=4),
|
||||
ft.Container(_text(f'{mp:.3f}'), width=W[2], padding=4),
|
||||
ft.Container(_text(str(t.current_position)), width=W[3], padding=4),
|
||||
ft.Container(_text(f'{self._data.avgPrices.get(tid, 0):.3f}'), width=W[4], padding=4),
|
||||
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="网格配置",
|
||||
cells = [
|
||||
_cell(str(tid), *_C[0]),
|
||||
_cell(f'{t.stock_code} {t.stock_name}', *_C[1]),
|
||||
_cell(f'{mp:.3f}', *_C[2]),
|
||||
_cell(str(t.current_position), *_C[3]),
|
||||
_cell(f'{self._data.avgPrices.get(tid, 0):.3f}', *_C[4]),
|
||||
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=20, tooltip="网格配置",
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
|
||||
width=W[5], padding=0),
|
||||
], spacing=0)
|
||||
width=_C[5][0], padding=0),
|
||||
]
|
||||
row = ft.Row(cells, spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
rows.append(ft.Divider(height=1, color='#f0f0f0'))
|
||||
self._uncl_col.controls = rows
|
||||
@@ -455,10 +524,57 @@ class _DrawerPanel:
|
||||
[_datatable(cols, rows, [65,55,70,35,65,60,70,55])], expand=True)]
|
||||
|
||||
def _refresh_market(self):
|
||||
rows = [[d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"]
|
||||
for sc, d in self._data.marketLog.items()]
|
||||
self._market_table.controls = [ft.ListView(
|
||||
[_datatable(["时间", "股票名称", "最新价格"], rows)], expand=True)]
|
||||
# (width, expand): 0=固定宽, >0=弹性比重; 股票名称列 expand 自动填充剩余空间
|
||||
_C = [(80, 0), (0, 1), (80, 0), (45, 0)]
|
||||
H = ["时间", "股票名称", "最新价格", "操作"]
|
||||
|
||||
def _header_cell(text, w, e):
|
||||
if e > 0:
|
||||
return ft.Container(_text(text, bold=True), padding=4, expand=e)
|
||||
return ft.Container(_text(text, bold=True), width=w, padding=4)
|
||||
|
||||
header = ft.Row([_header_cell(h, w, e) for h, (w, e) in zip(H, _C)], spacing=0)
|
||||
rows = [header, ft.Divider(height=1, color='#e0e0e0')]
|
||||
|
||||
for sc, d in self._data.marketLog.items():
|
||||
plain = _plain(sc)
|
||||
already_in_pool = plain in self._data.stockCodeIdMap
|
||||
|
||||
def _data_cell(text, w, e, color=None, size=None):
|
||||
if e > 0:
|
||||
return ft.Container(_text(text, color=color, size=size), padding=4, expand=e)
|
||||
return ft.Container(_text(text, color=color, size=size), width=w, padding=4)
|
||||
|
||||
cells = [
|
||||
_data_cell(d.get('time', ''), *_C[0]),
|
||||
_data_cell(f"{d['stock_name']}-{sc}", *_C[1]),
|
||||
_data_cell(f"{d['last_price']:.3f}", *_C[2]),
|
||||
]
|
||||
if already_in_pool:
|
||||
cells.append(_data_cell("已在池", *_C[3], color='#AAAAAA', size=11))
|
||||
else:
|
||||
cells.append(ft.Container(
|
||||
ft.IconButton(ft.Icons.ADD, icon_size=20, tooltip="添加持仓",
|
||||
on_click=lambda e, code=sc: self._on_add_from_market(code)),
|
||||
width=_C[3][0], padding=0))
|
||||
row = ft.Row(cells, spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
rows.append(ft.Divider(height=1, color='#f0f0f0'))
|
||||
|
||||
self._market_table.controls = [ft.Column(rows, spacing=0)]
|
||||
|
||||
def _on_add_from_market(self, stock_code: str):
|
||||
"""+ 按钮:从市场监控添加标的并打开网格配置"""
|
||||
info = self._data.marketLog.get(stock_code)
|
||||
if not info:
|
||||
return
|
||||
stock_name = info.get('stock_name', '')
|
||||
tid = self._data.add_from_market(stock_code, stock_name)
|
||||
target = self._data.tradeTargets.get(tid)
|
||||
if target is None:
|
||||
return
|
||||
# 打开网格配置弹窗
|
||||
self._dialogs.open_config(target)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
@@ -641,6 +757,31 @@ class _TradeDialogs:
|
||||
self._on_done()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}')
|
||||
|
||||
def confirm_remove(self, target):
|
||||
"""移除网格策略 — 根据持仓量显示不同提示"""
|
||||
if target.current_position > 0:
|
||||
msg = f"该标的当前持仓 {target.current_position} 股,将移回「未分类持仓」。\n\n确定要移除网格策略吗?\n\n{target.stock_code} {target.stock_name}"
|
||||
else:
|
||||
msg = f"该标的当前无持仓,将直接删除。\n\n确定要移除吗?\n\n{target.stock_code} {target.stock_name}"
|
||||
dlg = ft.AlertDialog(
|
||||
title=_text("确认移除"),
|
||||
content=_text(msg),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close()),
|
||||
ft.TextButton("确定", on_click=lambda e, t=target: self._do_remove(t)),
|
||||
],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def _do_remove(self, target):
|
||||
self._close()
|
||||
result = self._data.remove_target(target.get_id())
|
||||
self._on_done()
|
||||
if result == 'unclassified':
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 降级为未分类: {target.targetName()}')
|
||||
elif result == 'deleted':
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 已删除: {target.targetName()}')
|
||||
|
||||
def _close(self):
|
||||
self._page.pop_dialog()
|
||||
self._page.update()
|
||||
@@ -737,11 +878,21 @@ class QmtApp:
|
||||
on_click=lambda e: self._toggle_drawer())
|
||||
self._overlay = self._drawer.build()
|
||||
|
||||
# 市场状态指示灯
|
||||
self._market_dot = ft.Container(width=8, height=8, border_radius=4,
|
||||
bgcolor='#AAAAAA')
|
||||
self._market_label = _text("休市", color='#888888', size=12)
|
||||
market_status = ft.Row([self._market_dot, self._market_label], spacing=6,
|
||||
alignment=ft.MainAxisAlignment.CENTER)
|
||||
# 应用启动时同步当前状态
|
||||
self._update_market_status(qmtv.isMarketActive)
|
||||
|
||||
# 顶部标题栏 — 左侧市场状态,右侧刷新 + 侧栏按钮
|
||||
title = ft.Container(
|
||||
ft.Row([
|
||||
_text("网格策略持仓", bold=True, size=13),
|
||||
market_status,
|
||||
ft.Row([
|
||||
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18,
|
||||
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=20,
|
||||
on_click=lambda e: self._manual_refresh()),
|
||||
self._sidebar_icon,
|
||||
], spacing=0),
|
||||
@@ -749,8 +900,17 @@ class QmtApp:
|
||||
padding=ft.Padding(10, 10, 10, 5),
|
||||
)
|
||||
|
||||
grid_body = ft.Container(content=self._grid.build(), expand=True,
|
||||
padding=ft.Padding(10, 0, 10, 10))
|
||||
# 网格持仓面板 — 标题 + 数据表,与右侧 drawer 风格一致
|
||||
grid_body = ft.Container(
|
||||
ft.Column([
|
||||
ft.Container(_text("网格策略持仓", bold=True, size=14),
|
||||
padding=ft.Padding(10, 10, 10, 5)),
|
||||
ft.Container(content=self._grid.build(), expand=True,
|
||||
padding=ft.Padding(10, 0, 10, 10)),
|
||||
], expand=True),
|
||||
bgcolor=ft.Colors.SURFACE,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
self.page.add(ft.Column([
|
||||
title, ft.Stack([grid_body, self._overlay], expand=True),
|
||||
@@ -761,6 +921,8 @@ class QmtApp:
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _refresh_ui(self):
|
||||
self._data.refresh_orders()
|
||||
self._data.refresh_trades()
|
||||
self._grid._rebuild()
|
||||
self._drawer.refresh_all()
|
||||
self.page.update()
|
||||
@@ -795,7 +957,19 @@ class QmtApp:
|
||||
self._refresh_ui()
|
||||
|
||||
def _on_market_active(self, is_active: bool):
|
||||
pass
|
||||
self._update_market_status(is_active)
|
||||
self.page.update()
|
||||
|
||||
def _update_market_status(self, active: bool):
|
||||
"""更新顶栏市场状态指示灯"""
|
||||
if active:
|
||||
self._market_dot.bgcolor = '#4CAF50'
|
||||
self._market_label.value = '盘中'
|
||||
self._market_label.color = '#2E7D32'
|
||||
else:
|
||||
self._market_dot.bgcolor = '#AAAAAA'
|
||||
self._market_label.value = '休市'
|
||||
self._market_label.color = '#888888'
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 后台刷新
|
||||
|
||||
@@ -520,9 +520,9 @@ class TradeTargetUI(ttk.Frame):
|
||||
for item in self.order_tree.get_children():
|
||||
self.order_tree.delete(item)
|
||||
|
||||
_CANCELED = {54, 57}
|
||||
_TERMINAL = {54, 56, 57}
|
||||
for o in orders:
|
||||
if getattr(o, 'order_status', 0) in _CANCELED:
|
||||
if getattr(o, 'order_status', 0) in _TERMINAL:
|
||||
continue
|
||||
code = getattr(o, 'stock_code', '')
|
||||
if '.' in code:
|
||||
|
||||
Reference in New Issue
Block a user