feat: 内照明心交易日历与交易所口径成交额/手续费统计

新增按 08:00 切日的月历(盈亏、笔数、犯病日高亮与点击筛选);平仓时从交易所 fill 写入双边成交额与手续费,统计表与明细同步展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-30 08:05:46 +08:00
parent 865567fbd3
commit 6b872b1f43
13 changed files with 1113 additions and 12 deletions
+69 -1
View File
@@ -201,6 +201,7 @@ from history_window_lib import (
utc_window_to_utc_sql_strings,
)
from trade_result_lib import count_winning_trades, normalize_result_with_pnl
from trade_exchange_stats_lib import attach_exchange_stats_to_trade, filter_position_lifecycle_fills
def load_env_file(path):
@@ -1411,6 +1412,8 @@ def init_db():
"ALTER TABLE trade_records ADD COLUMN exchange_opened_at TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_closed_at TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_sync_key TEXT",
"ALTER TABLE trade_records ADD COLUMN exchange_turnover_usdt REAL",
"ALTER TABLE trade_records ADD COLUMN exchange_commission_usdt REAL",
):
try:
c.execute(ddl)
@@ -2254,6 +2257,8 @@ def insert_trade_record(
key_signal_type=None,
entry_reason=None,
trend_plan_id=None,
exchange_symbol=None,
attach_exchange_stats=True,
):
hold_minutes = calc_hold_minutes(hold_seconds)
open_ts = opened_at or app_now_str()
@@ -2270,7 +2275,7 @@ def insert_trade_record(
or entry_reason_for_monitor_type(monitor_type)
or ""
)
conn.execute(
cur = conn.execute(
"INSERT INTO trade_records (symbol,monitor_type,key_signal_type,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage,pnl_amount,hold_seconds,trade_style,risk_amount,planned_rr,actual_rr,hold_minutes,opened_at,opened_at_ms,closed_at,closed_at_ms,result,miss_reason,exchange_trade_id,entry_reason,trend_plan_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
symbol, monitor_type, kst, direction, trigger_price, snap_sl, snap_sl, take_profit,
@@ -2280,6 +2285,20 @@ def insert_trade_record(
trend_plan_id,
)
)
tid = int(cur.lastrowid or 0)
if attach_exchange_stats and tid:
ex_sym = (exchange_symbol or "").strip() or normalize_exchange_symbol(symbol)
_attach_okx_trade_exchange_stats(
conn,
tid,
exchange_symbol=ex_sym,
direction=direction,
opened_at_str=open_ts,
closed_at_str=close_ts,
opened_at_ms=open_ts_ms,
closed_at_ms=close_ts_ms,
)
return tid
def calc_duration_text(open_str, close_str):
@@ -3392,6 +3411,55 @@ def fetch_closing_fills_for_record(exchange_symbol, direction, opened_at_str, cl
return all_side_candidates[-20:]
def fetch_all_position_fills_for_record(
exchange_symbol, direction, opened_at_str, closed_at_str=None, opened_at_ms=None, closed_at_ms=None
):
if not (OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE):
return []
ensure_markets_loaded()
since_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
closed_ms = _to_ms_with_fallback(closed_at_ms, closed_at_str) if (closed_at_str or closed_at_ms is not None) else None
if closed_ms is not None:
closed_ms += 6 * 60 * 60 * 1000
try:
trades = exchange.fetch_my_trades(exchange_symbol, since=since_ms, limit=200)
except Exception:
trades = []
return filter_position_lifecycle_fills(
trades or [],
direction,
since_ms,
closed_ms,
hedge_mode=(OKX_POS_MODE == "hedge"),
close_buffer_ms=0,
)
def _attach_okx_trade_exchange_stats(
conn, trade_id, *, exchange_symbol, direction, opened_at_str, closed_at_str, opened_at_ms=None, closed_at_ms=None
):
if not (OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE):
return
open_ms = _to_ms_with_fallback(opened_at_ms, opened_at_str)
close_ms = _to_ms_with_fallback(closed_at_ms, closed_at_str)
contract_size = 1.0
try:
ensure_markets_loaded()
contract_size = float(exchange.market(exchange_symbol).get("contractSize") or 1)
except Exception:
pass
def _fetch():
return fetch_all_position_fills_for_record(
exchange_symbol, direction, opened_at_str, closed_at_str, opened_at_ms=open_ms, closed_at_ms=close_ms
)
try:
attach_exchange_stats_to_trade(conn, trade_id, fetch_fills=_fetch, contract_size=contract_size)
except Exception:
pass
def calc_weighted_exit_price(trades):
if not trades:
return None