"""期权复盘自动 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()