移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。 Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.8 KiB
Python
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
|