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
+106 -1
View File
@@ -202,6 +202,12 @@ 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,
sum_binance_commission_income,
trade_ids_from_fills,
)
def load_env_file(path):
if not os.path.exists(path):
@@ -1499,6 +1505,8 @@ def init_db():
("exchange_opened_at", "ALTER TABLE trade_records ADD COLUMN exchange_opened_at TEXT"),
("exchange_closed_at", "ALTER TABLE trade_records ADD COLUMN exchange_closed_at TEXT"),
("exchange_sync_key", "ALTER TABLE trade_records ADD COLUMN exchange_sync_key TEXT"),
("exchange_turnover_usdt", "ALTER TABLE trade_records ADD COLUMN exchange_turnover_usdt REAL"),
("exchange_commission_usdt", "ALTER TABLE trade_records ADD COLUMN exchange_commission_usdt REAL"),
):
try:
c.execute(ddl)
@@ -2644,6 +2652,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()
@@ -2670,7 +2680,20 @@ def insert_trade_record(
trend_plan_id,
)
)
return int(cur.lastrowid or 0)
tid = int(cur.lastrowid or 0)
if attach_exchange_stats and tid:
ex_sym = (exchange_symbol or "").strip() or normalize_exchange_symbol(symbol)
_attach_binance_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):
@@ -4265,6 +4288,88 @@ def fetch_closing_fills_for_record(exchange_symbol, direction, opened_at_str, cl
return _cluster_closing_trades_near_close(all_side_candidates[-5:], int(closed_ms))
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,
):
"""持仓生命周期内全部 fill(开+平),用于双边成交额与手续费。"""
if not (BINANCE_API_KEY and BINANCE_API_SECRET):
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
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=(BINANCE_POSITION_MODE == "hedge"),
)
def _attach_binance_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 (BINANCE_API_KEY and BINANCE_API_SECRET):
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,
)
income_comm = None
if open_ms and close_ms:
fills_preview = _fetch()
trade_ids = trade_ids_from_fills(fills_preview)
buffer_ms = 3 * 60 * 1000 if trade_ids else 5 * 60 * 1000
entries = _fetch_binance_income_entries(
exchange_symbol,
max(0, int(open_ms) - buffer_ms),
int(close_ms) + buffer_ms,
)
income_comm = sum_binance_commission_income(entries, trade_ids or None)
try:
attach_exchange_stats_to_trade(
conn,
trade_id,
fetch_fills=_fetch,
contract_size=contract_size,
income_commission=income_comm,
)
except Exception:
pass
def calc_weighted_exit_price(trades):
if not trades:
return None