d6bb99b5bb
Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.4 KiB
Python
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()
|