Files
Chan/web/api/wyckoff_crypto.py
T
jackyu66gitandCursor ec08de098e feat(ECR-009): Crypto Wyckoff Screener 独立页(D/W/M)
移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:46:35 +08:00

119 lines
3.8 KiB
Python

"""Crypto Wyckoff Screener API + page (independent of /api/analyze)."""
from __future__ import annotations
import os
import threading
from flask import Blueprint, jsonify, render_template, request
from crypto_wyckoff.domain_models import DecisionSignal, WyckoffCycle, WyckoffEvent, WyckoffPhase
from crypto_wyckoff.scheduler import get_status, run_tick, start_scheduler
from crypto_wyckoff import store as wyckoff_store
from crypto_wyckoff.version import ARCHITECTURE_VERSION, WYCKOFF_ENGINE_VERSION
bp = Blueprint("wyckoff_crypto", __name__)
_scheduler_started = False
_sched_lock = threading.Lock()
def ensure_scheduler() -> None:
global _scheduler_started
with _sched_lock:
if _scheduler_started:
return
if os.environ.get("CRYPTO_WYCKOFF_DISABLE", "").lower() in ("1", "true", "yes"):
return
interval = int(os.environ.get("CRYPTO_WYCKOFF_INTERVAL", "60"))
max_sym = os.environ.get("CRYPTO_WYCKOFF_MAX_SYMBOLS")
max_symbols = int(max_sym) if max_sym else None
start_scheduler(interval_sec=interval, max_symbols=max_symbols)
_scheduler_started = True
@bp.route("/wyckoff_crypto")
def page():
ensure_scheduler()
return render_template("wyckoff_crypto.html")
@bp.route("/api/wyckoff_crypto/meta")
def meta():
ensure_scheduler()
latest = wyckoff_store.latest_trade_date()
return jsonify(
{
"architecture_version": ARCHITECTURE_VERSION,
"engine_version": WYCKOFF_ENGINE_VERSION,
"latest_trade_date": latest,
"scan_count": wyckoff_store.count_for_date(latest),
"cycles": [c.value for c in WyckoffCycle],
"phases": [p.value for p in WyckoffPhase],
"events": [e.value for e in WyckoffEvent],
"decision_signals": [s.value for s in DecisionSignal],
"timezone": "UTC",
"timeframes": ["1d", "1w", "1M"],
"status": get_status(),
}
)
@bp.route("/api/wyckoff_crypto/status")
def status():
ensure_scheduler()
return jsonify(get_status())
@bp.route("/api/wyckoff_crypto/scan")
def scan():
ensure_scheduler()
rows = wyckoff_store.query_scan(
trade_date=request.args.get("trade_date"),
m_cycle=request.args.get("m_cycle"),
w_phase=request.args.get("w_phase"),
d_event=request.args.get("d_event"),
decision_signal=request.args.get("decision_signal"),
min_overall_score=_float_or_none(request.args.get("min_overall_score")),
min_alignment=_float_or_none(request.args.get("min_alignment")),
sort=request.args.get("sort") or "overall_score",
limit=min(int(request.args.get("limit") or 100), 500),
offset=int(request.args.get("offset") or 0),
)
return jsonify({"rows": rows, "count": len(rows)})
@bp.route("/api/wyckoff_crypto/symbol/<path:symbol>")
def symbol_detail(symbol: str):
ensure_scheduler()
row = wyckoff_store.get_symbol(symbol, request.args.get("trade_date"))
if not row:
return jsonify({"error": "not_found"}), 404
return jsonify(row)
@bp.route("/api/wyckoff_crypto/tick", methods=["POST"])
def manual_tick():
"""Manual one-shot tick (debug). Optional JSON/query max_symbols."""
ensure_scheduler()
max_sym = request.args.get("max_symbols") or (request.json or {}).get("max_symbols")
max_symbols = int(max_sym) if max_sym else None
def _job():
try:
run_tick(max_symbols=max_symbols, force_rescan=True)
except Exception:
pass
threading.Thread(target=_job, daemon=True).start()
return jsonify({"ok": True, "started": True})
def _float_or_none(v):
if v in (None, ""):
return None
try:
return float(v)
except (TypeError, ValueError):
return None