Fix hedge option PnL match by parsing opened_at as Asia/Shanghai.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,17 @@
|
|||||||
"""对冲计划结算辅助:到期内在价值与期权腿收口."""
|
"""对冲计划结算辅助:到期内在价值与期权腿收口."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import Any, Callable, Optional
|
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.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
|
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]:
|
def _sf(v: Any) -> Optional[float]:
|
||||||
try:
|
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]:
|
def _parse_opened_ms(raw: Any) -> Optional[int]:
|
||||||
|
"""墙钟开仓时间 → UTC ms.库内时间为业务时区(默认 Asia/Shanghai),不可当 UTC."""
|
||||||
if raw is None or raw == "":
|
if raw is None or raw == "":
|
||||||
return None
|
return None
|
||||||
s = str(raw).strip()
|
s = str(raw).strip()
|
||||||
if not s:
|
if not s:
|
||||||
return None
|
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:
|
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)
|
return int(dt.timestamp() * 1000)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"""期权持仓监控:浮盈翻倍微信提醒 + 平仓/到期状态同步."""
|
"""期权持仓监控:浮盈翻倍微信提醒 + 平仓/到期状态同步."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Callable
|
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
|
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:
|
def _safe_float(v: Any) -> float | None:
|
||||||
if v is None:
|
if v is None:
|
||||||
@@ -121,14 +125,15 @@ def run_options_profit_alerts(
|
|||||||
|
|
||||||
|
|
||||||
def _created_at_ms(created_at: Any) -> int | None:
|
def _created_at_ms(created_at: Any) -> int | None:
|
||||||
|
"""墙钟 created_at → UTC ms.库内时间为业务时区(默认 Asia/Shanghai),不可当 UTC."""
|
||||||
if not created_at:
|
if not created_at:
|
||||||
return None
|
return None
|
||||||
raw = str(created_at).strip()
|
raw = str(created_at).strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
return None
|
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:
|
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)
|
return int(dt.timestamp() * 1000)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -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_db import get_plan, init_hedge_plan_tables, insert_leg, insert_plan
|
||||||
from lib.hedge_plan.hedge_plan_settle_lib import (
|
from lib.hedge_plan.hedge_plan_settle_lib import (
|
||||||
|
_parse_opened_ms,
|
||||||
backfill_hedge_option_legs_realized_pnl,
|
backfill_hedge_option_legs_realized_pnl,
|
||||||
resolve_option_leg_realized_pnl,
|
resolve_option_leg_realized_pnl,
|
||||||
)
|
)
|
||||||
@@ -32,6 +33,30 @@ class HedgeExchangePnlTest(unittest.TestCase):
|
|||||||
self.assertEqual(src, "exchange")
|
self.assertEqual(src, "exchange")
|
||||||
self.assertEqual(pnl, 12.0)
|
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):
|
def test_backfill_updates_plan_total(self):
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
|
|||||||
Reference in New Issue
Block a user