Sync OKX options closed trades into hub archive with a separate tab.

Mirror perpetual archive flow into archive_options_trade_cache for offline calendar and review.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-24 00:39:59 +08:00
parent 6f1ae14b3d
commit 54f1857fa2
8 changed files with 1058 additions and 27 deletions
+138 -19
View File
@@ -79,6 +79,12 @@ from lib.hub.hub_symbol_archive_lib import (
update_review_quote,
upsert_trade_overlay,
)
from lib.hub.hub_options_archive_lib import (
init_options_archive_db,
list_archive_options_calendar,
list_daily_options_trades,
sync_options_exchange_archive,
)
from lib.hub.hub_entry_plan_lib import (
compute_entry_plan_stats,
create_entry_plan,
@@ -355,9 +361,11 @@ def _schedule_board_refresh() -> None:
async def _run_archive_sync_once() -> dict:
global _last_archive_sync
init_archive_db()
init_options_archive_db()
settings = load_settings()
targets = enabled_exchanges(settings)
results: list[dict] = []
options_results: list[dict] = []
for ex in targets:
ex_key = str(ex.get("key") or "").strip().lower()
if not ex_key:
@@ -390,34 +398,71 @@ async def _run_archive_sync_once() -> dict:
"msg": msg,
}
)
else:
trades = trades_resp.get("trades") or []
for t in trades:
if isinstance(t, dict):
t["exchange_key"] = ex_key
def remote_fetch(**kwargs):
return _fetch_instance_ohlcv_sync(
ex,
symbol=kwargs.get("symbol") or "",
timeframe=kwargs.get("timeframe") or "5m",
since_ms=kwargs.get("since_ms"),
limit=int(kwargs.get("limit") or 500),
)
r = await asyncio.to_thread(
sync_exchange_symbol_archives,
ex_key,
trades,
remote_fetch,
)
r["name"] = ex.get("name")
r["trade_count"] = len(trades)
results.append(r)
caps = [str(x).lower() for x in (ex.get("capabilities") or [])]
if "options" not in caps:
continue
trades = trades_resp.get("trades") or []
for t in trades:
opt_resp = await asyncio.to_thread(
_fetch_instance_options_review_archive_sync,
ex,
days=ARCHIVE_TRADE_DAYS,
limit=ARCHIVE_TRADE_LIMIT,
)
if not opt_resp.get("ok"):
options_results.append(
{
"exchange_key": ex_key,
"name": ex.get("name"),
"ok": False,
"status": opt_resp.get("status"),
"msg": opt_resp.get("msg")
or opt_resp.get("error")
or opt_resp.get("detail")
or "拉取期权复盘失败",
"product": "options",
}
)
continue
opt_trades = opt_resp.get("trades") or []
for t in opt_trades:
if isinstance(t, dict):
t["exchange_key"] = ex_key
def remote_fetch(**kwargs):
return _fetch_instance_ohlcv_sync(
ex,
symbol=kwargs.get("symbol") or "",
timeframe=kwargs.get("timeframe") or "5m",
since_ms=kwargs.get("since_ms"),
limit=int(kwargs.get("limit") or 500),
)
r = await asyncio.to_thread(
sync_exchange_symbol_archives,
orow = await asyncio.to_thread(
sync_options_exchange_archive,
ex_key,
trades,
remote_fetch,
opt_trades,
)
r["name"] = ex.get("name")
r["trade_count"] = len(trades)
results.append(r)
orow["name"] = ex.get("name")
options_results.append(orow)
out = {
"ok": True,
"exchanges": len(targets),
"results": results,
"options_results": options_results,
"updated_at": __import__("datetime").datetime.now().isoformat(timespec="seconds"),
}
_last_archive_sync = out
@@ -1365,6 +1410,34 @@ def _fetch_instance_trades_archive_sync(
return {"ok": False, "msg": str(e)}
def _fetch_instance_options_review_archive_sync(
ex: dict,
*,
days: int = 365,
limit: int = 2000,
) -> dict:
base = (ex.get("flask_url") or "").rstrip("/")
if not base:
return {"ok": False, "msg": "未配置 flask_url"}
params = {"days": str(int(days)), "limit": str(int(limit))}
url = f"{base}/api/hub/options/review/archive?{urlencode(params)}"
try:
with httpx.Client(timeout=max(HUB_FLASK_TIMEOUT, 120.0)) as client:
r = client.get(url, headers=_hub_headers())
if r.status_code >= 400:
parsed = _parse_http_json_body(r)
parsed.setdefault("ok", False)
parsed.setdefault("status", r.status_code)
return parsed
data = r.json() if r.content else {}
if isinstance(data, dict):
data.setdefault("ok", True)
return data
return {"ok": False, "msg": "无效 JSON"}
except Exception as e:
return {"ok": False, "msg": str(e)}
def _fetch_instance_ohlcv_sync(
ex: dict,
*,
@@ -3145,6 +3218,52 @@ def api_archive_calendar(
return {"ok": True, **payload}
@app.get("/api/archive/options/daily-trades")
def api_archive_options_daily_trades(
period: str = "",
trading_day: str = "",
date_from: str = "",
date_to: str = "",
exchange_key: str = "",
filter_profit: str = "",
filter_loss: str = "",
search: str = "",
source_type: str = "",
):
init_options_archive_db()
payload = list_daily_options_trades(
trading_day=trading_day,
period=period or "today",
date_from=date_from,
date_to=date_to,
exchange_key=exchange_key,
filter_profit=(filter_profit or "").lower() in ("1", "true", "yes", "on"),
filter_loss=(filter_loss or "").lower() in ("1", "true", "yes", "on"),
search=search,
source_type=source_type,
)
return {"ok": True, **payload}
@app.get("/api/archive/options/calendar")
def api_archive_options_calendar(
year: int = 0,
month: int = 0,
exchange_key: str = "",
):
init_options_archive_db()
if year <= 0 or month <= 0:
td = today_trading_day()
parts = td.split("-")
year = int(parts[0])
month = int(parts[1])
try:
payload = list_archive_options_calendar(year, month, exchange_key=exchange_key)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
return {"ok": True, **payload}
@app.get("/api/archive/quotes")
def api_archive_quotes():
init_archive_db()