期权复盘支持纯期权自动生成标的永续双周期K线,按时间标记进场出场。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -93,6 +93,141 @@ def _require_ex(cfg: dict[str, Any]):
|
||||
return ex, ""
|
||||
|
||||
|
||||
def _truthy(raw: Any) -> bool:
|
||||
return str(raw or "").strip().lower() in ("1", "true", "on", "yes")
|
||||
|
||||
|
||||
def _prepare_review_entry_images(
|
||||
conn: Any,
|
||||
trade_id: int,
|
||||
data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""客户端只提交手动槽位;未重新勾选自动K线时保留已有 auto 附图."""
|
||||
from lib.options.options_review_chart_async_lib import OPTIONS_REVIEW_AUTO_CHART_TF
|
||||
from lib.options.options_review_images_lib import parse_options_review_images_json
|
||||
|
||||
images = data.get("images") if isinstance(data.get("images"), list) else []
|
||||
cleaned: list[dict[str, str]] = []
|
||||
for item in images:
|
||||
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
|
||||
cleaned.append({"tf": tf, "file": file})
|
||||
|
||||
want_chart = _truthy(data.get("journal_exchange_chart")) and not cleaned
|
||||
if not want_chart:
|
||||
row = conn.execute(
|
||||
"SELECT images_json FROM options_review_entries WHERE trade_id=?",
|
||||
(int(trade_id),),
|
||||
).fetchone()
|
||||
if row:
|
||||
for item in parse_options_review_images_json(row["images_json"]):
|
||||
if str(item.get("tf") or "").strip() == OPTIONS_REVIEW_AUTO_CHART_TF:
|
||||
cleaned.insert(0, dict(item))
|
||||
break
|
||||
|
||||
out = dict(data)
|
||||
out["images"] = cleaned
|
||||
return out
|
||||
|
||||
|
||||
def _maybe_schedule_options_review_chart(
|
||||
cfg: dict[str, Any],
|
||||
trade: dict[str, Any],
|
||||
data: dict[str, Any],
|
||||
) -> bool:
|
||||
"""纯期权 + 无手动截图 + 勾选自动K线 → 后台生成标的永续双周期拼图(只钉进/出场)."""
|
||||
if str(trade.get("source_type") or "").strip() != "option_spot":
|
||||
return False
|
||||
images = data.get("images") if isinstance(data.get("images"), list) else []
|
||||
has_manual = any(str((x or {}).get("file") or "").strip() for x in images)
|
||||
if has_manual or not _truthy(data.get("journal_exchange_chart")):
|
||||
return False
|
||||
|
||||
app_module = cfg.get("app_module")
|
||||
if app_module is None:
|
||||
return False
|
||||
if not bool(getattr(app_module, "ORDER_CHART_ENABLED", True)):
|
||||
return False
|
||||
generate_fn = getattr(app_module, "generate_multi_timeframe_chart_png", None)
|
||||
if not callable(generate_fn):
|
||||
return False
|
||||
|
||||
from lib.instance.journal_chart_lib import (
|
||||
parse_journal_chart_anchor,
|
||||
parse_journal_chart_limit,
|
||||
parse_journal_chart_timeframes,
|
||||
)
|
||||
from lib.options.options_review_chart_async_lib import (
|
||||
schedule_options_review_exchange_chart,
|
||||
underlying_to_okx_perp_symbol,
|
||||
)
|
||||
from lib.options.options_review_images_lib import options_review_upload_dir
|
||||
from lib.options.options_review_lib import _now_str
|
||||
|
||||
fallback_tfs = list(getattr(app_module, "ORDER_CHART_TFS", None) or [])[:2]
|
||||
journal_tfs = parse_journal_chart_timeframes(
|
||||
data.get("journal_chart_tf1"),
|
||||
data.get("journal_chart_tf2"),
|
||||
fallback_tfs or None,
|
||||
)
|
||||
journal_limit = parse_journal_chart_limit(
|
||||
data.get("journal_chart_limit"),
|
||||
int(getattr(app_module, "ORDER_CHART_LIMIT", 100) or 100),
|
||||
)
|
||||
chart_anchor = parse_journal_chart_anchor(data.get("journal_chart_anchor"))
|
||||
|
||||
underlying = str(trade.get("underlying") or data.get("underlying") or "").strip()
|
||||
exchange_symbol = underlying_to_okx_perp_symbol(
|
||||
underlying,
|
||||
normalize_symbol_input_fn=app_module.normalize_symbol_input,
|
||||
normalize_okx_symbol_fn=app_module.normalize_okx_symbol,
|
||||
)
|
||||
if not exchange_symbol:
|
||||
return False
|
||||
|
||||
to_ms = getattr(app_module, "_local_input_datetime_to_ms", None)
|
||||
if not callable(to_ms):
|
||||
return False
|
||||
entry_ts = to_ms(trade.get("opened_at"))
|
||||
exit_ts = to_ms(trade.get("closed_at"))
|
||||
if entry_ts is None and exit_ts is None:
|
||||
return False
|
||||
|
||||
now_ms = None
|
||||
app_now = getattr(app_module, "app_now", None)
|
||||
if callable(app_now):
|
||||
try:
|
||||
now_ms = int(app_now().timestamp() * 1000)
|
||||
except Exception:
|
||||
now_ms = None
|
||||
|
||||
inst = str(trade.get("inst_id") or underlying or f"#{trade.get('id') or ''}").strip()
|
||||
upload_folder = options_review_upload_dir(cfg["upload_folder"])
|
||||
schedule_options_review_exchange_chart(
|
||||
trade_id=int(trade["id"]),
|
||||
exchange_symbol=exchange_symbol,
|
||||
title_prefix=f"{underlying or 'OPT'} {inst} review",
|
||||
journal_tfs=journal_tfs,
|
||||
journal_limit=journal_limit,
|
||||
marker_payload={
|
||||
"entry_ts_ms": entry_ts,
|
||||
"exit_ts_ms": exit_ts,
|
||||
# 只按时间钉进/出场,不传价格与止损
|
||||
"chart_anchor": chart_anchor,
|
||||
"now_ts_ms": now_ms,
|
||||
},
|
||||
upload_folder=upload_folder,
|
||||
generate_chart_fn=generate_fn,
|
||||
get_db_fn=cfg["get_db"],
|
||||
now_str_fn=_now_str,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: str) -> None:
|
||||
lr = cfg["login_required"]
|
||||
|
||||
@@ -216,9 +351,16 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
|
||||
return jsonify({"ok": False, "msg": "trade_id 无效"}), 400
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
data = _prepare_review_entry_images(conn, trade_id, data)
|
||||
out = save_review_entry(conn, trade_id, data)
|
||||
if out.get("ok"):
|
||||
conn.commit()
|
||||
chart_queued = _maybe_schedule_options_review_chart(
|
||||
cfg, out.get("trade") or {}, data
|
||||
)
|
||||
if chart_queued:
|
||||
out = dict(out)
|
||||
out["chart_queued"] = True
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user