diff --git a/lib/hedge_plan/hedge_plan_settle_lib.py b/lib/hedge_plan/hedge_plan_settle_lib.py index 45af41c..f835181 100644 --- a/lib/hedge_plan/hedge_plan_settle_lib.py +++ b/lib/hedge_plan/hedge_plan_settle_lib.py @@ -1,13 +1,17 @@ """对冲计划结算辅助:到期内在价值与期权腿收口.""" from __future__ import annotations +import os import time -from datetime import datetime, timezone +from datetime import datetime from typing import Any, Callable, Optional +from zoneinfo import ZoneInfo from lib.exchange.okx_options_lib import normalize_option_exp_ms, resolve_option_close_from_history from lib.hedge_plan.hedge_plan_calc_lib import option_expiry_pnl +_APP_TZ = ZoneInfo((os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai") + def _sf(v: Any) -> Optional[float]: try: @@ -64,14 +68,15 @@ def all_option_legs_expired(legs: list[dict[str, Any]], *, now_ms: Optional[int] def _parse_opened_ms(raw: Any) -> Optional[int]: + """墙钟开仓时间 → UTC ms.库内时间为业务时区(默认 Asia/Shanghai),不可当 UTC.""" if raw is None or raw == "": return None s = str(raw).strip() if not s: return None - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%f"): + for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M:%f", 26), ("%Y-%m-%d %H:%M", 16)): try: - dt = datetime.strptime(s[:26], fmt).replace(tzinfo=timezone.utc) + dt = datetime.strptime(s[:ln], fmt).replace(tzinfo=_APP_TZ) return int(dt.timestamp() * 1000) except ValueError: continue diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index 7e10085..6dc43cb 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -1,13 +1,17 @@ """期权持仓监控:浮盈翻倍微信提醒 + 平仓/到期状态同步.""" from __future__ import annotations +import os import sqlite3 import time from datetime import datetime, timezone from typing import Any, Callable +from zoneinfo import ZoneInfo from lib.exchange.okx_options_lib import normalize_option_exp_ms, resolve_option_close_from_history +_APP_TZ = ZoneInfo((os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai") + def _safe_float(v: Any) -> float | None: if v is None: @@ -121,14 +125,15 @@ def run_options_profit_alerts( def _created_at_ms(created_at: Any) -> int | None: + """墙钟 created_at → UTC ms.库内时间为业务时区(默认 Asia/Shanghai),不可当 UTC.""" if not created_at: return None raw = str(created_at).strip() if not raw: return None - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%f"): + for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M:%f", 26), ("%Y-%m-%d %H:%M", 16)): try: - dt = datetime.strptime(raw[:26], fmt).replace(tzinfo=timezone.utc) + dt = datetime.strptime(raw[:ln], fmt).replace(tzinfo=_APP_TZ) return int(dt.timestamp() * 1000) except ValueError: continue diff --git a/tests/test_hedge_exchange_pnl.py b/tests/test_hedge_exchange_pnl.py index aef97e3..44e1cfa 100644 --- a/tests/test_hedge_exchange_pnl.py +++ b/tests/test_hedge_exchange_pnl.py @@ -6,6 +6,7 @@ import unittest from lib.hedge_plan.hedge_plan_db import get_plan, init_hedge_plan_tables, insert_leg, insert_plan from lib.hedge_plan.hedge_plan_settle_lib import ( + _parse_opened_ms, backfill_hedge_option_legs_realized_pnl, resolve_option_leg_realized_pnl, ) @@ -32,6 +33,30 @@ class HedgeExchangePnlTest(unittest.TestCase): self.assertEqual(src, "exchange") self.assertEqual(pnl, 12.0) + def test_opened_at_beijing_wall_clock_not_utc(self): + # 北京 09:19 开仓 → UTC 01:19; 到期结算北京 16:00:31 = UTC 08:00:31 + open_ms = _parse_opened_ms("2026-07-20 09:19:30") + self.assertEqual(open_ms, 1784510370000) + leg = { + "inst_id": "ETH-USD_UM-260720-1870-C", + "opened_at": "2026-07-20 09:19:30", + "premium": 6.9, + } + hist = [ + { + "instId": "ETH-USD_UM-260720-1870-C", + "uTime": "1784534431512", + "realizedPnl": "-7.1812815", + "pnl": "-6.9", + "type": "2", + } + ] + pnl, src = resolve_option_leg_realized_pnl( + leg=leg, hist_rows=hist, fallback=-6.9 + ) + self.assertEqual(src, "exchange") + self.assertAlmostEqual(pnl, -7.1813, places=4) + def test_backfill_updates_plan_total(self): conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row