"""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/") 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