feat: 内照明心交易日历与交易所口径成交额/手续费统计
新增按 08:00 切日的月历(盈亏、笔数、犯病日高亮与点击筛选);平仓时从交易所 fill 写入双边成交额与手续费,统计表与明细同步展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+142
-5
@@ -118,6 +118,8 @@ def init_db(db_path: Path | None = None) -> None:
|
||||
closed_at_ms INTEGER,
|
||||
monitor_type TEXT,
|
||||
entry_reason TEXT,
|
||||
exchange_turnover_usdt REAL,
|
||||
exchange_commission_usdt REAL,
|
||||
payload_json TEXT,
|
||||
synced_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (exchange_key, trade_id)
|
||||
@@ -159,6 +161,14 @@ def init_db(db_path: Path | None = None) -> None:
|
||||
ON archive_review_quotes (quote_date DESC)
|
||||
"""
|
||||
)
|
||||
for ddl in (
|
||||
"ALTER TABLE archive_trade_cache ADD COLUMN exchange_turnover_usdt REAL",
|
||||
"ALTER TABLE archive_trade_cache ADD COLUMN exchange_commission_usdt REAL",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -167,6 +177,15 @@ def _now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def _optional_float(raw: Any) -> float | None:
|
||||
if raw in (None, ""):
|
||||
return None
|
||||
try:
|
||||
return float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_wall_clock_ms(raw: Any, *, tz: ZoneInfo = CHART_DISPLAY_TZ) -> int | None:
|
||||
"""将 YYYY-MM-DD[ HH:MM[:SS]] 按指定时区墙钟解析为 UTC 毫秒(默认 UTC+8)。"""
|
||||
if raw in (None, ""):
|
||||
@@ -331,8 +350,9 @@ def upsert_trades_cache(
|
||||
INSERT INTO archive_trade_cache (
|
||||
exchange_key, trade_id, symbol, direction, result, pnl_amount,
|
||||
opened_at, closed_at, opened_at_ms, closed_at_ms,
|
||||
monitor_type, entry_reason, payload_json, synced_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
monitor_type, entry_reason, exchange_turnover_usdt, exchange_commission_usdt,
|
||||
payload_json, synced_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(exchange_key, trade_id) DO UPDATE SET
|
||||
symbol=excluded.symbol,
|
||||
direction=excluded.direction,
|
||||
@@ -344,6 +364,8 @@ def upsert_trades_cache(
|
||||
closed_at_ms=excluded.closed_at_ms,
|
||||
monitor_type=excluded.monitor_type,
|
||||
entry_reason=excluded.entry_reason,
|
||||
exchange_turnover_usdt=excluded.exchange_turnover_usdt,
|
||||
exchange_commission_usdt=excluded.exchange_commission_usdt,
|
||||
payload_json=excluded.payload_json,
|
||||
synced_at=excluded.synced_at
|
||||
""",
|
||||
@@ -360,6 +382,8 @@ def upsert_trades_cache(
|
||||
t.get("closed_at_ms") or _parse_dt_ms(t.get("closed_at")),
|
||||
t.get("monitor_type"),
|
||||
entry_label,
|
||||
_optional_float(t.get("exchange_turnover_usdt")),
|
||||
_optional_float(t.get("exchange_commission_usdt")),
|
||||
json.dumps(payload, ensure_ascii=False, default=str),
|
||||
now,
|
||||
),
|
||||
@@ -437,6 +461,8 @@ def _trade_row_to_dict(row: sqlite3.Row, overlay: dict | None = None) -> dict[st
|
||||
"closed_at_ms",
|
||||
"monitor_type",
|
||||
"entry_reason",
|
||||
"exchange_turnover_usdt",
|
||||
"exchange_commission_usdt",
|
||||
"synced_at",
|
||||
):
|
||||
if key in d and d[key] not in (None, ""):
|
||||
@@ -1263,6 +1289,8 @@ def _empty_pnl_bucket() -> dict[str, Any]:
|
||||
"sick_count": 0,
|
||||
"pnl_total": 0.0,
|
||||
"pnl_ex_sick": 0.0,
|
||||
"turnover_total": 0.0,
|
||||
"commission_total": 0.0,
|
||||
"win_count": 0,
|
||||
"loss_count": 0,
|
||||
"avg_win": None,
|
||||
@@ -1286,6 +1314,8 @@ def _finalize_pnl_bucket(bucket: dict[str, Any]) -> None:
|
||||
bucket["max_loss"] = round(min(losses), 4) if losses else None
|
||||
bucket["pnl_total"] = round(float(bucket.get("pnl_total") or 0), 4)
|
||||
bucket["pnl_ex_sick"] = round(float(bucket.get("pnl_ex_sick") or 0), 4)
|
||||
bucket["turnover_total"] = round(float(bucket.get("turnover_total") or 0), 4)
|
||||
bucket["commission_total"] = round(float(bucket.get("commission_total") or 0), 4)
|
||||
bucket["win_rate"] = round(win_count / open_count * 100, 1) if open_count else None
|
||||
avg_win = bucket["avg_win"]
|
||||
if avg_win is not None and avg_loss is not None and avg_loss != 0:
|
||||
@@ -1294,9 +1324,18 @@ def _finalize_pnl_bucket(bucket: dict[str, Any]) -> None:
|
||||
bucket["profit_loss_ratio"] = None
|
||||
|
||||
|
||||
def _accumulate_trade_stat(bucket: dict[str, Any], *, pnl: float, is_sick: bool) -> None:
|
||||
def _accumulate_trade_stat(
|
||||
bucket: dict[str, Any],
|
||||
*,
|
||||
pnl: float,
|
||||
is_sick: bool,
|
||||
turnover: float = 0.0,
|
||||
commission: float = 0.0,
|
||||
) -> None:
|
||||
bucket["open_count"] += 1
|
||||
bucket["pnl_total"] += pnl
|
||||
bucket["turnover_total"] += turnover
|
||||
bucket["commission_total"] += commission
|
||||
if is_sick:
|
||||
bucket["sick_count"] += 1
|
||||
else:
|
||||
@@ -1316,10 +1355,16 @@ def _compute_period_stats(trade_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
pnl = float(td_row.get("pnl_amount") or 0)
|
||||
tag = str(td_row.get("behavior_tag") or "")
|
||||
is_sick = tag == "sick"
|
||||
_accumulate_trade_stat(total_bucket, pnl=pnl, is_sick=is_sick)
|
||||
turnover = float(td_row.get("exchange_turnover_usdt") or 0)
|
||||
commission = float(td_row.get("exchange_commission_usdt") or 0)
|
||||
_accumulate_trade_stat(
|
||||
total_bucket, pnl=pnl, is_sick=is_sick, turnover=turnover, commission=commission
|
||||
)
|
||||
if ex not in by_ex:
|
||||
by_ex[ex] = _empty_pnl_bucket()
|
||||
_accumulate_trade_stat(by_ex[ex], pnl=pnl, is_sick=is_sick)
|
||||
_accumulate_trade_stat(
|
||||
by_ex[ex], pnl=pnl, is_sick=is_sick, turnover=turnover, commission=commission
|
||||
)
|
||||
_finalize_pnl_bucket(total_bucket)
|
||||
for ex in by_ex:
|
||||
_finalize_pnl_bucket(by_ex[ex])
|
||||
@@ -1340,6 +1385,8 @@ def _compute_period_stats(trade_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"max_loss": total_bucket["max_loss"],
|
||||
"win_rate": total_bucket["win_rate"],
|
||||
"profit_loss_ratio": total_bucket["profit_loss_ratio"],
|
||||
"turnover_total": total_bucket["turnover_total"],
|
||||
"commission_total": total_bucket["commission_total"],
|
||||
"by_exchange": by_ex,
|
||||
}
|
||||
|
||||
@@ -1537,3 +1584,93 @@ def list_daily_trades(
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_archive_calendar(
|
||||
year: int,
|
||||
month: int,
|
||||
*,
|
||||
exchange_key: str = "",
|
||||
db_path: Path | None = None,
|
||||
reset_hour: int = TRADING_DAY_RESET_HOUR,
|
||||
) -> dict[str, Any]:
|
||||
"""按月返回每个交易日的盈亏、笔数、犯病标记(08:00 切日)。"""
|
||||
init_db(db_path)
|
||||
y = int(year)
|
||||
m = int(month)
|
||||
if m < 1 or m > 12:
|
||||
raise ValueError("month 无效")
|
||||
first = f"{y:04d}-{m:02d}-01"
|
||||
if m == 12:
|
||||
next_first = datetime(y + 1, 1, 1)
|
||||
else:
|
||||
next_first = datetime(y, m + 1, 1)
|
||||
last = (next_first - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
start_ms, _ = trading_day_bounds_ms(first, reset_hour=reset_hour)
|
||||
_, end_ms = trading_day_bounds_ms(last, reset_hour=reset_hour)
|
||||
ex_filter = (exchange_key or "").strip().lower()
|
||||
conn = _connect(db_path)
|
||||
try:
|
||||
params: list[Any] = [start_ms, end_ms]
|
||||
where = "closed_at_ms IS NOT NULL AND closed_at_ms >= ? AND closed_at_ms < ?"
|
||||
if ex_filter:
|
||||
where += " AND exchange_key=?"
|
||||
params.append(ex_filter)
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM archive_trade_cache WHERE {where}",
|
||||
params,
|
||||
).fetchall()
|
||||
overlays_by_ex: dict[str, dict[int, dict]] = {}
|
||||
days: dict[str, dict[str, Any]] = {}
|
||||
for r in rows:
|
||||
ex_k = r["exchange_key"]
|
||||
if ex_k not in overlays_by_ex:
|
||||
overlays_by_ex[ex_k] = load_overlays(ex_k, db_path=db_path)
|
||||
td_row = _trade_row_to_dict(r, overlays_by_ex[ex_k].get(int(r["trade_id"])))
|
||||
closed_ms = td_row.get("closed_at_ms") or _parse_dt_ms(td_row.get("closed_at"))
|
||||
if not closed_ms:
|
||||
continue
|
||||
day = ms_to_trading_day(int(closed_ms), reset_hour=reset_hour)
|
||||
if not day:
|
||||
continue
|
||||
if day < first or day > last:
|
||||
continue
|
||||
bucket = days.setdefault(
|
||||
day,
|
||||
{
|
||||
"trading_day": day,
|
||||
"open_count": 0,
|
||||
"sick_count": 0,
|
||||
"pnl_total": 0.0,
|
||||
"turnover_total": 0.0,
|
||||
"commission_total": 0.0,
|
||||
"has_sick": False,
|
||||
},
|
||||
)
|
||||
pnl = float(td_row.get("pnl_amount") or 0)
|
||||
tag = str(td_row.get("behavior_tag") or "")
|
||||
is_sick = tag == "sick"
|
||||
bucket["open_count"] += 1
|
||||
bucket["pnl_total"] += pnl
|
||||
bucket["turnover_total"] += float(td_row.get("exchange_turnover_usdt") or 0)
|
||||
bucket["commission_total"] += float(td_row.get("exchange_commission_usdt") or 0)
|
||||
if is_sick:
|
||||
bucket["sick_count"] += 1
|
||||
bucket["has_sick"] = True
|
||||
for d in days.values():
|
||||
d["pnl_total"] = round(float(d["pnl_total"]), 4)
|
||||
d["turnover_total"] = round(float(d["turnover_total"]), 4)
|
||||
d["commission_total"] = round(float(d["commission_total"]), 4)
|
||||
month_pnl = sum(float(d["pnl_total"]) for d in days.values())
|
||||
month_count = sum(int(d["open_count"]) for d in days.values())
|
||||
return {
|
||||
"year": y,
|
||||
"month": m,
|
||||
"date_from": first,
|
||||
"date_to": last,
|
||||
"days": days,
|
||||
"month_pnl_total": round(month_pnl, 4),
|
||||
"month_open_count": month_count,
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user