docs: architecture diagram (PNG + matplotlib generator)
End-to-end map: external services → providers/news layer → signal core (engine + factory + aggregator) → SignalReport → four delivery surfaces, plus the backtester replaying the same core. Regenerate with venv/bin/python docs/architecture_diagram.py after architecture changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aq1jZ6jBsBdEC2xT3u46SA
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
@@ -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}")
|
||||||
Reference in New Issue
Block a user