diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 0000000..0d6be71 Binary files /dev/null and b/docs/architecture.png differ diff --git a/docs/architecture_diagram.py b/docs/architecture_diagram.py new file mode 100644 index 0000000..53565cd --- /dev/null +++ b/docs/architecture_diagram.py @@ -0,0 +1,199 @@ +"""ForexBot2 architecture diagram → docs/architecture.png. + +Regenerate after architecture changes: venv/bin/python docs/architecture_diagram.py +(matplotlib only — no graphviz on the dev box).""" +import os +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.patches import FancyBboxPatch, FancyArrowPatch + +W, H = 100.0, 100.0 +fig, ax = plt.subplots(figsize=(16, 11.5)) +ax.set_xlim(0, W); ax.set_ylim(0, H); ax.axis("off") + +INK = "#263238"; MUT = "#607D8B"; ARR = "#455A64"; HAUL = "#7B8FA1" + +def box(x, y, w, h, title, lines, fc, ec, title_fs=8.2, body_fs=6.6, lw=1.3): + ax.add_patch(FancyBboxPatch((x, y), w, h, + boxstyle="round,pad=0.35,rounding_size=0.9", + facecolor=fc, edgecolor=ec, linewidth=lw, zorder=2)) + cy = y + h - 1.9 + ax.text(x + w / 2, cy, title, ha="center", va="center", fontsize=title_fs, + fontweight="bold", color=INK, zorder=3) + cy -= 2.3 + for ln in lines: + ax.text(x + w / 2, cy, ln, ha="center", va="center", fontsize=body_fs, + color=INK, zorder=3) + cy -= 1.85 + +def band(x, y, w, h, label, color): + ax.add_patch(FancyBboxPatch((x, y), w, h, + boxstyle="round,pad=0.3,rounding_size=1.2", + facecolor="none", edgecolor=color, linewidth=1.0, + linestyle=(0, (4, 3)), zorder=1)) + ax.text(x + 0.9, y + h - 1.4, label, ha="left", va="center", + fontsize=7.2, fontweight="bold", color=color, zorder=3) + +def arrow(p1, p2, color=ARR, lw=1.5, style="-|>", conn="arc3,rad=0.0", + label=None, lx=0, ly=0, ls="solid", fs=6.4, rot=0): + ax.add_patch(FancyArrowPatch(p1, p2, arrowstyle=style, mutation_scale=11, + color=color, linewidth=lw, linestyle=ls, + connectionstyle=conn, zorder=4)) + if label: + mx, my = (p1[0] + p2[0]) / 2 + lx, (p1[1] + p2[1]) / 2 + ly + ax.text(mx, my, label, fontsize=fs, color=MUT, ha="center", + va="center", zorder=5, rotation=rot, + bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none", alpha=0.9)) + +# ── title ──────────────────────────────────────────────────────────── +ax.text(2, 98.4, "ForexBot2 — XAU/USD Signal-Intelligence Platform", + fontsize=15, fontweight="bold", color=INK, ha="left") +ax.text(2, 96.0, "one pipeline, four delivery surfaces · closed bars only · live == backtest parity", + fontsize=8.5, color=MUT, ha="left") + +EXT_FC, EXT_EC = "#ECEFF1", "#90A4AE" +DAT_FC, DAT_EC = "#E3F2FD", "#5C9BD1" +NWS_FC, NWS_EC = "#E0F2F1", "#3D9C8F" +COR_FC, COR_EC = "#FFFDF5", "#C9A227" +REP_FC, REP_EC = "#FFE9A8", "#B8860B" +DEL_FC, DEL_EC = "#E8F5E9", "#5FAF63" +BTS_FC, BTS_EC = "#EDE7F6", "#8E6BC6" + +# ── external services (top band) ───────────────────────────────────── +band(1, 85.0, 98, 9.4, "EXTERNAL SERVICES", "#78909C") +box(2.5, 86.2, 15.5, 6.4, "Oanda v20 API", + ["candles (live feed)", "practice orders"], EXT_FC, EXT_EC) +box(19.5, 86.2, 15.5, 6.4, "Twelve Data · Tiingo", + ["registered fallbacks", "(Tiingo: tick stream)"], EXT_FC, EXT_EC) +box(36.5, 86.2, 15.5, 6.4, "Forex Factory", + ["economic calendar", "MyFXBook RSS fallback"], EXT_FC, EXT_EC) +box(53.5, 86.2, 12.5, 6.4, "News RSS ×4", + ["gold / macro", "headlines"], EXT_FC, EXT_EC) +box(67.5, 86.2, 13.5, 6.4, "DeepSeek API", + ["V4-Flash LLM", "thinking: disabled"], EXT_FC, EXT_EC) +box(82.5, 86.2, 15, 6.4, "Telegram API", + ["owner bot +", "subscriber channel"], EXT_FC, EXT_EC) + +# ── data & context layer ───────────────────────────────────────────── +box(2.5, 69.8, 33, 11.0, "providers/ (BaseDataProvider)", + ["single-flight fetch · TTL+boundary cache", + "forming-candle DROP · validate/clean", + "paginated fetch_range · optional tick stream", + "active feed knob: data_providers.active = oanda"], + DAT_FC, DAT_EC) +box(38.5, 69.8, 33, 11.0, "news_engine.py + llm_client.py", + ["calendar events + keyword sentiment (gold-inverse)", + "DeepSeek enrichment: batch headline scores,", + "event gold-direction, market blurb — display-only,", + "keyword fallback, daily request cap"], + NWS_FC, NWS_EC) +box(74.5, 69.8, 23, 11.0, "app_config.py", + ["credentials & deployment:", + "env → config.json → refuse", + "(no hardcoded fallbacks)", + ".env loaded at import"], + "#FAFAFA", "#9E9E9E") + +# ── signal core container ──────────────────────────────────────────── +ax.add_patch(FancyBboxPatch((2.5, 40.5), 95, 26.5, + boxstyle="round,pad=0.35,rounding_size=1.2", + facecolor=COR_FC, edgecolor=COR_EC, linewidth=1.6, zorder=1)) +ax.text(50, 65.2, "SIGNAL CORE — runs identically in live scans and backtests (closed bars only)", + ha="center", fontsize=8.4, fontweight="bold", color="#8a6d1a", zorder=3) + +box(4.5, 51.8, 28, 12.0, "engine.py — TechnicalEngine", + ["9 scored indicators + ATR context:", + "EMA · RSI · MACD · BB · ADX · ROC ·", + "Keltner · divergence · candlestick", + "regime → adaptive weights → composite"], + "white", COR_EC) +box(4.5, 42.0, 28, 8.4, "pair_signals/xauusd.py — factory", + ["ALWAYS used: config.json overlay +", + "validated per-TF deltas (hour blocks, trail…)"], + "white", COR_EC) +box(35.5, 42.0, 60, 20.7, "signal_aggregator.py — analyze_pair → analyze_from_dataframes", + ["multi-TF agreement (M5→[M15,H1] · M15→[M5,H1] · H1→[M15,H4] · H4→[H1,D1])", + "→ news context (warn-only by default) → diminishing-decay penalties", + "→ exhaustion hard block → quality grade A+…F", + "→ ordered post-grade gate chain (session · XAU SELL vol/hour · regime · M5 noise)", + "→ setup generators: XAU ATR (A+/A) · generic swing · XAU_PB · XAU_CP", + "→ entry · SL · TP1/TP2 · trailing plan"], + "white", COR_EC, body_fs=6.9) + +# ── SignalReport hub ───────────────────────────────────────────────── +box(31, 33.2, 38, 5.2, "SignalReport", + ["signal (−2…+2) · grade · trade setup · reasons · news summary"], + REP_FC, REP_EC, title_fs=8.8) + +# ── delivery surfaces ──────────────────────────────────────────────── +band(1, 13.2, 98, 17.2, "DELIVERY SURFACES", "#4C8F50") +box(2.5, 15.0, 12, 12.6, "main.py", + ["CLI report", "--detail", "--json"], DEL_FC, DEL_EC) +box(16, 15.0, 24, 12.6, "oanda_daemon.py + executor", + ["practice auto-execution", + "book-parity rules: no broker TP,", + "5-bar cooldown, once-per-bar", + "trail, OPEN_ONLY fills,", + "boot reconcile · orders → Oanda"], + DEL_FC, DEL_EC, title_fs=7.8) +box(42.5, 15.0, 31, 12.6, "dashboard_publisher.py → Apache", + ["independent daemon · scans at candle closes", + "atomic publish to /var/www/trade:", + "dashboard.html · snapshot.json · news.json", + "+ news_summary.php — on-demand AI", + "gold-impact summary (whitelist · cache · cap)"], + DEL_FC, DEL_EC) +box(76, 15.0, 21.5, 12.6, "telegram_bot.py", + ["candle-boundary auto-scans", "quality threshold + dedup", + "alerts + LLM context line", "WS level-breach monitor"], + DEL_FC, DEL_EC) + +# ── backtest / QA ──────────────────────────────────────────────────── +box(2.5, 3.2, 71.8, 8.0, "backtest/gpu_backtest.py — bar-by-bar replay through the SAME analyze_from_dataframes", + ["realism (results-critical): next-open entries · raw-path resolution · spread charged · SL-first ties · close-only trail", + "CuPy GPU / NumPy fallback · parquet cache · fed by providers.fetch_range (news off)"], + BTS_FC, BTS_EC) +box(77.5, 3.2, 20, 8.0, "test_all.py", + ["359 tests · 22 modules", "no network (mocked)"], "#FAFAFA", "#9E9E9E") + +# ── arrows: short structural hops (solid dark) ─────────────────────── +arrow((10, 86.2), (14, 81.2), label="OHLCV", lx=-3.4, ly=0.2) # oanda → providers +arrow((27, 86.2), (24, 81.2), ls=(0, (3, 2)), lw=1.1) # fallbacks → providers +arrow((44, 86.2), (48, 81.2)) # FF → news +arrow((59.5, 86.2), (56, 81.2)) # RSS → news +arrow((72, 86.2), (62, 81.2), label="LLM", lx=3.4, ly=0.6) # deepseek → news +arrow((19, 69.8), (19, 64.2), label="DataFrames (UTC, closed bars)", lx=0.2, ly=0.9) +arrow((33.5, 57.5), (35.5, 55.5)) # engine → aggregator +arrow((32.5, 46.0), (35.5, 48.0)) # factory → aggregator +arrow((55, 69.8), (55, 63.1), label="warn-only context", lx=0, ly=1.0) +arrow((86, 69.8), (86, 67.3), ls=(0, (3, 2)), lw=1.1) # config → core +arrow((50, 40.5), (50, 38.8)) # core → report +arrow((36, 34.5), (8.5, 27.95), conn="arc3,rad=0.12") # report → cli +arrow((40, 33.2), (28, 27.95), conn="arc3,rad=0.06") # report → daemon +arrow((60, 33.2), (58, 27.95)) # report → dashboard +arrow((66, 34.5), (86.5, 27.95), conn="arc3,rad=-0.12") # report → telegram + +# ── long-haul edges (thin dashed, routed through gutters) ──────────── +# telegram bot ↔ Telegram API — right-margin arc, clear of app_config +arrow((95, 86.2), (95, 27.95), conn="arc3,rad=0.15", color=HAUL, + lw=1.2, ls=(0, (4, 2))) +ax.text(99.2, 57, "alerts / commands", fontsize=6.4, color=MUT, + rotation=90, ha="center", va="center") +# browser click → news_summary.php → DeepSeek +# (oanda_daemon's order flow is noted inside its box — no long arc) +arrow((66, 27.95), (73, 86.2), conn="arc3,rad=-0.18", color=HAUL, + lw=1.2, ls=(0, (4, 2)), + label="on-demand AI summary\n(browser click → PHP)", lx=2.5, ly=11.6) +# providers.fetch_range → backtester — far-left margin channel +arrow((1.5, 69.8), (1.5, 11.0), color=BTS_EC, lw=1.1, ls=(0, (3, 2)), + label="fetch_range\n(history)", lx=3.6, ly=-1.2) +# backtester replays the signal core — right gutter between boxes +arrow((74.0, 11.6), (74.9, 40.3), color=HAUL, lw=1.3) +ax.text(81.8, 38.9, "replays the exact signal code", fontsize=6.4, + color=MUT, ha="center", va="center", zorder=5, + bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none", alpha=0.9)) + +out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "architecture.png") +fig.savefig(out, dpi=200, bbox_inches="tight", facecolor="white") +print(f"saved {out}")