diff --git a/modules/risk/account_risk_lib.py b/modules/risk/account_risk_lib.py index 2d1c7aa..cef3d8d 100644 --- a/modules/risk/account_risk_lib.py +++ b/modules/risk/account_risk_lib.py @@ -216,49 +216,109 @@ def _opened_in_trading_day(open_time: str, now: Optional[datetime] = None) -> bo return oms >= int(trading_day_start(now).timestamp() * 1000) -def count_daily_opens(conn, now: Optional[datetime] = None) -> int: - rows = conn.execute( - "SELECT open_time FROM trade_order_monitors " +def _daily_open_slots(conn, now: Optional[datetime] = None) -> set[tuple[str, str]]: + """当日开仓槽位:按品种+方向去重(含已平),避免监控复活产生重复计数。""" + slots: set[tuple[str, str]] = set() + for r in conn.execute( + "SELECT symbol, direction, open_time FROM trade_order_monitors " "WHERE open_time IS NOT NULL AND trim(open_time) <> ''" - ).fetchall() - return sum(1 for r in rows if _opened_in_trading_day(r["open_time"], now)) + ).fetchall(): + if not _opened_in_trading_day(r["open_time"], now): + continue + sym = (r["symbol"] or "").strip() + if sym: + slots.add(_position_slot_key(sym, r["direction"] or "long")) + return slots + + +def count_daily_opens(conn, now: Optional[datetime] = None) -> int: + return len(_daily_open_slots(conn, now)) + + +def _risk_amount_for_monitor_row(r, equity: float) -> float: + from modules.core.contract_specs import calc_position_metrics + + entry = float(r["entry_price"] or 0) + if entry <= 0: + return 0.0 + lots = int(r["lots"] or 0) + if lots <= 0: + return 0.0 + sl = float(r["stop_loss"] if r["stop_loss"] is not None else entry) + tp = float(r["take_profit"] if r["take_profit"] is not None else entry) + m = calc_position_metrics( + r["direction"] or "long", + entry, + sl, + tp, + lots, + entry, + equity, + r["symbol"] or "", + ) + return float(m.get("risk_amount") or 0) def daily_trading_risk_used_pct( conn, equity: float, now: Optional[datetime] = None, ) -> Optional[float]: + """当日交易风险占权益(%):每品种槽位只计一次。 + + - 仍持仓:按止损距离算风险金额(以损定仓口径) + - 已平仓:按当日已实现亏损计(pnl_net<0),不再重复累加历史监控行 + """ if equity <= 0: return None - from modules.core.contract_specs import calc_position_metrics + slots = _daily_open_slots(conn, now) + if not slots: + return 0.0 - total = 0.0 - rows = conn.execute( + active_risk: dict[tuple[str, str], float] = {} + for r in conn.execute( """SELECT symbol, direction, lots, entry_price, stop_loss, take_profit, open_time FROM trade_order_monitors - WHERE open_time IS NOT NULL AND trim(open_time) <> ''""" - ).fetchall() - for r in rows: + WHERE status='active' AND open_time IS NOT NULL AND trim(open_time) <> ''""" + ).fetchall(): if not _opened_in_trading_day(r["open_time"], now): continue - entry = float(r["entry_price"] or 0) - if entry <= 0: + key = _position_slot_key(r["symbol"], r["direction"]) + if key not in slots: continue - sl = float(r["stop_loss"] if r["stop_loss"] is not None else entry) - tp = float(r["take_profit"] if r["take_profit"] is not None else entry) - lots = int(r["lots"] or 0) - if lots <= 0: + amt = _risk_amount_for_monitor_row(r, equity) + if amt > 0: + active_risk[key] = amt + + closed_risk: dict[tuple[str, str], float] = {} + for r in conn.execute( + """SELECT symbol, direction, pnl_net, open_time + FROM trade_logs + WHERE open_time IS NOT NULL AND trim(open_time) <> ''""" + ).fetchall(): + if not _opened_in_trading_day(r["open_time"], now): continue - m = calc_position_metrics( - r["direction"] or "long", - entry, - sl, - tp, - lots, - entry, - equity, - r["symbol"] or "", - ) - total += float(m.get("risk_amount") or 0) + key = _position_slot_key(r["symbol"], r["direction"]) + if key not in slots or key in active_risk: + continue + loss = max(0.0, -float(r["pnl_net"] or 0)) + if loss > 0: + closed_risk[key] = max(closed_risk.get(key, 0.0), loss) + + for r in conn.execute( + """SELECT symbol, direction, lots, entry_price, stop_loss, take_profit, open_time + FROM trade_order_monitors + WHERE status='closed' AND open_time IS NOT NULL AND trim(open_time) <> '' + ORDER BY id DESC""" + ).fetchall(): + if not _opened_in_trading_day(r["open_time"], now): + continue + key = _position_slot_key(r["symbol"], r["direction"]) + if key not in slots or key in active_risk or key in closed_risk: + continue + amt = _risk_amount_for_monitor_row(r, equity) + if amt > 0: + closed_risk[key] = amt + + total = sum(active_risk.values()) + sum(closed_risk.values()) if total <= 0: return 0.0 return round(total / equity * 100, 2)