fix(hub): align AI summary trades with records and restyle headings
- Query trade_records with trading-day window and reviewed fields instead of missing session_date column - Style summary headings and account names as red text with white stroke Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+117
-36
@@ -4,6 +4,16 @@ from __future__ import annotations
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
TRADE_COMPLETED_RESULTS = (
|
||||
"止盈",
|
||||
"止损",
|
||||
"保本止盈",
|
||||
"移动止盈",
|
||||
"手动平仓",
|
||||
"强制清仓",
|
||||
"外部平仓",
|
||||
)
|
||||
|
||||
|
||||
def trading_day_from_dt(dt: datetime, reset_hour: int = 8) -> str:
|
||||
"""与实例 get_trading_day 一致:小时 < reset_hour 归属上一日历日。"""
|
||||
@@ -16,6 +26,28 @@ def current_trading_day(*, now: datetime | None = None, reset_hour: int = 8) ->
|
||||
return trading_day_from_dt(now or datetime.now(), reset_hour)
|
||||
|
||||
|
||||
def parse_dt_for_trading_day(raw: Any) -> datetime | None:
|
||||
if raw is None:
|
||||
return None
|
||||
s = str(raw).strip().replace("Z", "").replace("T", " ")
|
||||
if not s:
|
||||
return None
|
||||
for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
|
||||
try:
|
||||
return datetime.strptime(s[:ln], fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def trading_day_window_bounds(trading_day: str, reset_hour: int = 8) -> tuple[str, str]:
|
||||
"""交易日 [reset_hour, 次日 reset_hour) 对应的北京时间字符串区间(闭区间)。"""
|
||||
day = datetime.strptime((trading_day or "").strip()[:10], "%Y-%m-%d")
|
||||
start = day.replace(hour=reset_hour, minute=0, second=0, microsecond=0)
|
||||
end = start + timedelta(days=1) - timedelta(seconds=1)
|
||||
return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _row_dict(row, row_to_dict: Optional[Callable] = None) -> dict:
|
||||
if row is None:
|
||||
return {}
|
||||
@@ -36,62 +68,111 @@ def _row_dict(row, row_to_dict: Optional[Callable] = None) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _effective_field(d: dict, reviewed_key: str, base_key: str, default: Any = None) -> Any:
|
||||
rv = d.get(reviewed_key)
|
||||
if rv is not None and str(rv).strip() != "":
|
||||
return rv
|
||||
bv = d.get(base_key)
|
||||
if bv is not None and str(bv).strip() != "":
|
||||
return bv
|
||||
return default
|
||||
|
||||
|
||||
def _effective_pnl(d: dict) -> float:
|
||||
reviewed = d.get("reviewed_pnl_amount")
|
||||
if reviewed is not None and str(reviewed).strip() != "":
|
||||
try:
|
||||
return float(reviewed)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
ex = d.get("exchange_realized_pnl")
|
||||
if ex is not None and str(ex).strip() != "":
|
||||
try:
|
||||
return float(ex)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
return float(d.get("pnl_amount") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _trade_close_dt(d: dict) -> datetime | None:
|
||||
raw = _effective_field(d, "reviewed_closed_at", "closed_at")
|
||||
if raw is None or str(raw).strip() == "":
|
||||
raw = d.get("created_at") or d.get("opened_at")
|
||||
return parse_dt_for_trading_day(raw)
|
||||
|
||||
|
||||
def _normalize_trade_row(
|
||||
d: dict,
|
||||
*,
|
||||
trading_day: str,
|
||||
reset_hour: int,
|
||||
) -> dict[str, Any] | None:
|
||||
effective_result = str(_effective_field(d, "reviewed_result", "result") or "").strip()
|
||||
if effective_result not in TRADE_COMPLETED_RESULTS:
|
||||
return None
|
||||
close_dt = _trade_close_dt(d)
|
||||
if not close_dt:
|
||||
return None
|
||||
if trading_day_from_dt(close_dt, reset_hour) != trading_day:
|
||||
return None
|
||||
pnl = _effective_pnl(d)
|
||||
closed_at = _effective_field(d, "reviewed_closed_at", "closed_at")
|
||||
opened_at = _effective_field(d, "reviewed_opened_at", "opened_at")
|
||||
return {
|
||||
"symbol": d.get("symbol"),
|
||||
"direction": d.get("direction"),
|
||||
"result": effective_result,
|
||||
"pnl_amount": round(pnl, 4),
|
||||
"closed_at": closed_at,
|
||||
"opened_at": opened_at,
|
||||
"monitor_type": d.get("monitor_type"),
|
||||
"actual_rr": d.get("actual_rr"),
|
||||
"planned_rr": d.get("planned_rr"),
|
||||
"trade_style": d.get("trade_style"),
|
||||
"entry_reason": d.get("entry_reason"),
|
||||
"reviewed": bool(d.get("reviewed_at") or d.get("reviewed_result")),
|
||||
}
|
||||
|
||||
|
||||
def fetch_trades_for_trading_day(
|
||||
conn,
|
||||
trading_day: str,
|
||||
*,
|
||||
row_to_dict_fn: Optional[Callable] = None,
|
||||
reset_hour: int = 8,
|
||||
limit: int = 200,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""返回指定交易日的已平仓记录(优先 session_date,否则 closed_at 日期)。"""
|
||||
"""返回指定交易日的已平仓记录(与 /records 交易记录一致,复盘字段优先)。"""
|
||||
day = (trading_day or "").strip()[:10]
|
||||
if not day:
|
||||
return []
|
||||
lim = max(1, min(int(limit or 200), 500))
|
||||
start_bj, end_bj = trading_day_window_bounds(day, reset_hour)
|
||||
ts_expr = "REPLACE(COALESCE(reviewed_closed_at, closed_at, created_at, opened_at), 'T', ' ')"
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT symbol, exchange_symbol, direction, result, pnl_amount,
|
||||
closed_at, opened_at, session_date, monitor_type,
|
||||
actual_rr, planned_rr, trade_style, entry_reason
|
||||
SELECT symbol, direction, result, reviewed_result, pnl_amount, reviewed_pnl_amount,
|
||||
exchange_realized_pnl, closed_at, reviewed_closed_at, opened_at, reviewed_opened_at,
|
||||
created_at, monitor_type, actual_rr, planned_rr, trade_style, entry_reason,
|
||||
reviewed_at
|
||||
FROM trade_records
|
||||
WHERE (
|
||||
(session_date IS NOT NULL AND TRIM(session_date) != '' AND session_date = ?)
|
||||
OR (
|
||||
(session_date IS NULL OR TRIM(session_date) = '')
|
||||
AND closed_at IS NOT NULL AND TRIM(closed_at) != ''
|
||||
AND substr(closed_at, 1, 10) = ?
|
||||
)
|
||||
)
|
||||
AND result IS NOT NULL AND TRIM(result) != ''
|
||||
ORDER BY COALESCE(closed_at, opened_at) ASC
|
||||
WHERE {ts_expr} >= ? AND {ts_expr} <= ?
|
||||
ORDER BY {ts_expr} ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
(day, day, lim),
|
||||
(start_bj, end_bj, lim * 3),
|
||||
).fetchall()
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
d = _row_dict(row, row_to_dict_fn)
|
||||
try:
|
||||
pnl = float(d.get("pnl_amount") or 0)
|
||||
except (TypeError, ValueError):
|
||||
pnl = 0.0
|
||||
out.append(
|
||||
{
|
||||
"symbol": d.get("symbol"),
|
||||
"exchange_symbol": d.get("exchange_symbol"),
|
||||
"direction": d.get("direction"),
|
||||
"result": d.get("result"),
|
||||
"pnl_amount": round(pnl, 4),
|
||||
"closed_at": d.get("closed_at"),
|
||||
"opened_at": d.get("opened_at"),
|
||||
"session_date": d.get("session_date"),
|
||||
"monitor_type": d.get("monitor_type"),
|
||||
"actual_rr": d.get("actual_rr"),
|
||||
"planned_rr": d.get("planned_rr"),
|
||||
"trade_style": d.get("trade_style"),
|
||||
"entry_reason": d.get("entry_reason"),
|
||||
}
|
||||
)
|
||||
norm = _normalize_trade_row(d, trading_day=day, reset_hour=reset_hour)
|
||||
if norm:
|
||||
out.append(norm)
|
||||
if len(out) >= lim:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user