Add stats trading calendar and fix CTP position avg/sync.

Calendar shows daily closed trade count and PnL with emotion-day highlighting; day click loads review-first trade list. Use exchange-only entry average and improve vnpy position sync after CTP reconnect.
This commit is contained in:
dekun
2026-06-30 11:59:25 +08:00
parent d07fc4b70d
commit 8ebad6e8a2
8 changed files with 926 additions and 198 deletions
+68 -24
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import json
import logging
import threading
import time
from datetime import datetime
from typing import Any, Callable, Optional
@@ -120,7 +121,7 @@ from trading_context import (
is_ctp_connected,
trading_mode_label,
)
from ctp_entry_price import resolve_ctp_entry
from ctp_entry_price import round_to_tick
from ctp_symbol import ths_to_vnpy_symbol
from ctp_trading_state import position_key, trading_state
from vnpy_bridge import (
@@ -549,14 +550,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
mode: str,
fallback: float = 0.0,
) -> float:
"""滚仓/展示用均价:优先柜台成交加权与持仓价。"""
"""滚仓/展示用均价:仅柜台持仓价。"""
if not ctp_status(mode).get("connected"):
return fallback
trades: list = []
try:
trades = ctp_list_trades(mode)
except Exception:
pass
for p in trading_state.get_positions() or _ctp_positions(
mode, refresh_if_empty=False,
):
@@ -564,11 +560,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
continue
if not _match_ctp_symbol(p.get("symbol") or "", sym):
continue
entry, _ = resolve_ctp_entry(
sym, direction, p, trades, tick=ctp_get_tick_price(mode, sym),
)
if entry > 0:
return float(entry)
avg = float(p.get("avg_price") or 0)
if avg > 0:
return avg
return fallback
def _resolve_ctp_entry_price(
@@ -577,17 +571,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
direction: str,
ctp: Optional[dict],
) -> tuple[float, str]:
del mode, direction
if not ctp:
return 0.0, "none"
trades: list = []
tick = None
if ctp_status(mode).get("connected"):
try:
trades = ctp_list_trades(mode)
except Exception:
pass
tick = ctp_get_tick_price(mode, sym)
return resolve_ctp_entry(sym, direction, ctp, trades, tick=tick)
avg = float(ctp.get("avg_price") or 0)
if avg > 0:
return round_to_tick(avg, sym), "ctp"
return 0.0, "none"
def _open_commission_from_ctp_trades(
mode: str, sym: str, direction: str,
@@ -1246,6 +1236,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
ctp_margin = float(ctp.get("margin") or 0)
if (margin is None or float(margin or 0) <= 0) and ctp_margin > 0:
margin = ctp_margin
if ctp_status(mode).get("connected"):
source_label = "CTP 柜台"
codes = ths_to_codes(sym)
tick = calc_order_tick_metrics(sym, lots, entry, trading_mode=mode)
@@ -1677,10 +1669,17 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
monitor_by_pk = _monitors_by_position_key(conn)
ctp_list: list[dict] = []
if ctp_status(mode).get("connected"):
ctp_list = trading_state.get_positions()
ctp_list = _ctp_positions(mode, refresh_if_empty=False, refresh_margin=False)
if not ctp_list:
ctp_list = _ctp_positions(
mode, refresh_if_empty=True, refresh_margin=not fast,
ctp_list = trading_state.get_positions()
if not ctp_list:
try:
with _ctp_td_lock:
get_bridge().calibrate_trading_state()
except Exception as exc:
logger.debug("live calibrate: %s", exc)
ctp_list = trading_state.get_positions() or _ctp_positions(
mode, refresh_if_empty=False, refresh_margin=False,
)
rows: list[dict] = []
@@ -1740,6 +1739,51 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
continue
seen.add(rk)
deduped.append(row)
if not deduped and ctp_status(mode).get("connected") and monitor_by_pk:
margin_used = float(ctp_account_margin_used(mode) or 0)
since_connect = 9999.0
try:
since_connect = time.time() - float(
getattr(get_bridge(), "_last_connect_ok_ts", 0) or 0,
)
except Exception:
pass
if margin_used > 100 or since_connect < 300:
for mon in monitor_by_pk.values():
lots = int(mon.get("lots") or 0)
if lots <= 0:
continue
sym = (mon.get("symbol") or "").strip()
direction = (mon.get("direction") or "long").strip().lower()
if fast:
mon = _overlay_sl_tp_readonly(conn, mon, sym, direction) or mon
else:
mon = (
_restore_monitor_sl_tp_if_missing(conn, mon, sym, direction)
or mon
)
try:
row = _compose_position_row(
conn,
mon=mon,
ctp=None,
mode=mode,
capital=capital,
now_iso=now_iso,
fast=fast,
)
if not row:
continue
rk = row.get("key") or row.get("position_key") or ""
if rk and rk in seen:
continue
if rk:
seen.add(rk)
deduped.append(row)
except Exception as exc:
logger.warning("compose monitor fallback row failed: %s", exc)
return deduped
def _build_trading_live_payload(conn, *, fast: bool = False) -> dict: