diff --git a/ChanMacro/requirements.txt b/ChanMacro/requirements.txt index 6a07a7d..a04a603 100644 --- a/ChanMacro/requirements.txt +++ b/ChanMacro/requirements.txt @@ -5,3 +5,4 @@ pydantic>=2.0.0 requests>=2.31.0 python-dotenv>=1.0.0 scipy>=1.10.0 +flask>=3.0.0 diff --git a/ChanMacro/web/app.py b/ChanMacro/web/app.py new file mode 100644 index 0000000..be01938 --- /dev/null +++ b/ChanMacro/web/app.py @@ -0,0 +1,158 @@ +""" +web/app.py — ChanMacro dashboard (Flask, port 8124). +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from datetime import date as Date, timedelta +from flask import Flask, render_template, jsonify, request + +from database import get_connection +from config import config +from scoring.price_structure import PriceStructureScorer +from scoring.breadth_scorer import BreadthScorer +from scoring.oi_matrix import OIMatrixScorer +from scoring.volatility_regime import VolatilityRegimeScorer +from regime_detector import RegimeDetector +from models import MarketStateVector +from expectancy.engine import BayesianExpectancyEngine + +app = Flask(__name__) + + +def _build_state(target: Date): + """Shared: build MarketStateVector for a date.""" + ps = PriceStructureScorer().compute(target) + br = BreadthScorer().compute(target) + oi = OIMatrixScorer().compute(target) + vol = VolatilityRegimeScorer().compute(target) + + detector = RegimeDetector() + detector.load_state(config.db_path) + r = detector.detect(ps.score, br.breadth_top50, vol.vol_regime.value, target) + + state = MarketStateVector( + date=target, regime=r.regime, regime_confidence=r.confidence, + regime_version=r.regime_version, regime_maturity_score=r.maturity_score, + breadth_top20=br.breadth_top20, breadth_top30=br.breadth_top30, + breadth_top50=br.breadth_top50, breadth_bucket=br.breadth_bucket, + breadth_divergence=br.breadth_divergence, + oi_state=oi.oi_state, volatility_regime=vol.vol_regime, + price_structure_score=ps, breadth_score=br, + oi_matrix_score=oi, volatility_regime_score=vol, + ) + state.market_state_hash = state.compute_hash() + return state + + +@app.route("/") +def dashboard(): + return render_template("index.html") + + +@app.route("/api/state") +def api_state(): + """Current market state with all factor scores.""" + try: + target = Date.today() + state = _build_state(target) + return jsonify({ + "date": str(state.date), + "regime": state.regime.value, + "regime_confidence": state.regime_confidence, + "regime_maturity": state.regime_maturity_score, + "breadth": { + "score": state.breadth_score.score, + "bucket": state.breadth_bucket.value, + "top20": state.breadth_top20, + "top30": state.breadth_top30, + "top50": state.breadth_top50, + "divergence": state.breadth_divergence, + "narrative": state.breadth_score.narrative, + }, + "oi_state": state.oi_state.value, + "oi_score": state.oi_matrix_score.score, + "oi_narrative": state.oi_matrix_score.narrative, + "volatility": state.volatility_regime.value, + "price_structure": { + "score": state.price_structure_score.score, + "trend": state.price_structure_score.trend_strength, + "vol_comp": state.price_structure_score.volatility_compression, + "momentum": state.price_structure_score.momentum, + "label": state.price_structure_score.label, + "narrative": state.price_structure_score.narrative, + }, + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/history") +def api_history(): + """Regime and factor score history.""" + days = request.args.get("days", 60, type=int) + conn = get_connection() + + # Regime history + regimes = conn.execute( + "SELECT date, regime, confidence, maturity_score FROM regime_history ORDER BY date DESC LIMIT ?", + (days,) + ).fetchall() + + # Breadth history + breadth = conn.execute( + "SELECT date, advance_top50, decline_top50, above_ema20_top50 FROM breadth_daily ORDER BY date DESC LIMIT ?", + (days,) + ).fetchall() + + conn.close() + + return jsonify({ + "regimes": [{"date": r["date"], "regime": r["regime"], + "confidence": r["confidence"], "maturity": r["maturity_score"]} + for r in reversed(regimes)], + "breadth": [{"date": b["date"], "advance": b["advance_top50"], + "decline": b["decline_top50"], "above_ema20": b["above_ema20_top50"]} + for b in reversed(breadth)], + }) + + +@app.route("/api/expectancy") +def api_expectancy(): + """Query signal expectancy.""" + signal = request.args.get("signal", "B3") + try: + target = Date.today() + state = _build_state(target) + engine = BayesianExpectancyEngine(level_min_samples=5) + report = engine.estimate(state, signal_type=signal, target_date=target) + + layers = [] + for l in report.layers: + layers.append({ + "name": l.name, + "samples": l.samples, + "effective_samples": l.effective_samples, + "raw_winrate": l.raw_winrate, + "posterior_winrate": l.posterior_winrate, + "avg_return": l.avg_return, + }) + + return jsonify({ + "signal": signal, + "final_estimate": report.final_estimate, + "sufficiency": report.sufficiency.value, + "source": report.source, + "avg_return_7d": report.avg_return_7d, + "profit_factor": report.profit_factor, + "max_adverse": report.max_adverse_excursion, + "layers": layers, + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8124, debug=True) diff --git a/ChanMacro/web/static/js/dashboard.js b/ChanMacro/web/static/js/dashboard.js new file mode 100644 index 0000000..cb0b35e --- /dev/null +++ b/ChanMacro/web/static/js/dashboard.js @@ -0,0 +1,160 @@ +// dashboard.js — ChanMacro dashboard + +let regimeChart = null, breadthChart = null; + +const REGIME_COLORS = { TREND: "#3fb950", RANGE: "#d29922", PANIC: "#f85149" }; +const BUCKET_CLASS = { EXTREME: "bucket-EXTREME", STRONG: "bucket-STRONG", + NORMAL: "bucket-NORMAL", WEAK: "bucket-WEAK", PANIC: "bucket-PANIC" }; + +async function loadState() { + try { + const r = await fetch("/api/state"); + const d = await r.json(); + if (d.error) { document.getElementById("db-status").textContent = d.error; return; } + + document.getElementById("db-status").textContent = "✓ " + d.date; + document.getElementById("update-time").textContent = "更新于 " + new Date().toLocaleTimeString(); + + // Hero + const regime = d.regime; + document.getElementById("hero-regime").textContent = regime === "TREND" ? "趋势" : regime === "RANGE" ? "震荡" : "恐慌"; + document.getElementById("hero-regime").className = "hero-regime regime-" + regime; + document.getElementById("hero-badge").textContent = regime; + document.getElementById("hero-badge").className = "badge-regime badge-" + regime; + document.getElementById("hero-conf").textContent = (d.regime_confidence * 100).toFixed(0) + "%"; + document.getElementById("hero-maturity").textContent = d.regime_maturity.toFixed(0) + "/100"; + + // Factors + document.getElementById("f-price").textContent = d.price_structure.score.toFixed(0); + document.getElementById("f-price-sub").textContent = d.price_structure.label; + document.getElementById("f-price-narr").textContent = d.price_structure.narrative; + + const b = d.breadth; + document.getElementById("f-breadth").textContent = b.score.toFixed(0); + document.getElementById("f-breadth").className = "factor-value " + (BUCKET_CLASS[b.bucket] || ""); + document.getElementById("f-breadth-sub").textContent = + `${b.bucket} · T20=${b.top20.toFixed(0)} T50=${b.top50.toFixed(0)} div=${b.divergence > 0 ? "+" : ""}${b.divergence.toFixed(0)}`; + document.getElementById("f-breadth-narr").textContent = b.narrative; + + document.getElementById("f-oi").textContent = d.oi_state; + document.getElementById("f-oi-sub").textContent = `分数: ${d.oi_score.toFixed(0)}`; + document.getElementById("f-oi-narr").textContent = d.oi_narrative; + + document.getElementById("f-vol").textContent = d.volatility; + document.getElementById("f-vol-sub").textContent = `分数: ${d.price_structure.score.toFixed(0)}`; + } catch (e) { + document.getElementById("db-status").textContent = "连接失败"; + } +} + +async function loadHistory() { + try { + const r = await fetch("/api/history?days=60"); + const d = await r.json(); + + // Regime chart + const dates = d.regimes.map(x => x.date); + const regimes = d.regimes.map(x => x.regime); + const colors = regimes.map(r => REGIME_COLORS[r] || "#8b949e"); + + if (regimeChart) regimeChart.destroy(); + const ctx1 = document.getElementById("chart-regime").getContext("2d"); + regimeChart = new Chart(ctx1, { + type: "bar", + data: { + labels: dates, + datasets: [{ + label: "置信度", + data: d.regimes.map(x => x.confidence * 100), + backgroundColor: colors, + borderWidth: 0, + borderRadius: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: ctx => `${d.regimes[ctx.dataIndex].regime} · ${ctx.raw.toFixed(0)}%` + } + } + }, + scales: { + x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } }, + y: { max: 100, ticks: { color: "#8b949e" } } + } + } + }); + + // Breadth chart + if (breadthChart) breadthChart.destroy(); + const ctx2 = document.getElementById("chart-breadth").getContext("2d"); + breadthChart = new Chart(ctx2, { + type: "line", + data: { + labels: d.breadth.map(x => x.date), + datasets: [ + { label: "上涨", data: d.breadth.map(x => x.advance), borderColor: "#3fb950", + backgroundColor: "rgba(63,185,80,0.1)", fill: true, tension: 0.3, pointRadius: 0 }, + { label: "下跌", data: d.breadth.map(x => x.decline), borderColor: "#f85149", + backgroundColor: "rgba(248,81,73,0.1)", fill: true, tension: 0.3, pointRadius: 0 }, + { label: ">EMA20", data: d.breadth.map(x => x.above_ema20), borderColor: "#58a6ff", + borderDash: [4, 2], tension: 0.3, pointRadius: 0 }, + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { labels: { color: "#8b949e", usePointStyle: true, boxWidth: 8 } } }, + scales: { + x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } }, + y: { max: 50, ticks: { color: "#8b949e" } } + } + } + }); + } catch (e) { + console.error("History load failed:", e); + } +} + +async function loadExpectancy() { + const signal = document.getElementById("exp-signal").value; + try { + const r = await fetch(`/api/expectancy?signal=${signal}`); + const d = await r.json(); + if (d.error) { document.getElementById("exp-layers").innerHTML = `
| 层级 | 样本 | 有效样本 | 原始胜率 | 后验胜率 | 平均收益 |
|---|