feat(hub): add symbol archive with permanent 5m klines
Add /archive page, hub_symbol_archive.db, trade overlay, 4h background sync, and instance /api/hub/trades/archive. Document in hub-symbol-archive-kline.md with cross-links. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -176,6 +176,97 @@ def fetch_trades_for_trading_day(
|
||||
return out
|
||||
|
||||
|
||||
def _normalize_archive_trade_row(
|
||||
d: dict,
|
||||
*,
|
||||
exchange_key: str = "",
|
||||
reset_hour: int = 8,
|
||||
) -> 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
|
||||
pnl = _effective_pnl(d)
|
||||
closed_at = _effective_field(d, "reviewed_closed_at", "closed_at")
|
||||
opened_at = _effective_field(d, "reviewed_opened_at", "opened_at")
|
||||
opened_ms = d.get("opened_at_ms")
|
||||
closed_ms = d.get("closed_at_ms")
|
||||
if opened_ms in (None, ""):
|
||||
odt = parse_dt_for_trading_day(opened_at)
|
||||
opened_ms = int(odt.timestamp() * 1000) if odt else None
|
||||
if closed_ms in (None, ""):
|
||||
cdt = close_dt
|
||||
closed_ms = int(cdt.timestamp() * 1000) if cdt else None
|
||||
try:
|
||||
trade_id = int(d.get("id"))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return {
|
||||
"id": trade_id,
|
||||
"exchange_key": (exchange_key or "").strip().lower(),
|
||||
"symbol": (d.get("symbol") or "").strip().upper(),
|
||||
"direction": d.get("direction"),
|
||||
"result": effective_result,
|
||||
"pnl_amount": round(pnl, 4),
|
||||
"closed_at": closed_at,
|
||||
"opened_at": opened_at,
|
||||
"opened_at_ms": int(opened_ms) if opened_ms else None,
|
||||
"closed_at_ms": int(closed_ms) if closed_ms else None,
|
||||
"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"),
|
||||
"trigger_price": d.get("trigger_price"),
|
||||
"stop_loss": _effective_field(d, "reviewed_stop_loss", "stop_loss"),
|
||||
"take_profit": _effective_field(d, "reviewed_take_profit", "take_profit"),
|
||||
"reviewed": bool(d.get("reviewed_at") or d.get("reviewed_result")),
|
||||
"trading_day": trading_day_from_dt(close_dt, reset_hour),
|
||||
}
|
||||
|
||||
|
||||
def fetch_trades_for_archive(
|
||||
conn,
|
||||
*,
|
||||
days: int = 365,
|
||||
row_to_dict_fn: Optional[Callable] = None,
|
||||
reset_hour: int = 8,
|
||||
limit: int = 2000,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""返回近 N 天已平仓记录(供币种档案聚合)。"""
|
||||
lim = max(1, min(int(limit or 2000), 5000))
|
||||
day_span = max(1, min(int(days or 365), 3650))
|
||||
cutoff = datetime.now() - timedelta(days=day_span)
|
||||
cutoff_s = cutoff.strftime("%Y-%m-%d %H:%M:%S")
|
||||
ts_expr = "REPLACE(COALESCE(reviewed_closed_at, closed_at, created_at, opened_at), 'T', ' ')"
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT id, symbol, direction, result, reviewed_result, pnl_amount, reviewed_pnl_amount,
|
||||
exchange_realized_pnl, closed_at, reviewed_closed_at, opened_at, reviewed_opened_at,
|
||||
opened_at_ms, closed_at_ms, created_at, monitor_type, actual_rr, planned_rr,
|
||||
trade_style, entry_reason, trigger_price, stop_loss, take_profit,
|
||||
reviewed_stop_loss, reviewed_take_profit, reviewed_at
|
||||
FROM trade_records
|
||||
WHERE {ts_expr} >= ?
|
||||
ORDER BY {ts_expr} DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(cutoff_s, lim * 2),
|
||||
).fetchall()
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
d = _row_dict(row, row_to_dict_fn)
|
||||
norm = _normalize_archive_trade_row(d, reset_hour=reset_hour)
|
||||
if norm:
|
||||
out.append(norm)
|
||||
if len(out) >= lim:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def summarize_trades(trades: list[dict]) -> dict[str, Any]:
|
||||
"""单笔列表 → 笔数 / 盈亏 / 胜败统计。"""
|
||||
total_pnl = 0.0
|
||||
|
||||
Reference in New Issue
Block a user