e5a586f903
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
289 lines
11 KiB
Python
289 lines
11 KiB
Python
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
|
# 专有软件 — 未经授权禁止复制、传播、转售。
|
|
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
|
|
|
"""数据看板:账户、关键位、平仓记录聚合。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Callable, Optional
|
|
from zoneinfo import ZoneInfo
|
|
|
|
_TZ = ZoneInfo("Asia/Shanghai")
|
|
_PRICE_CACHE: dict[str, tuple[float, float]] = {}
|
|
_PRICE_CACHE_TTL = 2.0
|
|
|
|
|
|
def _cached_fetch_price(
|
|
fetch_price: Callable[[str, str, str], Optional[float]],
|
|
sym: str,
|
|
market: str,
|
|
sina: str,
|
|
) -> Optional[float]:
|
|
key = sym or ""
|
|
now = datetime.now().timestamp()
|
|
hit = _PRICE_CACHE.get(key)
|
|
if hit and (now - hit[1]) < _PRICE_CACHE_TTL:
|
|
return hit[0]
|
|
price = fetch_price(sym, market, sina)
|
|
if price is not None:
|
|
_PRICE_CACHE[key] = (float(price), now)
|
|
return price
|
|
|
|
|
|
def _direction_label(direction: str) -> str:
|
|
return "做多" if (direction or "").strip().lower() == "long" else "做空"
|
|
|
|
|
|
def _symbol_fields(ths_code: str) -> dict[str, Any]:
|
|
from modules.core.symbols import position_symbol_meta
|
|
|
|
sym = (ths_code or "").strip()
|
|
meta = position_symbol_meta(sym)
|
|
return {
|
|
"symbol_code": sym,
|
|
"symbol_name": meta.get("name") or sym,
|
|
"symbol_exchange": meta.get("exchange") or "",
|
|
"symbol_is_main": bool(meta.get("is_main")),
|
|
}
|
|
|
|
|
|
def build_risk_overview(
|
|
conn,
|
|
get_setting: Callable[[str, str], str],
|
|
*,
|
|
equity: Optional[float] = None,
|
|
margin_used: Optional[float] = None,
|
|
) -> dict[str, Any]:
|
|
from risk.account_risk_lib import (
|
|
cooling_hours_manual,
|
|
cooling_hours_manual_journal,
|
|
count_daily_opens,
|
|
daily_position_limit,
|
|
daily_trading_risk_pct_limit,
|
|
daily_trading_risk_used_pct,
|
|
ensure_account_risk_schema,
|
|
get_risk_status,
|
|
manual_close_daily_limit,
|
|
max_active_positions,
|
|
risk_control_enabled,
|
|
trading_day_label,
|
|
trading_day_reset_hour,
|
|
)
|
|
from modules.core.trading_context import (
|
|
get_fixed_amount,
|
|
get_fixed_lots,
|
|
get_max_margin_pct,
|
|
get_roll_max_margin_pct,
|
|
get_sizing_mode,
|
|
)
|
|
|
|
ensure_account_risk_schema(conn)
|
|
risk = dict(get_risk_status(conn, equity=equity) or {})
|
|
row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone()
|
|
td = trading_day_label()
|
|
stored_td = str(row["trading_day"] or "") if row else ""
|
|
manual_count = int(row["manual_close_count"] or 0) if row and stored_td == td else 0
|
|
|
|
margin_pct_used: Optional[float] = None
|
|
if equity and equity > 0 and margin_used is not None and margin_used >= 0:
|
|
margin_pct_used = round(float(margin_used) / float(equity) * 100, 2)
|
|
|
|
max_margin = get_max_margin_pct(get_setting)
|
|
sizing = get_sizing_mode(get_setting)
|
|
sizing_label = "固定金额" if sizing == "amount" else "固定手数"
|
|
|
|
daily_opens = int(risk.get("daily_open_count") or count_daily_opens(conn))
|
|
daily_risk_used = risk.get("daily_risk_used_pct")
|
|
if daily_risk_used is None and equity and equity > 0:
|
|
daily_risk_used = daily_trading_risk_used_pct(conn, float(equity))
|
|
|
|
return {
|
|
"enabled": risk_control_enabled(),
|
|
"status": risk,
|
|
"manual_close_count_today": manual_count,
|
|
"margin_pct_used": margin_pct_used,
|
|
"daily_open_count": daily_opens,
|
|
"daily_risk_used_pct": daily_risk_used,
|
|
"limits": {
|
|
"max_active_positions": max_active_positions(),
|
|
"position_mode": "single" if max_active_positions() <= 1 else "multi",
|
|
"position_mode_label": "单仓模式" if max_active_positions() <= 1 else "多仓模式",
|
|
"daily_position_limit": daily_position_limit(),
|
|
"daily_trading_risk_pct_limit": daily_trading_risk_pct_limit(),
|
|
"manual_close_daily_limit": manual_close_daily_limit(),
|
|
"cooling_hours_manual": cooling_hours_manual(),
|
|
"cooling_hours_manual_journal": cooling_hours_manual_journal(),
|
|
"trading_day_reset_hour": trading_day_reset_hour(),
|
|
"max_margin_pct": max_margin,
|
|
"roll_max_margin_pct": get_roll_max_margin_pct(get_setting),
|
|
"sizing_mode": sizing,
|
|
"sizing_label": sizing_label,
|
|
"fixed_lots": get_fixed_lots(get_setting),
|
|
"fixed_amount": get_fixed_amount(get_setting),
|
|
},
|
|
}
|
|
|
|
|
|
def build_dashboard_payload(
|
|
*,
|
|
get_db: Callable,
|
|
get_setting: Callable[[str, str], str],
|
|
fetch_price: Callable[[str, str, str], Optional[float]],
|
|
closes_limit: int = 40,
|
|
sync_ctp_trades: bool = False,
|
|
) -> dict[str, Any]:
|
|
from modules.core.trading_context import get_account_capital, get_trading_mode, trading_mode_label
|
|
from modules.ctp.vnpy_bridge import ctp_account_margin_used, ctp_status, get_bridge
|
|
|
|
mode = get_trading_mode(get_setting)
|
|
ctp_st = dict(ctp_status(mode) or {})
|
|
conn = get_db()
|
|
try:
|
|
capital = float(get_account_capital(conn, get_setting) or 0)
|
|
equity = capital
|
|
available: Optional[float] = None
|
|
margin_used: Optional[float] = None
|
|
|
|
if ctp_st.get("connected"):
|
|
if sync_ctp_trades:
|
|
try:
|
|
from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp
|
|
|
|
sync_trade_logs_from_ctp(
|
|
conn, mode, capital=capital, trading_mode=mode,
|
|
)
|
|
conn.commit()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
b = get_bridge()
|
|
if b.connected_mode == mode and b.ping():
|
|
acc = b.get_account() or {}
|
|
else:
|
|
acc = {}
|
|
balance = float(acc.get("balance") or 0)
|
|
if balance > 0:
|
|
equity = balance
|
|
avail = acc.get("available")
|
|
if avail is not None:
|
|
available = round(float(avail), 2)
|
|
mu = ctp_account_margin_used(mode)
|
|
if mu is not None and mu > 0:
|
|
margin_used = round(float(mu), 2)
|
|
elif available is not None and equity > 0:
|
|
margin_used = round(max(0.0, equity - available), 2)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
from modules.core.trading_context import _cached_ctp_account
|
|
|
|
cached = _cached_ctp_account(mode)
|
|
balance = float(cached.get("balance") or 0)
|
|
if balance > 0:
|
|
equity = balance
|
|
avail = cached.get("available")
|
|
if avail is not None:
|
|
available = round(float(avail), 2)
|
|
if equity > 0:
|
|
margin_used = round(max(0.0, equity - available), 2)
|
|
|
|
key_rows = conn.execute(
|
|
"""
|
|
SELECT id, symbol, symbol_name, market_code, sina_code,
|
|
monitor_type, direction, upper, lower, trade_mode,
|
|
bar_period, trailing_be
|
|
FROM key_monitors
|
|
WHERE status='active' OR status IS NULL
|
|
ORDER BY id DESC
|
|
"""
|
|
).fetchall()
|
|
keys: list[dict[str, Any]] = []
|
|
for r in key_rows:
|
|
sym = r["symbol"]
|
|
market = r["market_code"] or ""
|
|
sina = r["sina_code"] or ""
|
|
upper = float(r["upper"] or 0)
|
|
lower = float(r["lower"] or 0)
|
|
price = _cached_fetch_price(fetch_price, sym, market, sina)
|
|
dist_upper = dist_lower = None
|
|
if price is not None:
|
|
dist_upper = round(upper - float(price), 2)
|
|
dist_lower = round(float(price) - lower, 2)
|
|
mtype = r["monitor_type"] or ""
|
|
sf = _symbol_fields(sym)
|
|
keys.append({
|
|
"id": r["id"],
|
|
"symbol": sym,
|
|
**sf,
|
|
"symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym,
|
|
"monitor_type": mtype,
|
|
"direction": r["direction"] or "",
|
|
"direction_label": _direction_label(r["direction"] or "long")
|
|
if r["direction"] else "",
|
|
"upper": upper,
|
|
"lower": lower,
|
|
"trade_mode": r["trade_mode"] or "",
|
|
"bar_period": r["bar_period"] or "5m",
|
|
"trailing_be": bool(r["trailing_be"]),
|
|
"price": price,
|
|
"dist_upper": dist_upper,
|
|
"dist_lower": dist_lower,
|
|
})
|
|
|
|
close_rows = conn.execute(
|
|
"""
|
|
SELECT id, symbol, symbol_name, direction, lots,
|
|
entry_price, close_price, pnl, pnl_net, fee,
|
|
close_time, result, source
|
|
FROM trade_logs
|
|
ORDER BY id DESC
|
|
LIMIT ?
|
|
""",
|
|
(max(1, min(200, closes_limit)),),
|
|
).fetchall()
|
|
closes: list[dict[str, Any]] = []
|
|
for r in close_rows:
|
|
sym_code = r["symbol"] or ""
|
|
sf = _symbol_fields(sym_code)
|
|
closes.append({
|
|
"id": r["id"],
|
|
"symbol": r["symbol_name"] or sf.get("symbol_name") or sym_code,
|
|
"symbol_code": sym_code,
|
|
**sf,
|
|
"symbol_name": r["symbol_name"] or sf.get("symbol_name") or sym_code,
|
|
"direction": r["direction"] or "long",
|
|
"direction_label": _direction_label(r["direction"] or "long"),
|
|
"lots": float(r["lots"] or 0),
|
|
"entry_price": float(r["entry_price"] or 0),
|
|
"close_price": float(r["close_price"] or 0),
|
|
"pnl": float(r["pnl"] or 0) if r["pnl"] is not None else None,
|
|
"pnl_net": float(r["pnl_net"] or 0) if r["pnl_net"] is not None else None,
|
|
"fee": float(r["fee"] or 0) if r["fee"] is not None else None,
|
|
"close_time": (r["close_time"] or "")[:16].replace("T", " "),
|
|
"result": r["result"] or "",
|
|
"source": r["source"] or "",
|
|
})
|
|
|
|
now_iso = datetime.now(_TZ).strftime("%Y-%m-%d %H:%M:%S")
|
|
risk = build_risk_overview(
|
|
conn, get_setting, equity=equity, margin_used=margin_used,
|
|
)
|
|
return {
|
|
"ok": True,
|
|
"updated_at": now_iso,
|
|
"trading_mode_label": trading_mode_label(get_setting),
|
|
"ctp_status": ctp_st,
|
|
"account": {
|
|
"equity": round(equity, 2),
|
|
"margin_used": margin_used,
|
|
"available": available,
|
|
"capital_fallback": round(capital, 2),
|
|
},
|
|
"risk": risk,
|
|
"keys": keys,
|
|
"closes": closes,
|
|
}
|
|
finally:
|
|
conn.close()
|