Files
crypto_monitor/lib/options/options_review_register.py
T
dekun 10128d18bc Add OKX options review module with hedge plan entries.
Import closed OKX option history and closed hedge plans into one list for journaling, images, and stats without mixing contract reviews.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 09:35:19 +08:00

233 lines
9.1 KiB
Python

"""OKX 期权复盘模块:Flask 路由注册(含对冲计划级复盘)."""
from __future__ import annotations
import os
from typing import Any
from flask import Flask, jsonify, request, send_file
from jinja2 import ChoiceLoader, FileSystemLoader
from werkzeug.utils import secure_filename
from lib.options.options_review_db import SOURCE_TYPES, init_options_review_tables
from lib.options.options_review_images_lib import (
OPTIONS_REVIEW_UPLOAD_TFS,
normalize_options_review_draft_id,
options_review_image_paths,
options_review_upload_dir,
save_options_review_slot_file,
)
from lib.options.options_review_lib import (
SOURCE_LABELS,
compute_review_stats,
delete_review_entry,
get_review_trade,
list_review_trades,
save_review_entry,
sync_all_review_sources,
)
def attach_options_review_templates(app: Flask, repo_root: str) -> None:
tpl_dir = os.path.join(repo_root, "lib", "options", "templates")
if not os.path.isdir(tpl_dir):
return
existing = app.jinja_loader
loaders = [FileSystemLoader(tpl_dir)]
if existing is not None:
if isinstance(existing, ChoiceLoader):
loaders = list(existing.loaders) + loaders
else:
loaders.insert(0, existing)
app.jinja_loader = ChoiceLoader(loaders)
def install_options_review(app: Flask, repo_root: str, app_module: Any) -> None:
attach_options_review_templates(app, repo_root)
cfg = {
"get_db": app_module.get_db,
"login_required": app_module.login_required,
"exchange_options": getattr(app_module, "exchange_options", None),
"render_main_page": app_module.render_main_page,
"upload_folder": getattr(app_module, "UPLOAD_FOLDER", None)
or os.path.join(os.path.dirname(getattr(app_module, "BASE_DIR", repo_root)), "static", "images"),
"options_enabled": bool(getattr(app_module, "OKX_OPTIONS_ENABLED", False)),
"app_module": app_module,
}
app.extensions["options_review_cfg"] = cfg
register_options_review_routes(app, cfg, repo_root)
def _require_ex(cfg: dict[str, Any]):
from lib.exchange.okx_options_lib import options_api_ready
if not cfg.get("options_enabled"):
return None, "期权模块未启用"
ex = cfg.get("exchange_options")
ok, reason = options_api_ready(ex)
if not ok:
return None, reason or "期权 API 未配置"
return ex, ""
def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: str) -> None:
lr = cfg["login_required"]
@app.route("/options/review")
@lr
def options_review_page():
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
redir = redirect_to_embed_shell_if_enabled("options_review")
if redir is not None:
return redir
return cfg["render_main_page"]("options_review")
@app.route("/static/options_review.js")
@lr
def static_options_review_js():
path = os.path.join(repo_root, "lib", "common", "static", "options_review.js")
if not os.path.isfile(path):
return ("not found", 404)
return send_file(path, mimetype="application/javascript; charset=utf-8")
@app.route("/static/images/options_journal/<path:filename>")
@lr
def static_options_review_image(filename: str):
folder = options_review_upload_dir(cfg["upload_folder"])
safe = os.path.basename(filename or "")
path = os.path.join(folder, safe)
if not os.path.isfile(path):
return ("not found", 404)
return send_file(path)
@app.route("/api/options/review/sync", methods=["POST"])
@lr
def api_options_review_sync():
conn = cfg["get_db"]()
try:
init_options_review_tables(conn)
ex, err = _require_ex(cfg)
# 对冲可无交易所密钥;期权历史需要密钥
result = sync_all_review_sources(conn, ex if ex is not None else None)
if ex is None and result.get("options"):
result["options"] = {"ok": False, "msg": err}
conn.commit()
return jsonify(result)
finally:
conn.close()
@app.route("/api/options/review/trades")
@lr
def api_options_review_trades():
conn = cfg["get_db"]()
try:
items = list_review_trades(
conn,
source_type=(request.args.get("source_type") or "").strip() or None,
underlying=(request.args.get("underlying") or "").strip() or None,
opt_type=(request.args.get("opt_type") or "").strip() or None,
strategy_tag=(request.args.get("strategy_tag") or "").strip() or None,
reviewed=(request.args.get("reviewed") or "").strip() or None,
include_hedge_legs=(request.args.get("include_hedge_legs") or "").strip().lower()
in ("1", "true", "yes"),
closed_from=(request.args.get("closed_from") or "").strip() or None,
closed_to=(request.args.get("closed_to") or "").strip() or None,
limit=min(500, max(1, int(request.args.get("limit") or 200))),
offset=max(0, int(request.args.get("offset") or 0)),
)
return jsonify({"ok": True, "trades": items, "source_labels": SOURCE_LABELS})
finally:
conn.close()
@app.route("/api/options/review/trades/<int:trade_id>")
@lr
def api_options_review_trade_detail(trade_id: int):
conn = cfg["get_db"]()
try:
item = get_review_trade(conn, trade_id)
if not item:
return jsonify({"ok": False, "msg": "未找到"}), 404
return jsonify({"ok": True, "trade": item})
finally:
conn.close()
@app.route("/api/options/review/entry", methods=["POST"])
@lr
def api_options_review_entry_save():
data = request.get_json(silent=True) or {}
try:
trade_id = int(data.get("trade_id"))
except (TypeError, ValueError):
return jsonify({"ok": False, "msg": "trade_id 无效"}), 400
conn = cfg["get_db"]()
try:
out = save_review_entry(conn, trade_id, data)
if out.get("ok"):
conn.commit()
return jsonify(out), (200 if out.get("ok") else 400)
finally:
conn.close()
@app.route("/api/options/review/entry/<int:trade_id>", methods=["DELETE"])
@lr
def api_options_review_entry_delete(trade_id: int):
conn = cfg["get_db"]()
try:
out = delete_review_entry(conn, trade_id)
if out.get("ok"):
entry = out.get("entry") or {}
folder = options_review_upload_dir(cfg["upload_folder"])
for path in options_review_image_paths(entry, folder):
try:
os.remove(path)
except OSError:
pass
conn.commit()
return jsonify(out), (200 if out.get("ok") else 400)
finally:
conn.close()
@app.route("/api/options/review/upload_slot", methods=["POST"])
@lr
def api_options_review_upload_slot():
draft_id = normalize_options_review_draft_id(
request.form.get("draft_id") if request.form else None
)
tf = str((request.form.get("tf") if request.form else None) or "").strip()
if not draft_id:
return jsonify({"ok": False, "error": "invalid draft_id"}), 400
if tf not in OPTIONS_REVIEW_UPLOAD_TFS:
return jsonify({"ok": False, "error": "invalid tf"}), 400
f = request.files.get("file") if request.files else None
if not f or not getattr(f, "filename", None):
return jsonify({"ok": False, "error": "no file"}), 400
folder = options_review_upload_dir(cfg["upload_folder"])
item = save_options_review_slot_file(
f, draft_id, tf, folder, secure_filename_fn=secure_filename
)
if not item:
return jsonify({"ok": False, "error": "save failed"}), 500
return jsonify({"ok": True, "tf": tf, "file": item["file"]})
@app.route("/api/options/review/stats")
@lr
def api_options_review_stats():
conn = cfg["get_db"]()
try:
stats = compute_review_stats(
conn,
source_type=(request.args.get("source_type") or "").strip() or None,
underlying=(request.args.get("underlying") or "").strip() or None,
include_hedge_legs=(request.args.get("include_hedge_legs") or "").strip().lower()
in ("1", "true", "yes"),
closed_from=(request.args.get("closed_from") or "").strip() or None,
closed_to=(request.args.get("closed_to") or "").strip() or None,
require_strategy=(request.args.get("require_strategy") or "").strip().lower()
in ("1", "true", "yes"),
)
stats["source_types"] = list(SOURCE_TYPES)
stats["source_labels"] = SOURCE_LABELS
return jsonify(stats)
finally:
conn.close()