"""期权业务时区墙钟:库内统一按 APP_TIMEZONE(默认 Asia/Shanghai)存储,不可当 UTC.""" from __future__ import annotations import os import sqlite3 from datetime import datetime, timezone from typing import Any, Optional from zoneinfo import ZoneInfo _APP_TZ = ZoneInfo( (os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai" ) _MIGRATE_FLAG = "options_ts_utc_wall_to_app_done" def options_app_tz() -> ZoneInfo: return _APP_TZ def options_app_now_str() -> str: return datetime.now(_APP_TZ).strftime("%Y-%m-%d %H:%M:%S") def ms_to_options_app_str(ms: Any) -> Optional[str]: """交易所 ms(UTC 绝对时间) → 业务时区墙钟字符串.""" try: val = float(ms) except (TypeError, ValueError): return None if val <= 0: return None try: dt = datetime.fromtimestamp(int(val) / 1000.0, tz=timezone.utc).astimezone(_APP_TZ) return dt.strftime("%Y-%m-%d %H:%M:%S") except (TypeError, ValueError, OSError, OverflowError): return None def options_wall_clock_to_ms(raw: Any) -> Optional[int]: """业务时区墙钟 → UTC ms(与合约复盘 _local_input_datetime_to_ms 同口径).""" if raw is None or raw == "": return None s = str(raw).strip().replace("T", " ") for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16)): try: dt = datetime.strptime(s[:ln], fmt).replace(tzinfo=_APP_TZ) return int(dt.timestamp() * 1000) except ValueError: continue return None def utc_wall_clock_to_app_str(raw: Any) -> Optional[str]: """把误存成 UTC 墙钟的字符串改写为业务时区墙钟(+8 等).""" if raw is None or raw == "": return None s = str(raw).strip().replace("T", " ") for fmt, ln in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16)): try: dt = datetime.strptime(s[:ln], fmt).replace(tzinfo=timezone.utc) return dt.astimezone(_APP_TZ).strftime("%Y-%m-%d %H:%M:%S") except ValueError: continue return None def migrate_options_utc_wall_clock_to_app(conn: sqlite3.Connection) -> dict[str, int]: """一次性:历史 UTC 墙钟(SQLite CURRENT_TIMESTAMP / 旧 monitor)改为业务时区. 幂等:同步状态 options_ts_utc_wall_to_app_done=1 后跳过. """ from lib.options.options_review_db import init_options_review_tables from lib.options.options_review_lib import get_sync_state, set_sync_state init_options_review_tables(conn) if get_sync_state(conn, _MIGRATE_FLAG) == "1": return {"skipped": 1, "trades": 0, "review": 0, "hidden": 0} def _shift_table(table: str, cols: tuple[str, ...]) -> int: try: rows = conn.execute( f"SELECT rowid AS _rid, {', '.join(cols)} FROM {table}" ).fetchall() except sqlite3.Error: return 0 n = 0 for row in rows: sets: list[str] = [] vals: list[Any] = [] for col in cols: old = row[col] new = utc_wall_clock_to_app_str(old) if new and str(old or "").strip() != new: sets.append(f"{col}=?") vals.append(new) if not sets: continue vals.append(int(row["_rid"])) conn.execute( f"UPDATE {table} SET {', '.join(sets)} WHERE rowid=?", vals, ) n += 1 return n trades = 0 try: from lib.options.options_db import init_options_tables init_options_tables(conn) trades = _shift_table("options_trades", ("created_at", "closed_at")) except Exception: trades = 0 review = _shift_table("options_review_trades", ("opened_at", "closed_at")) hidden = _shift_table("options_review_hidden", ("closed_at",)) set_sync_state(conn, _MIGRATE_FLAG, "1") return {"skipped": 0, "trades": trades, "review": review, "hidden": hidden}