feat: 内照明心交易日历与交易所口径成交额/手续费统计
新增按 08:00 切日的月历(盈亏、笔数、犯病日高亮与点击筛选);平仓时从交易所 fill 写入双边成交额与手续费,统计表与明细同步展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from hub_symbol_archive_lib import init_db, list_archive_calendar, upsert_trades_cache, upsert_trade_overlay
|
||||
|
||||
|
||||
def _bj_ms(y, m, d, hh, mm):
|
||||
dt = datetime(y, m, d, hh, mm, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
|
||||
return int(dt.timestamp() * 1000)
|
||||
|
||||
|
||||
class ArchiveCalendarTests(unittest.TestCase):
|
||||
def test_calendar_groups_by_trading_day_and_sick(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
db = Path(td) / "arch.db"
|
||||
init_db(db)
|
||||
upsert_trades_cache(
|
||||
"binance",
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"symbol": "BTC/USDT",
|
||||
"direction": "long",
|
||||
"result": "止盈",
|
||||
"pnl_amount": 10.0,
|
||||
"opened_at": "2026-06-18 09:00:00",
|
||||
"closed_at": "2026-06-18 10:00:00",
|
||||
"closed_at_ms": _bj_ms(2026, 6, 18, 10, 0),
|
||||
"exchange_turnover_usdt": 2000.0,
|
||||
"exchange_commission_usdt": 0.8,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"symbol": "ETH/USDT",
|
||||
"direction": "short",
|
||||
"result": "止损",
|
||||
"pnl_amount": -5.0,
|
||||
"opened_at": "2026-06-18 14:00:00",
|
||||
"closed_at": "2026-06-18 15:00:00",
|
||||
"closed_at_ms": _bj_ms(2026, 6, 18, 15, 0),
|
||||
},
|
||||
],
|
||||
db_path=db,
|
||||
)
|
||||
upsert_trade_overlay("binance", 2, behavior_tag="sick", db_path=db)
|
||||
payload = list_archive_calendar(2026, 6, db_path=db)
|
||||
self.assertEqual(payload["month"], 6)
|
||||
days = payload["days"]
|
||||
self.assertTrue(days)
|
||||
sick_days = [d for d in days.values() if d.get("has_sick")]
|
||||
self.assertTrue(sick_days)
|
||||
self.assertGreaterEqual(payload["month_open_count"], 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
from trade_exchange_stats_lib import (
|
||||
aggregate_bilateral_stats,
|
||||
commission_usdt_from_fill,
|
||||
filter_position_lifecycle_fills,
|
||||
merge_commission_prefer_income,
|
||||
quote_turnover_usdt_from_fill,
|
||||
)
|
||||
|
||||
|
||||
class TradeExchangeStatsTests(unittest.TestCase):
|
||||
def test_turnover_from_cost(self):
|
||||
t = {"cost": 1000.0, "price": 50, "amount": 20}
|
||||
self.assertEqual(quote_turnover_usdt_from_fill(t), 1000.0)
|
||||
|
||||
def test_commission_from_fee(self):
|
||||
t = {"fee": {"cost": -0.42, "currency": "USDT"}}
|
||||
self.assertEqual(commission_usdt_from_fill(t), 0.42)
|
||||
|
||||
def test_bilateral_aggregate(self):
|
||||
fills = [
|
||||
{"side": "buy", "cost": 500, "fee": {"cost": -0.2, "currency": "USDT"}, "timestamp": 1000},
|
||||
{"side": "sell", "cost": 520, "fee": {"cost": -0.21, "currency": "USDT"}, "timestamp": 2000},
|
||||
]
|
||||
stats = aggregate_bilateral_stats(fills)
|
||||
self.assertIsNotNone(stats)
|
||||
self.assertEqual(stats["exchange_turnover_usdt"], 1020.0)
|
||||
self.assertEqual(stats["exchange_commission_usdt"], 0.41)
|
||||
|
||||
def test_filter_long_lifecycle(self):
|
||||
base = 1_700_000_000_000
|
||||
trades = [
|
||||
{"side": "buy", "timestamp": base, "cost": 100},
|
||||
{"side": "sell", "timestamp": base + 60_000, "cost": 110},
|
||||
{"side": "buy", "timestamp": base + 120_000, "cost": 999},
|
||||
]
|
||||
got = filter_position_lifecycle_fills(
|
||||
trades, "long", base - 1000, base + 90_000, close_buffer_ms=0
|
||||
)
|
||||
self.assertEqual(len(got), 2)
|
||||
|
||||
def test_prefer_income_commission(self):
|
||||
self.assertEqual(merge_commission_prefer_income(0.3, 0.45), 0.45)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user