Files
crypto_monitor/lib/options/options_review_chart_async_lib.py

138 lines
4.4 KiB
Python

"""期权复盘自动 K 线:后台线程生成标的永续双周期拼图,写回 images_json."""
from __future__ import annotations
import logging
import threading
from typing import Any, Callable, Optional
from lib.options.options_review_images_lib import (
images_json_dumps,
parse_options_review_images_json,
)
logger = logging.getLogger(__name__)
OPTIONS_REVIEW_AUTO_CHART_TF = "auto"
def underlying_to_okx_perp_symbol(
underlying: str,
*,
normalize_symbol_input_fn: Callable[[str], str],
normalize_okx_symbol_fn: Callable[[str], str],
) -> str:
"""ETH / ETH-USD / ETH-USDT-SWAP → ETH/USDT:USDT."""
raw = str(underlying or "").strip().upper()
if not raw:
return ""
base = raw.split("-", 1)[0].split("/", 1)[0].strip()
if not base:
return ""
guess = normalize_symbol_input_fn(base) or f"{base}/USDT"
return str(normalize_okx_symbol_fn(guess) or "").strip()
def merge_auto_chart_into_images(
existing: Optional[list],
filename: str,
) -> list[dict[str, str]]:
"""保留手动槽位,用 tf=auto 放置拼图(方案甲一张总图)."""
fname = str(filename or "").strip()
out: list[dict[str, str]] = []
for item in existing or []:
if not isinstance(item, dict):
continue
tf = str(item.get("tf") or "").strip()
file = str(item.get("file") or "").strip()
if not file or tf == OPTIONS_REVIEW_AUTO_CHART_TF:
continue
out.append({"tf": tf, "file": file})
if fname:
out.insert(0, {"tf": OPTIONS_REVIEW_AUTO_CHART_TF, "file": fname})
return out
def build_options_review_chart_filename(trade_id: int) -> str:
return f"options_journal_chart_{int(trade_id)}.png"
def schedule_options_review_exchange_chart(
*,
trade_id: int,
exchange_symbol: str,
title_prefix: str,
journal_tfs: list[str],
journal_limit: int,
marker_payload: dict[str, Any],
upload_folder: str,
generate_chart_fn: Callable[..., str | None],
get_db_fn: Callable[[], Any],
now_str_fn: Callable[[], str],
) -> None:
"""保存后立即返回;线程内画图并合并进 options_review_entries.images_json."""
tid = int(trade_id or 0)
if tid <= 0 or not callable(generate_chart_fn) or not callable(get_db_fn):
return
symbol = str(exchange_symbol or "").strip()
if not symbol:
return
tfs = [str(x).strip() for x in (journal_tfs or []) if str(x).strip()]
if not tfs:
return
def _run() -> None:
try:
chart_fname = build_options_review_chart_filename(tid)
saved = generate_chart_fn(
symbol,
title_prefix,
timeframes=tfs,
limit=journal_limit,
out_dir=upload_folder,
filename=chart_fname,
filename_prefix="options_journal_chart",
marker_payload=marker_payload,
marker_timeframes={x.lower() for x in tfs},
layout="vertical",
)
if not saved:
logger.warning("options review chart async empty trade_id=%s", tid)
return
conn = get_db_fn()
try:
row = conn.execute(
"SELECT images_json FROM options_review_entries WHERE trade_id=?",
(tid,),
).fetchone()
if not row:
logger.warning(
"options review chart async missing entry trade_id=%s", tid
)
return
images = parse_options_review_images_json(row["images_json"])
merged = merge_auto_chart_into_images(images, saved)
primary = merged[0]["file"] if merged else saved
conn.execute(
"""UPDATE options_review_entries
SET images_json=?, image=?, updated_at=?
WHERE trade_id=?""",
(
images_json_dumps(merged),
primary,
now_str_fn(),
tid,
),
)
conn.commit()
finally:
conn.close()
except Exception:
logger.exception("options review chart async failed trade_id=%s", tid)
threading.Thread(
target=_run,
name=f"options-review-chart-{tid}",
daemon=True,
).start()