RayMelius Claude Opus 4.6 commited on
Commit
8311583
Β·
1 Parent(s): 2eb2bc5

Add developer guide PDF with flow diagrams

Browse files

16-page guide covering system architecture, Kafka message flow, MDF regime
engine, RL agent observation space, AI strategy decision flow, session
lifecycle, database schema, and configuration reference. Includes 6
matplotlib-generated flow diagrams. Regenerate with: python docs/generate_dev_guide.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. docs/generate_dev_guide.py +985 -0
docs/generate_dev_guide.py ADDED
@@ -0,0 +1,985 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate StockEx Developer's Guide PDF with flow diagrams."""
3
+
4
+ import os
5
+ import sys
6
+ import textwrap
7
+ from io import BytesIO
8
+
9
+ import matplotlib
10
+ matplotlib.use("Agg")
11
+ import matplotlib.pyplot as plt
12
+ import matplotlib.patches as mpatches
13
+ from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
14
+ import numpy as np
15
+
16
+ from fpdf import FPDF
17
+
18
+ OUT_DIR = os.path.dirname(os.path.abspath(__file__))
19
+ OUT_FILE = os.path.join(OUT_DIR, "StockEx_Developer_Guide.pdf")
20
+
21
+ # ── Colours ───────────────────────────────────────────────────────────────────
22
+ C_PRIMARY = "#1a237e"
23
+ C_SECONDARY = "#283593"
24
+ C_ACCENT = "#42a5f5"
25
+ C_KAFKA = "#e65100"
26
+ C_SERVICE = "#1565c0"
27
+ C_DB = "#2e7d32"
28
+ C_AI = "#6a1b9a"
29
+ C_RL = "#00695c"
30
+ C_LLM = "#bf360c"
31
+ C_BG = "#f5f5f5"
32
+ C_LIGHT = "#e3f2fd"
33
+
34
+
35
+ def hex_to_rgb(h):
36
+ h = h.lstrip("#")
37
+ return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
38
+
39
+
40
+ # ── Diagram helpers ───────────────────────────────────────────────────────────
41
+
42
+ def save_fig_to_bytes(fig) -> bytes:
43
+ buf = BytesIO()
44
+ fig.savefig(buf, format="png", dpi=180, bbox_inches="tight", facecolor="white")
45
+ plt.close(fig)
46
+ buf.seek(0)
47
+ return buf.read()
48
+
49
+
50
+ def draw_box(ax, x, y, w, h, label, color=C_SERVICE, fontsize=8, sublabel=None):
51
+ box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.02",
52
+ facecolor=color, edgecolor="#333", linewidth=0.8, alpha=0.85)
53
+ ax.add_patch(box)
54
+ if sublabel:
55
+ ax.text(x + w/2, y + h*0.62, label, ha="center", va="center",
56
+ fontsize=fontsize, fontweight="bold", color="white")
57
+ ax.text(x + w/2, y + h*0.32, sublabel, ha="center", va="center",
58
+ fontsize=fontsize-1.5, color="#ddd")
59
+ else:
60
+ ax.text(x + w/2, y + h/2, label, ha="center", va="center",
61
+ fontsize=fontsize, fontweight="bold", color="white")
62
+
63
+
64
+ def draw_arrow(ax, x1, y1, x2, y2, color="#555", style="->"):
65
+ ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
66
+ arrowprops=dict(arrowstyle=style, color=color, lw=1.2))
67
+
68
+
69
+ def draw_arrow_label(ax, x1, y1, x2, y2, label, color="#555"):
70
+ draw_arrow(ax, x1, y1, x2, y2, color)
71
+ mx, my = (x1+x2)/2, (y1+y2)/2
72
+ ax.text(mx, my + 0.02, label, ha="center", va="bottom", fontsize=5.5, color=color)
73
+
74
+
75
+ # ── Diagram 1: System Architecture ────────────────────────────────────────────
76
+
77
+ def diagram_architecture():
78
+ fig, ax = plt.subplots(1, 1, figsize=(10, 6.5))
79
+ ax.set_xlim(0, 1)
80
+ ax.set_ylim(0, 1)
81
+ ax.axis("off")
82
+ ax.set_title("System Architecture β€” Single Container", fontsize=12,
83
+ fontweight="bold", color=C_PRIMARY, pad=15)
84
+
85
+ # nginx
86
+ draw_box(ax, 0.30, 0.90, 0.40, 0.07, "nginx :7860", "#546e7a", 9, "Reverse Proxy")
87
+
88
+ # Row 1: input services
89
+ draw_box(ax, 0.02, 0.74, 0.18, 0.10, "MD Feeder", C_SERVICE, 8, "Regime Engine")
90
+ draw_box(ax, 0.22, 0.74, 0.15, 0.10, "FIX OEG", C_SERVICE, 8, ":5001")
91
+ draw_box(ax, 0.39, 0.74, 0.15, 0.10, "FIX UI", C_SERVICE, 8, ":5002")
92
+ draw_box(ax, 0.56, 0.74, 0.15, 0.10, "Frontend", C_SERVICE, 8, ":5003")
93
+ draw_box(ax, 0.73, 0.74, 0.18, 0.10, "AI Analyst", C_AI, 8, "LLM")
94
+
95
+ # Kafka
96
+ draw_box(ax, 0.10, 0.55, 0.80, 0.10, "Apache Kafka (KRaft)", C_KAFKA, 10,
97
+ "orders | trades | snapshots | control | ai_insights")
98
+
99
+ # Row 2: core services
100
+ draw_box(ax, 0.05, 0.32, 0.20, 0.12, "Matcher", C_SERVICE, 9, ":6000 SQLite")
101
+ draw_box(ax, 0.30, 0.32, 0.25, 0.12, "Dashboard", C_SERVICE, 9, ":5000 SSE + OHLCV")
102
+ draw_box(ax, 0.60, 0.32, 0.28, 0.12, "Clearing House", C_RL, 9, ":5004 RL + LLM Members")
103
+
104
+ # Databases
105
+ draw_box(ax, 0.07, 0.17, 0.16, 0.07, "matcher.db", C_DB, 7, "SQLite")
106
+ draw_box(ax, 0.32, 0.17, 0.16, 0.07, "OHLCV.db", C_DB, 7, "SQLite")
107
+ draw_box(ax, 0.64, 0.17, 0.20, 0.07, "clearing_house.db", C_DB, 7, "SQLite")
108
+
109
+ # Browser
110
+ draw_box(ax, 0.30, 0.03, 0.40, 0.07, "Browser", "#37474f", 10, "SSE / REST")
111
+
112
+ # Arrows: nginx β†’ services
113
+ for x in [0.11, 0.295, 0.465, 0.635]:
114
+ draw_arrow(ax, 0.50, 0.90, x + 0.05, 0.84, "#78909c")
115
+
116
+ # Arrows: services β†’ kafka
117
+ for x in [0.11, 0.295, 0.465, 0.635, 0.82]:
118
+ draw_arrow(ax, x + 0.05, 0.74, x + 0.05, 0.65, C_KAFKA)
119
+
120
+ # Arrows: kafka β†’ consumers
121
+ draw_arrow(ax, 0.25, 0.55, 0.15, 0.44, C_KAFKA)
122
+ draw_arrow(ax, 0.50, 0.55, 0.42, 0.44, C_KAFKA)
123
+ draw_arrow(ax, 0.70, 0.55, 0.74, 0.44, C_KAFKA)
124
+
125
+ # Arrows: services β†’ DBs
126
+ draw_arrow(ax, 0.15, 0.32, 0.15, 0.24, C_DB)
127
+ draw_arrow(ax, 0.42, 0.32, 0.40, 0.24, C_DB)
128
+ draw_arrow(ax, 0.74, 0.32, 0.74, 0.24, C_DB)
129
+
130
+ # Dashboard β†’ browser
131
+ draw_arrow(ax, 0.42, 0.32, 0.50, 0.10, "#37474f")
132
+
133
+ return save_fig_to_bytes(fig)
134
+
135
+
136
+ # ── Diagram 2: Message Flow ──────────────────────────────────────────────────
137
+
138
+ def diagram_message_flow():
139
+ fig, ax = plt.subplots(1, 1, figsize=(10, 5))
140
+ ax.set_xlim(0, 1)
141
+ ax.set_ylim(0, 1)
142
+ ax.axis("off")
143
+ ax.set_title("Kafka Message Flow", fontsize=12, fontweight="bold",
144
+ color=C_PRIMARY, pad=15)
145
+
146
+ # Producers (left)
147
+ draw_box(ax, 0.02, 0.80, 0.16, 0.08, "MD Feeder", C_SERVICE, 7)
148
+ draw_box(ax, 0.02, 0.65, 0.16, 0.08, "FIX OEG", C_SERVICE, 7)
149
+ draw_box(ax, 0.02, 0.50, 0.16, 0.08, "Frontend", C_SERVICE, 7)
150
+ draw_box(ax, 0.02, 0.35, 0.16, 0.08, "Clearing House", C_RL, 7)
151
+ draw_box(ax, 0.02, 0.20, 0.16, 0.08, "Dashboard", C_SERVICE, 7)
152
+
153
+ # Kafka topics (center)
154
+ topics = ["orders", "trades", "snapshots", "control", "ai_insights"]
155
+ colors = ["#e65100", "#d84315", "#bf360c", "#ff6f00", "#f57c00"]
156
+ for i, (t, c) in enumerate(zip(topics, colors)):
157
+ y = 0.82 - i * 0.155
158
+ draw_box(ax, 0.35, y, 0.16, 0.07, t, c, 8)
159
+
160
+ # Consumers (right)
161
+ draw_box(ax, 0.68, 0.80, 0.16, 0.08, "Matcher", C_SERVICE, 7)
162
+ draw_box(ax, 0.68, 0.65, 0.16, 0.08, "Dashboard", C_SERVICE, 7)
163
+ draw_box(ax, 0.68, 0.50, 0.16, 0.08, "Clearing House", C_RL, 7)
164
+ draw_box(ax, 0.68, 0.35, 0.16, 0.08, "MD Feeder", C_SERVICE, 7)
165
+ draw_box(ax, 0.68, 0.20, 0.16, 0.08, "AI Analyst", C_AI, 7)
166
+
167
+ # Producer arrows
168
+ draw_arrow_label(ax, 0.18, 0.84, 0.35, 0.855, "orders+snap", C_KAFKA)
169
+ draw_arrow_label(ax, 0.18, 0.69, 0.35, 0.855, "orders", C_KAFKA)
170
+ draw_arrow_label(ax, 0.18, 0.54, 0.35, 0.855, "orders", C_KAFKA)
171
+ draw_arrow_label(ax, 0.18, 0.39, 0.35, 0.855, "orders", C_KAFKA)
172
+ draw_arrow(ax, 0.18, 0.24, 0.35, 0.545, "#ff6f00")
173
+
174
+ # Matcher β†’ trades
175
+ draw_arrow(ax, 0.68, 0.82, 0.51, 0.72, "#d84315")
176
+
177
+ # Consumer arrows
178
+ draw_arrow(ax, 0.51, 0.855, 0.68, 0.84, C_KAFKA) # orders β†’ matcher
179
+ draw_arrow(ax, 0.51, 0.82, 0.68, 0.69, C_KAFKA) # orders β†’ dashboard
180
+ draw_arrow(ax, 0.51, 0.70, 0.68, 0.69, "#d84315") # trades β†’ dashboard
181
+ draw_arrow(ax, 0.51, 0.70, 0.68, 0.54, "#d84315") # trades β†’ CH
182
+ draw_arrow(ax, 0.51, 0.545, 0.68, 0.39, "#ff6f00") # control β†’ MDF
183
+ draw_arrow(ax, 0.51, 0.545, 0.68, 0.54, "#ff6f00") # control β†’ CH
184
+ draw_arrow(ax, 0.51, 0.39, 0.68, 0.24, "#f57c00") # insights β†’ analyst
185
+
186
+ # Labels
187
+ ax.text(0.10, 0.95, "Producers", ha="center", fontsize=9, fontweight="bold", color="#555")
188
+ ax.text(0.43, 0.95, "Kafka Topics", ha="center", fontsize=9, fontweight="bold", color=C_KAFKA)
189
+ ax.text(0.76, 0.95, "Consumers", ha="center", fontsize=9, fontweight="bold", color="#555")
190
+
191
+ return save_fig_to_bytes(fig)
192
+
193
+
194
+ # ── Diagram 3: AI Strategy Decision Flow ─────────────────────────────────────
195
+
196
+ def diagram_ai_strategy():
197
+ fig, ax = plt.subplots(1, 1, figsize=(10, 6))
198
+ ax.set_xlim(0, 1)
199
+ ax.set_ylim(0, 1)
200
+ ax.axis("off")
201
+ ax.set_title("AI Trading Strategy β€” Decision Flow", fontsize=12,
202
+ fontweight="bold", color=C_PRIMARY, pad=15)
203
+
204
+ # Strategy selector
205
+ draw_box(ax, 0.30, 0.88, 0.40, 0.07, "CH_AI_STRATEGY", "#37474f", 9,
206
+ "hybrid (default) | rl | llm")
207
+
208
+ # Hybrid split
209
+ draw_box(ax, 0.10, 0.72, 0.35, 0.08, "RL Path (USR01-05)", C_RL, 8)
210
+ draw_box(ax, 0.55, 0.72, 0.35, 0.08, "LLM Path (USR06-10)", C_LLM, 8)
211
+
212
+ # RL pipeline
213
+ draw_box(ax, 0.02, 0.56, 0.20, 0.07, "Kafka Trades", C_KAFKA, 7)
214
+ draw_box(ax, 0.02, 0.44, 0.20, 0.07, "OHLCV Bars", C_RL, 7, "60-bar window")
215
+ draw_box(ax, 0.02, 0.32, 0.20, 0.07, "50 Indicators", C_RL, 7, "SMA/EMA/MACD/RSI/BB")
216
+ draw_box(ax, 0.02, 0.20, 0.20, 0.07, "Scaler", C_RL, 7, "StandardScaler")
217
+ draw_box(ax, 0.02, 0.08, 0.20, 0.07, "PPO Neural Net", C_RL, 7, "3008-dim β†’ action")
218
+
219
+ # RL output
220
+ draw_box(ax, 0.28, 0.08, 0.18, 0.07, "Hold/Buy/Sell\n+ size", "#004d40", 7)
221
+
222
+ # LLM pipeline
223
+ draw_box(ax, 0.58, 0.56, 0.20, 0.07, "Build Prompt", C_LLM, 7, "member state + BBOs")
224
+ draw_box(ax, 0.58, 0.44, 0.20, 0.07, "Groq API", C_LLM, 7, "free tier")
225
+ draw_box(ax, 0.58, 0.32, 0.20, 0.07, "HuggingFace", C_LLM, 7, "fallback #1")
226
+ draw_box(ax, 0.58, 0.20, 0.20, 0.07, "Ollama", C_LLM, 7, "fallback #2")
227
+ draw_box(ax, 0.58, 0.08, 0.20, 0.07, "Parse JSON", C_LLM, 7, "β†’ order dict")
228
+
229
+ # Rule-based fallback
230
+ draw_box(ax, 0.82, 0.08, 0.16, 0.07, "Rule-Based\nFallback", "#455a64", 7)
231
+
232
+ # Arrows - strategy selector
233
+ draw_arrow(ax, 0.40, 0.88, 0.27, 0.80, C_RL)
234
+ draw_arrow(ax, 0.60, 0.88, 0.72, 0.80, C_LLM)
235
+
236
+ # RL pipeline arrows
237
+ draw_arrow(ax, 0.12, 0.56, 0.12, 0.51, C_RL)
238
+ draw_arrow(ax, 0.12, 0.44, 0.12, 0.39, C_RL)
239
+ draw_arrow(ax, 0.12, 0.32, 0.12, 0.27, C_RL)
240
+ draw_arrow(ax, 0.12, 0.20, 0.12, 0.15, C_RL)
241
+ draw_arrow(ax, 0.22, 0.11, 0.28, 0.11, C_RL)
242
+
243
+ # LLM pipeline arrows
244
+ draw_arrow(ax, 0.68, 0.56, 0.68, 0.51, C_LLM)
245
+ draw_arrow(ax, 0.68, 0.44, 0.68, 0.39, C_LLM)
246
+ draw_arrow(ax, 0.68, 0.32, 0.68, 0.27, C_LLM)
247
+ draw_arrow(ax, 0.68, 0.20, 0.68, 0.15, C_LLM)
248
+
249
+ # Fallback arrows
250
+ draw_arrow(ax, 0.46, 0.11, 0.58, 0.11, "#888")
251
+ ax.text(0.52, 0.13, "fail?", ha="center", fontsize=6, color="#888")
252
+ draw_arrow(ax, 0.78, 0.11, 0.82, 0.11, "#888")
253
+ ax.text(0.80, 0.13, "fail?", ha="center", fontsize=6, color="#888")
254
+
255
+ # Submit
256
+ draw_box(ax, 0.35, 0.00, 0.30, 0.05, "β†’ Submit Order to Kafka 'orders'", C_KAFKA, 7)
257
+
258
+ return save_fig_to_bytes(fig)
259
+
260
+
261
+ # ── Diagram 4: MDF Regime Engine ─────────────────────────────────────────────
262
+
263
+ def diagram_mdf_regimes():
264
+ fig, ax = plt.subplots(1, 1, figsize=(10, 4.5))
265
+ ax.set_xlim(0, 1)
266
+ ax.set_ylim(0, 1)
267
+ ax.axis("off")
268
+ ax.set_title("MD Feeder β€” Regime-Based Price Dynamics", fontsize=12,
269
+ fontweight="bold", color=C_PRIMARY, pad=15)
270
+
271
+ # Regimes
272
+ regimes = [
273
+ ("trending_up", 0.04, "#1b5e20", "Positive drift\n+ momentum\nRSI ↑, MACD > 0"),
274
+ ("trending_down", 0.22, "#b71c1c", "Negative drift\n+ momentum\nRSI ↓, MACD < 0"),
275
+ ("mean_revert", 0.40, "#1565c0", "Pull to start\nprice\nRSI β†’ 50"),
276
+ ("volatile", 0.58, "#e65100", "Wide swings\nexpanded spread\nBB width ↑"),
277
+ ("calm", 0.76, "#37474f", "Tight range\nnarrow spread\nBB width ↓"),
278
+ ]
279
+ for name, x, color, desc in regimes:
280
+ draw_box(ax, x, 0.55, 0.16, 0.10, name, color, 7)
281
+ ax.text(x + 0.08, 0.48, desc, ha="center", va="top", fontsize=6,
282
+ color="#333", linespacing=1.4)
283
+
284
+ # Transition
285
+ draw_box(ax, 0.25, 0.20, 0.50, 0.07, "Auto-rotate every 15-50 ticks", "#546e7a", 8,
286
+ "Bias: mean_revert when price deviates > 15% from start")
287
+
288
+ # Arrows from regimes to transition
289
+ for name, x, _, _ in regimes:
290
+ draw_arrow(ax, x + 0.08, 0.55, 0.50, 0.27, "#999")
291
+
292
+ # Output
293
+ draw_box(ax, 0.15, 0.03, 0.70, 0.08, "Snapshot with Indicators: SMA(5,20), EMA(12,26), MACD, RSI(14), BB position, regime",
294
+ C_KAFKA, 7)
295
+ draw_arrow(ax, 0.50, 0.20, 0.50, 0.11, C_KAFKA)
296
+
297
+ return save_fig_to_bytes(fig)
298
+
299
+
300
+ # ── Diagram 5: RL Observation Space ──────────────────────────────────────────
301
+
302
+ def diagram_rl_observation():
303
+ fig, ax = plt.subplots(1, 1, figsize=(10, 4))
304
+ ax.set_xlim(0, 1)
305
+ ax.set_ylim(0, 1)
306
+ ax.axis("off")
307
+ ax.set_title("RL Agent β€” Observation Space (3,008 dimensions)", fontsize=12,
308
+ fontweight="bold", color=C_PRIMARY, pad=15)
309
+
310
+ # 60 bars
311
+ draw_box(ax, 0.02, 0.65, 0.20, 0.12, "60 OHLCV\nBars", C_RL, 8)
312
+
313
+ # 20 base features
314
+ draw_box(ax, 0.28, 0.75, 0.30, 0.12,
315
+ "20 Base Features", C_RL, 8,
316
+ "Close, Vol, SMA(5,10,20,50)\nEMA(12,26), RSI, MACD, Signal\nBB(upper,lower,width,pos)...")
317
+
318
+ # 30 lag features
319
+ draw_box(ax, 0.28, 0.55, 0.30, 0.12,
320
+ "30 Lag Features", C_RL, 8,
321
+ "Close, Vol, PriceChg, RSI\nMACD, Volatility\n@ lags 1,2,3,5,10")
322
+
323
+ # = 50 per bar
324
+ draw_box(ax, 0.65, 0.65, 0.14, 0.12, "50 features\nΓ— 60 bars\n= 3,000", "#004d40", 8)
325
+
326
+ # Portfolio
327
+ draw_box(ax, 0.65, 0.40, 0.14, 0.15, "8 Portfolio\nFeatures", "#004d40", 8,
328
+ "capital, qty, net_worth\nreturns, held_value...")
329
+
330
+ # Total
331
+ draw_box(ax, 0.84, 0.55, 0.14, 0.15, "3,008-dim\nObservation\nVector", C_PRIMARY, 9)
332
+
333
+ # Arrows
334
+ draw_arrow(ax, 0.22, 0.71, 0.28, 0.81, C_RL)
335
+ draw_arrow(ax, 0.22, 0.71, 0.28, 0.61, C_RL)
336
+ draw_arrow(ax, 0.58, 0.81, 0.65, 0.75, "#004d40")
337
+ draw_arrow(ax, 0.58, 0.61, 0.65, 0.67, "#004d40")
338
+ draw_arrow(ax, 0.79, 0.71, 0.84, 0.66, C_PRIMARY)
339
+ draw_arrow(ax, 0.79, 0.47, 0.84, 0.58, C_PRIMARY)
340
+
341
+ # PPO
342
+ draw_box(ax, 0.30, 0.15, 0.40, 0.12, "PPO MlpPolicy (~2.5 MB)", C_PRIMARY, 9,
343
+ "Forward pass β†’ [action_type (0-2), position_size (0-1)]")
344
+ draw_arrow(ax, 0.91, 0.55, 0.70, 0.27, C_PRIMARY)
345
+
346
+ # Output
347
+ draw_box(ax, 0.30, 0.00, 0.13, 0.07, "0: Hold", "#78909c", 7)
348
+ draw_box(ax, 0.45, 0.00, 0.11, 0.07, "1: Buy", "#1b5e20", 7)
349
+ draw_box(ax, 0.58, 0.00, 0.11, 0.07, "2: Sell", "#b71c1c", 7)
350
+ draw_arrow(ax, 0.50, 0.15, 0.37, 0.07, "#78909c")
351
+ draw_arrow(ax, 0.50, 0.15, 0.50, 0.07, "#1b5e20")
352
+ draw_arrow(ax, 0.50, 0.15, 0.63, 0.07, "#b71c1c")
353
+
354
+ return save_fig_to_bytes(fig)
355
+
356
+
357
+ # ── Diagram 6: Clearing House Lifecycle ──────────────────────────────────────
358
+
359
+ def diagram_ch_lifecycle():
360
+ fig, ax = plt.subplots(1, 1, figsize=(10, 4.5))
361
+ ax.set_xlim(0, 1)
362
+ ax.set_ylim(0, 1)
363
+ ax.axis("off")
364
+ ax.set_title("Clearing House β€” Session Lifecycle", fontsize=12,
365
+ fontweight="bold", color=C_PRIMARY, pad=15)
366
+
367
+ # Timeline
368
+ steps = [
369
+ (0.05, "Start of Day", "#1b5e20",
370
+ "Dashboard sends\n'start' to control\ntopic"),
371
+ (0.22, "MDF Generates\nOrders", C_SERVICE,
372
+ "Regime engine\npopulates books\nwith depth"),
373
+ (0.39, "CH AI Trades", C_RL,
374
+ "Every 45s per member\nRL / LLM / hybrid\n→ orders to Kafka"),
375
+ (0.56, "Trade Attribution", C_KAFKA,
376
+ "Trades topic:\ndetect USRxx- prefix\n→ record to CH DB"),
377
+ (0.73, "End of Day", "#b71c1c",
378
+ "Dashboard POSTs\n/ch/eod\nSettlement + P&L"),
379
+ ]
380
+ for x, label, color, desc in steps:
381
+ draw_box(ax, x, 0.60, 0.18, 0.12, label, color, 7)
382
+ ax.text(x + 0.09, 0.52, desc, ha="center", va="top", fontsize=6,
383
+ color="#333", linespacing=1.4)
384
+ if x < 0.73:
385
+ draw_arrow(ax, x + 0.18, 0.66, x + 0.21, 0.66, color)
386
+
387
+ # Bottom: obligation check
388
+ draw_box(ax, 0.15, 0.08, 0.70, 0.10, "Daily Obligation Check: β‰₯ 20 securities traded per member", "#e65100", 8,
389
+ "Members failing obligation flagged in EOD settlement report")
390
+
391
+ draw_arrow(ax, 0.50, 0.60, 0.50, 0.18, "#e65100")
392
+
393
+ return save_fig_to_bytes(fig)
394
+
395
+
396
+ # ── PDF Builder ───────────────────────────────────────────────────────────────
397
+
398
+ def _ascii(text: str) -> str:
399
+ """Replace Unicode chars with ASCII equivalents for fpdf core fonts."""
400
+ return (text
401
+ .replace("\u2014", "--") # em-dash
402
+ .replace("\u2013", "-") # en-dash
403
+ .replace("\u2019", "'") # right single quote
404
+ .replace("\u201c", '"') # left double quote
405
+ .replace("\u201d", '"') # right double quote
406
+ .replace("\u2192", "->") # β†’
407
+ .replace("\u2190", "<-") # ←
408
+ .replace("\u2191", "^") # ↑
409
+ .replace("\u2193", "v") # ↓
410
+ .replace("\u2265", ">=") # β‰₯
411
+ .replace("\u2264", "<=") # ≀
412
+ .replace("\u00d7", "x") # Γ—
413
+ )
414
+
415
+
416
+ class DevGuidePDF(FPDF):
417
+
418
+ def header(self):
419
+ if self.page_no() > 1:
420
+ self.set_font("Helvetica", "I", 8)
421
+ self.set_text_color(120, 120, 120)
422
+ self.cell(0, 5, "StockEx Developer's Guide", align="L")
423
+ self.cell(0, 5, f"Page {self.page_no()}", align="R")
424
+ self.ln(8)
425
+
426
+ def chapter_title(self, title):
427
+ self.set_font("Helvetica", "B", 16)
428
+ self.set_text_color(*hex_to_rgb(C_PRIMARY))
429
+ self.cell(0, 12, _ascii(title), new_x="LMARGIN", new_y="NEXT")
430
+ self.set_draw_color(*hex_to_rgb(C_ACCENT))
431
+ self.set_line_width(0.6)
432
+ self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y())
433
+ self.ln(4)
434
+
435
+ def section_title(self, title):
436
+ self.set_font("Helvetica", "B", 12)
437
+ self.set_text_color(*hex_to_rgb(C_SECONDARY))
438
+ self.cell(0, 9, _ascii(title), new_x="LMARGIN", new_y="NEXT")
439
+ self.ln(2)
440
+
441
+ def subsection_title(self, title):
442
+ self.set_font("Helvetica", "B", 10)
443
+ self.set_text_color(*hex_to_rgb(C_SECONDARY))
444
+ self.cell(0, 7, _ascii(title), new_x="LMARGIN", new_y="NEXT")
445
+ self.ln(1)
446
+
447
+ def body_text(self, text):
448
+ self.set_font("Helvetica", "", 9)
449
+ self.set_text_color(33, 33, 33)
450
+ self.multi_cell(0, 5, _ascii(text))
451
+ self.ln(2)
452
+
453
+ def code_block(self, text):
454
+ self.set_font("Courier", "", 8)
455
+ self.set_fill_color(240, 240, 240)
456
+ self.set_text_color(50, 50, 50)
457
+ for line in text.strip().split("\n"):
458
+ self.cell(0, 4.5, " " + _ascii(line), new_x="LMARGIN", new_y="NEXT", fill=True)
459
+ self.ln(3)
460
+
461
+ def table(self, headers, rows, col_widths=None):
462
+ if col_widths is None:
463
+ w = (self.w - self.l_margin - self.r_margin) / len(headers)
464
+ col_widths = [w] * len(headers)
465
+ # Header
466
+ self.set_font("Helvetica", "B", 8)
467
+ self.set_fill_color(*hex_to_rgb(C_PRIMARY))
468
+ self.set_text_color(255, 255, 255)
469
+ for i, h in enumerate(headers):
470
+ self.cell(col_widths[i], 6, _ascii(h), border=1, fill=True, align="C")
471
+ self.ln()
472
+ # Rows
473
+ self.set_font("Helvetica", "", 8)
474
+ self.set_text_color(33, 33, 33)
475
+ for j, row in enumerate(rows):
476
+ fill = j % 2 == 0
477
+ if fill:
478
+ self.set_fill_color(245, 245, 245)
479
+ for i, cell in enumerate(row):
480
+ self.cell(col_widths[i], 5.5, _ascii(str(cell)), border=1, fill=fill,
481
+ align="C" if i > 0 else "L")
482
+ self.ln()
483
+ self.ln(3)
484
+
485
+ _diagram_counter = 0
486
+
487
+ def add_diagram(self, img_bytes, w=180):
488
+ DevGuidePDF._diagram_counter += 1
489
+ tmp = os.path.join(OUT_DIR, f"_tmp_diagram_{DevGuidePDF._diagram_counter}.png")
490
+ with open(tmp, "wb") as f:
491
+ f.write(img_bytes)
492
+ self.image(tmp, x=(self.w - w) / 2, w=w)
493
+ os.remove(tmp)
494
+ self.ln(5)
495
+
496
+
497
+ def build_pdf():
498
+ os.makedirs(OUT_DIR, exist_ok=True)
499
+ pdf = DevGuidePDF()
500
+ pdf.set_auto_page_break(auto=True, margin=15)
501
+
502
+ # ── Cover page ────────────────────────────────────────────────────────
503
+ pdf.add_page()
504
+ pdf.ln(50)
505
+ pdf.set_font("Helvetica", "B", 32)
506
+ pdf.set_text_color(*hex_to_rgb(C_PRIMARY))
507
+ pdf.cell(0, 15, "StockEx", align="C", new_x="LMARGIN", new_y="NEXT")
508
+ pdf.set_font("Helvetica", "", 18)
509
+ pdf.set_text_color(100, 100, 100)
510
+ pdf.cell(0, 10, "Developer's Guide", align="C", new_x="LMARGIN", new_y="NEXT")
511
+ pdf.ln(5)
512
+ pdf.set_font("Helvetica", "", 11)
513
+ pdf.cell(0, 7, "Kafka-based Stock Exchange Simulator", align="C", new_x="LMARGIN", new_y="NEXT")
514
+ pdf.cell(0, 7, "with AI-Powered Clearing House Members", align="C", new_x="LMARGIN", new_y="NEXT")
515
+ pdf.ln(20)
516
+ pdf.set_font("Helvetica", "I", 10)
517
+ pdf.set_text_color(150, 150, 150)
518
+ pdf.cell(0, 6, "Version 2.0 | March 2026", align="C", new_x="LMARGIN", new_y="NEXT")
519
+ pdf.cell(0, 6, "github.com/Bonum/StockEx", align="C", new_x="LMARGIN", new_y="NEXT")
520
+
521
+ # ── Table of Contents ────────────────────────────────────────────────
522
+ pdf.add_page()
523
+ pdf.chapter_title("Table of Contents")
524
+ toc = [
525
+ "1. System Architecture",
526
+ "2. Kafka Message Flow",
527
+ "3. Services Reference",
528
+ "4. Market Data Feeder β€” Regime Engine",
529
+ "5. Clearing House β€” AI Trading Members",
530
+ "6. RL Agent β€” Neural Network Details",
531
+ "7. LLM Integration",
532
+ "8. Session Lifecycle",
533
+ "9. Database Schema",
534
+ "10. Configuration Reference",
535
+ "11. Deployment",
536
+ ]
537
+ for item in toc:
538
+ pdf.set_font("Helvetica", "", 11)
539
+ pdf.set_text_color(33, 33, 33)
540
+ pdf.cell(0, 7, _ascii(item), new_x="LMARGIN", new_y="NEXT")
541
+
542
+ # ── 1. System Architecture ────────────────────────────────────────────
543
+ pdf.add_page()
544
+ pdf.chapter_title("1. System Architecture")
545
+ pdf.body_text(
546
+ "StockEx runs as a single Docker container on HuggingFace Spaces (port 7860) "
547
+ "or as multiple containers via docker-compose. All services communicate through "
548
+ "Apache Kafka (KRaft mode, no ZooKeeper) with five topics: orders, trades, "
549
+ "snapshots, control, and ai_insights.\n\n"
550
+ "nginx acts as the reverse proxy, routing paths to the appropriate Flask service. "
551
+ "Three SQLite databases persist matcher state, OHLCV history, and clearing house data."
552
+ )
553
+ pdf.add_diagram(diagram_architecture())
554
+
555
+ # ── 2. Kafka Message Flow ─────────────────────────────────────────────
556
+ pdf.add_page()
557
+ pdf.chapter_title("2. Kafka Message Flow")
558
+ pdf.body_text(
559
+ "All order flow and control signals pass through Kafka topics. Producers send "
560
+ "messages (orders, snapshots, control signals) and multiple consumers process them "
561
+ "independently. This decoupled architecture allows services to be added or removed "
562
+ "without affecting others."
563
+ )
564
+ pdf.add_diagram(diagram_message_flow())
565
+
566
+ pdf.section_title("Order Message Format")
567
+ pdf.code_block("""\
568
+ {
569
+ "cl_ord_id": "USR01-1709876543210-1",
570
+ "symbol": "ALPHA",
571
+ "side": "BUY",
572
+ "quantity": 100,
573
+ "price": 10.50,
574
+ "ord_type": "LIMIT",
575
+ "time_in_force": "DAY",
576
+ "timestamp": 1709876543.123,
577
+ "source": "CLRH"
578
+ }""")
579
+
580
+ pdf.section_title("Trade Message Format")
581
+ pdf.code_block("""\
582
+ {
583
+ "trade_id": 12345,
584
+ "symbol": "ALPHA",
585
+ "price": 10.50,
586
+ "quantity": 100,
587
+ "buy_id": "USR01-1709876543210-1",
588
+ "sell_id": "MDF-1709876543200-42",
589
+ "timestamp": 1709876543.456
590
+ }""")
591
+
592
+ pdf.section_title("Snapshot with Indicators")
593
+ pdf.code_block("""\
594
+ {
595
+ "symbol": "ALPHA", "best_bid": 24.85, "best_ask": 25.05,
596
+ "bid_size": 200, "ask_size": 150, "timestamp": 1709876543.789,
597
+ "source": "MDF",
598
+ "indicators": {
599
+ "sma_5": 24.92, "sma_20": 24.78,
600
+ "ema_12": 24.88, "ema_26": 24.75,
601
+ "macd": 0.13, "rsi_14": 62.5,
602
+ "bb_pos": 0.72, "regime": "trending_up"
603
+ }
604
+ }""")
605
+
606
+ # ── 3. Services Reference ─────────────────────────────────────────────
607
+ pdf.add_page()
608
+ pdf.chapter_title("3. Services Reference")
609
+ pdf.table(
610
+ ["Service", "Port", "Key File", "Description"],
611
+ [
612
+ ["nginx", "7860", "nginx.conf", "Reverse proxy"],
613
+ ["Dashboard", "5000", "dashboard/dashboard.py", "SSE streaming, session control, charts"],
614
+ ["Matcher", "6000", "matcher/matcher.py", "Price-time matching, SQLite persistence"],
615
+ ["MD Feeder", "-", "md_feeder/mdf_simulator.py", "Regime-based order generation"],
616
+ ["Clearing House", "5004", "clearing_house/app.py", "AI members, RL/LLM trading"],
617
+ ["AI Analyst", "-", "ai_analyst/ai_analyst.py", "LLM market commentary"],
618
+ ["FIX OEG", "5001", "fix_oeg/fix_oeg_server.py", "FIX 4.4 acceptor"],
619
+ ["FIX UI", "5002", "fix-ui-client/fix-ui-client.py", "Browser FIX client"],
620
+ ["Frontend", "5003", "frontend/frontend.py", "Order entry form"],
621
+ ],
622
+ [35, 15, 55, 85],
623
+ )
624
+
625
+ pdf.section_title("Startup Order (entrypoint.sh)")
626
+ pdf.body_text(
627
+ "Services start sequentially with sleep delays to allow initialization:\n"
628
+ "1. Kafka (KRaft) β€” wait for broker ready (up to 30 retries)\n"
629
+ "2. Create Kafka topics (orders, trades, snapshots, control, ai_insights)\n"
630
+ "3. Matcher (:6000) β€” wait 8s\n"
631
+ "4. MD Feeder β€” background\n"
632
+ "5. FIX OEG (:5001) β€” wait 6s\n"
633
+ "6. FIX UI (:5002) β€” wait 3s\n"
634
+ "7. AI Analyst β€” wait 1s\n"
635
+ "8. Frontend (:5003) β€” wait 2s\n"
636
+ "9. Clearing House (:5004) β€” wait 3s\n"
637
+ "10. nginx (:7860)\n"
638
+ "11. Dashboard (:5000) β€” exec (foreground process)"
639
+ )
640
+
641
+ # ── 4. MDF Regime Engine ──────────────────────────────────────────────
642
+ pdf.add_page()
643
+ pdf.chapter_title("4. Market Data Feeder β€” Regime Engine")
644
+ pdf.body_text(
645
+ "The MD Feeder drives price evolution through a PriceDynamics class that simulates "
646
+ "five market regimes. Each symbol independently cycles through regimes, producing "
647
+ "realistic technical indicator patterns that the RL and LLM strategies can exploit.\n\n"
648
+ "Price dynamics use drift + noise + momentum, with adaptive volatility that expands "
649
+ "in volatile regimes and contracts in calm periods. When price deviates more than 15% "
650
+ "from the session start price, the engine biases regime transitions toward mean reversion."
651
+ )
652
+ pdf.add_diagram(diagram_mdf_regimes())
653
+
654
+ pdf.section_title("Regime Parameters")
655
+ pdf.table(
656
+ ["Regime", "Drift", "Noise", "Spread", "Aggr. Prob", "Indicator Effect"],
657
+ [
658
+ ["trending_up", "+0.2-0.8 * vol", "0.5 * vol", "0.8-1.5x", "20%", "MACD > 0, RSI rising"],
659
+ ["trending_down", "-0.2-0.8 * vol", "0.5 * vol", "0.8-1.5x", "20%", "MACD < 0, RSI falling"],
660
+ ["mean_revert", "3% pull to start", "0.3 * vol", "1.0x", "20%", "RSI oscillates ~50"],
661
+ ["volatile", "random", "1.5 * vol", "1.5-2.5x", "35%", "BB width expands"],
662
+ ["calm", "near zero", "0.2 * vol", "0.6-1.0x", "20%", "BB width contracts"],
663
+ ],
664
+ [30, 28, 22, 22, 20, 68],
665
+ )
666
+
667
+ pdf.section_title("Order Book Depth")
668
+ pdf.body_text(
669
+ "Each cycle places 3 levels of resting bids and asks per symbol (6 passive orders), "
670
+ "ensuring the order book always has visible depth. In trending regimes, the passive side "
671
+ "gets thicker depth (1.5x quantity) to model institutional support/resistance.\n\n"
672
+ "Aggressive orders (20-35% probability) cross the spread to generate trades. "
673
+ "The aggressive side is biased by indicators: sell when RSI > 70 + BB overbought, "
674
+ "buy when RSI < 30 + BB oversold, and follow MACD direction in trending regimes."
675
+ )
676
+
677
+ # ── 5. Clearing House ────────────────────────────────────────────────
678
+ pdf.add_page()
679
+ pdf.chapter_title("5. Clearing House β€” AI Trading Members")
680
+ pdf.body_text(
681
+ "The Clearing House service manages 10 simulated trading members (USR01-USR10), each "
682
+ "starting with EUR 100,000 capital and a daily obligation to trade at least 20 securities. "
683
+ "Members not controlled by a human user are traded by the AI engine."
684
+ )
685
+ pdf.add_diagram(diagram_ai_strategy())
686
+
687
+ pdf.section_title("Strategy Selection")
688
+ pdf.table(
689
+ ["Mode", "Env Var Value", "Behavior"],
690
+ [
691
+ ["Hybrid (default)", "hybrid", "USR01-05 = RL, USR06-10 = LLM"],
692
+ ["RL only", "rl", "All members use PPO neural network"],
693
+ ["LLM only", "llm", "All members use Groq/HF/Ollama"],
694
+ ],
695
+ [35, 35, 120],
696
+ )
697
+ pdf.body_text(
698
+ "Strategy is set via CH_AI_STRATEGY env var and can be switched at runtime:\n"
699
+ " POST /ch/api/strategy {\"strategy\": \"rl\"}\n\n"
700
+ "Every strategy falls back: RL β†’ LLM β†’ rule-based. The rule-based fallback uses "
701
+ "portfolio balance heuristics (buy when holdings < 20% of net worth, sell when > 60%)."
702
+ )
703
+
704
+ pdf.section_title("Trade Attribution")
705
+ pdf.body_text(
706
+ "The trade consumer thread monitors the Kafka 'trades' topic. When a trade's buy_id "
707
+ "or sell_id matches the pattern USRxx-*, the trade is attributed to that clearing house "
708
+ "member. This updates their capital, holdings, and daily trade count in the CH database."
709
+ )
710
+
711
+ # ── 6. RL Agent ───────────────────────────────────────────────────────
712
+ pdf.add_page()
713
+ pdf.chapter_title("6. RL Agent β€” Neural Network Details")
714
+ pdf.body_text(
715
+ "The RL strategy uses Adilbai/stock-trading-rl-agent, a PPO (Proximal Policy Optimization) "
716
+ "model trained with Stable-Baselines3 on 500,000 timesteps of historical stock data "
717
+ "(AAPL, MSFT, GOOGL, AMZN, TSLA)."
718
+ )
719
+
720
+ pdf.section_title("Model Specifications")
721
+ pdf.table(
722
+ ["Property", "Value"],
723
+ [
724
+ ["Algorithm", "PPO (Proximal Policy Optimization)"],
725
+ ["Policy network", "MlpPolicy (Multi-Layer Perceptron)"],
726
+ ["Model file size", "~2.5 MB (final_model.zip)"],
727
+ ["Scaler file size", "~50 KB (scaler.pkl, StandardScaler)"],
728
+ ["Observation space", "3,008 dimensions"],
729
+ ["Action space", "2D: action_type (0-2) + position_size (0-1)"],
730
+ ["Training steps", "500,000"],
731
+ ["Learning rate", "0.0003"],
732
+ ["Gamma (discount)", "0.99"],
733
+ ["Batch size", "64"],
734
+ ["License", "MIT (free, runs locally)"],
735
+ ["Storage location", "/app/data/rl_model/ (downloaded on first use)"],
736
+ ],
737
+ [50, 140],
738
+ )
739
+
740
+ pdf.add_diagram(diagram_rl_observation())
741
+
742
+ pdf.section_title("Observation Space Breakdown")
743
+ pdf.body_text(
744
+ "The 3,008-dimensional observation vector consists of:\n\n"
745
+ "Market state (3,000 dims): 60 time bars x 50 features per bar\n"
746
+ " - 20 base features: Close, Volume, SMA(5,10,20,50), EMA(12,26), RSI(14), "
747
+ "MACD, Signal, Histogram, BB(upper,lower,width,position), Volatility(20), "
748
+ "Price changes, H/L ratio, Volume ratio\n"
749
+ " - 30 lag features: Close, Volume, Price change, RSI, MACD, Volatility "
750
+ "at lags 1, 2, 3, 5, 10\n\n"
751
+ "Portfolio state (8 dims): capital, held_qty, net_worth, returns, held_value, "
752
+ "has_position (binary), capital_ratio, holdings_ratio"
753
+ )
754
+
755
+ pdf.section_title("Price History Management")
756
+ pdf.body_text(
757
+ "The RL module (ch_rl_trader.py) maintains a rolling buffer of OHLCV bars per symbol:\n"
758
+ " - Trades from Kafka feed into a current-bar accumulator (feed_trade())\n"
759
+ " - Bars finalize every CH_RL_BAR_INTERVAL seconds (default 60s)\n"
760
+ " - On startup, bars are seeded from BBO reference prices with small noise\n"
761
+ " - Minimum CH_RL_MIN_BARS (default 30) required before RL predictions start\n"
762
+ " - Buffer holds up to 120 bars (deque with maxlen)\n\n"
763
+ "The shipped scaler.pkl was trained on real stock data. When it fails on synthetic "
764
+ "StockEx data (shape mismatch), the code falls back to manual z-score normalization."
765
+ )
766
+
767
+ pdf.section_title("Inference vs LLM Comparison")
768
+ pdf.table(
769
+ ["Aspect", "RL (PPO)", "LLM"],
770
+ [
771
+ ["Input", "3,008-dim float vector", "Text prompt (~500 chars)"],
772
+ ["Processing", "NN forward pass (<1ms)", "API call (2-30s)"],
773
+ ["Output", "[action_type, position_size]", "JSON text to parse"],
774
+ ["Internet required", "No (local model)", "Yes (Groq/HF) or Ollama"],
775
+ ["Cost", "Free (CPU inference)", "Free tier or self-hosted"],
776
+ ["Determinism", "Configurable", "Temperature-dependent"],
777
+ ],
778
+ [35, 65, 90],
779
+ )
780
+
781
+ # ── 7. LLM Integration ───────────────────────────────────────────────
782
+ pdf.add_page()
783
+ pdf.chapter_title("7. LLM Integration")
784
+ pdf.body_text(
785
+ "The LLM strategy sends a text prompt containing the member's state (capital, holdings, "
786
+ "obligation) and current market BBOs, requesting a single JSON trade decision."
787
+ )
788
+
789
+ pdf.section_title("Provider Fallback Chain")
790
+ pdf.table(
791
+ ["Priority (HF Spaces)", "Priority (Local)", "Provider", "Env Vars"],
792
+ [
793
+ ["1st", "2nd", "Groq", "GROQ_API_KEY, GROQ_MODEL"],
794
+ ["-", "3rd", "HuggingFace Inference", "HF_TOKEN, HF_MODEL"],
795
+ ["2nd", "1st", "Ollama (local)", "OLLAMA_HOST, OLLAMA_MODEL"],
796
+ ],
797
+ [35, 30, 45, 80],
798
+ )
799
+ pdf.body_text(
800
+ "On HuggingFace Spaces (detected via SPACE_ID env var), Groq is preferred (free tier, fast). "
801
+ "Locally, Ollama with a fine-tuned model is preferred. The HF Inference endpoint is used "
802
+ "as a fallback with the custom fine-tuned model RayMelius/stockex-ch-trader.\n\n"
803
+ "If all LLM providers fail, the system falls back to rule-based trading."
804
+ )
805
+
806
+ pdf.section_title("Prompt Structure")
807
+ pdf.code_block("""\
808
+ You are simulating clearing house member USR01 making ONE trading decision.
809
+
810
+ Member state:
811
+ Available capital: EUR 95,230.00
812
+ Securities obligation remaining today: 15 more to trade
813
+ Current holdings:
814
+ ALPHA: 200 shares @ avg cost 24.90
815
+
816
+ Current market (Bid/Ask):
817
+ ALPHA: Bid 24.85 / Ask 25.05
818
+ NBG: Bid 17.95 / Ask 18.15
819
+ ...
820
+
821
+ Rules:
822
+ - Do not spend more than your available capital
823
+ - Do not sell more shares than you hold
824
+ - Choose a realistic price close to the BBO mid-price
825
+ - Quantity should be between 10 and 200
826
+
827
+ Respond ONLY with valid JSON:
828
+ Example: {"symbol": "ALPHA", "side": "BUY", "quantity": 50, "price": 5.95}""")
829
+
830
+ # ── 8. Session Lifecycle ──────────────────────────────────────────────
831
+ pdf.add_page()
832
+ pdf.chapter_title("8. Session Lifecycle")
833
+ pdf.add_diagram(diagram_ch_lifecycle())
834
+
835
+ pdf.section_title("Control Messages")
836
+ pdf.table(
837
+ ["Action", "Dashboard Trigger", "Effect on MDF", "Effect on CH AI"],
838
+ [
839
+ ["start", "Start of Day button", "running=True, reload prices", "suspended=False"],
840
+ ["stop", "End of Day button", "running=False", "suspended=True"],
841
+ ["suspend", "Suspend button", "order gen paused", "suspended=True"],
842
+ ["resume", "Resume button", "order gen resumed", "suspended=False"],
843
+ ],
844
+ [25, 40, 55, 70],
845
+ )
846
+
847
+ pdf.section_title("Manual vs Automatic Mode")
848
+ pdf.body_text(
849
+ "The Dashboard supports two session modes:\n\n"
850
+ "Automatic: A background scheduler reads market_schedule.txt and automatically "
851
+ "triggers Start of Day / End of Day based on configured times and timezone.\n\n"
852
+ "Manual: The scheduler is disabled. Sessions are started/stopped exclusively "
853
+ "via the Dashboard buttons. All trading activity (MDF, CH AI, FIX) still operates "
854
+ "normally once a session is started."
855
+ )
856
+
857
+ pdf.section_title("EOD Settlement")
858
+ pdf.body_text(
859
+ "When End of Day is triggered, the Dashboard POSTs to /ch/eod. The Clearing House:\n"
860
+ "1. Calculates unrealized P&L for each member (current market value vs avg cost)\n"
861
+ "2. Checks daily obligation (>= 20 securities traded)\n"
862
+ "3. Records settlement entry in the database\n"
863
+ "4. Logs members who failed their obligation"
864
+ )
865
+
866
+ # ── 9. Database Schema ────────────────────────────────────────────────
867
+ pdf.add_page()
868
+ pdf.chapter_title("9. Database Schema")
869
+
870
+ pdf.section_title("matcher.db (Matcher Service)")
871
+ pdf.body_text("Stores all orders and trades for the matching engine.")
872
+ pdf.code_block("""\
873
+ orders: order_id, symbol, side, price, quantity, remaining_qty,
874
+ status, cl_ord_id, timestamp
875
+ trades: trade_id, symbol, price, quantity, buy_order_id,
876
+ sell_order_id, timestamp""")
877
+
878
+ pdf.section_title("clearing_house.db (Clearing House)")
879
+ pdf.body_text("Stores member accounts, trade attribution, and settlement history.")
880
+ pdf.code_block("""\
881
+ members: member_id (PK), password_hash, capital
882
+ holdings: member_id, symbol, quantity, avg_cost
883
+ (composite PK: member_id + symbol)
884
+ trade_log: id, member_id, symbol, side, quantity, price,
885
+ order_id, timestamp
886
+ settlements: id, member_id, trading_date, opening_capital,
887
+ closing_capital, realized_pnl, unrealized_pnl,
888
+ obligation_met
889
+ ai_decisions: id, member_id, raw_response, parsed_order,
890
+ source, timestamp""")
891
+
892
+ pdf.section_title("Dashboard OHLCV (In-Memory + Periodic SQLite)")
893
+ pdf.body_text(
894
+ "The Dashboard aggregates trades into 1-minute OHLCV buckets for candlestick charts. "
895
+ "Data is held in memory with periodic persistence to SQLite for history."
896
+ )
897
+
898
+ # ── 10. Configuration Reference ───────────────────────────────────────
899
+ pdf.add_page()
900
+ pdf.chapter_title("10. Configuration Reference")
901
+
902
+ pdf.section_title("Core Settings (shared/config.py)")
903
+ pdf.table(
904
+ ["Variable", "Default", "Description"],
905
+ [
906
+ ["KAFKA_BOOTSTRAP", "kafka:9092", "Kafka broker address"],
907
+ ["MATCHER_URL", "http://matcher:6000", "Matcher REST API"],
908
+ ["SECURITIES_FILE", "/app/data/securities.txt", "Symbol definitions"],
909
+ ["TICK_SIZE", "0.05", "Minimum price increment"],
910
+ ["ORDERS_PER_MIN", "8", "MDF order rate per symbol"],
911
+ ["CH_DAILY_OBLIGATION", "20", "Min securities per member/day"],
912
+ ["CH_STARTING_CAPITAL", "100,000", "Initial member capital (EUR)"],
913
+ ],
914
+ [45, 55, 90],
915
+ )
916
+
917
+ pdf.section_title("AI Strategy Settings")
918
+ pdf.table(
919
+ ["Variable", "Default", "Description"],
920
+ [
921
+ ["CH_AI_STRATEGY", "hybrid", "llm, rl, or hybrid"],
922
+ ["CH_AI_INTERVAL", "45", "Seconds between AI cycles"],
923
+ ["CH_RL_MODEL_REPO", "Adilbai/stock-trading-rl-agent", "HF model repo"],
924
+ ["CH_RL_BAR_INTERVAL", "60", "Seconds per OHLCV bar"],
925
+ ["CH_RL_MIN_BARS", "30", "Min bars before RL starts"],
926
+ ["GROQ_API_KEY", "-", "Groq API key"],
927
+ ["GROQ_MODEL", "llama-3.1-8b-instant", "Groq model name"],
928
+ ["HF_TOKEN", "-", "HuggingFace API token"],
929
+ ["HF_MODEL", "RayMelius/stockex-ch-trader", "HF model for CH"],
930
+ ["OLLAMA_HOST", "-", "Ollama server URL"],
931
+ ["OLLAMA_MODEL", "llama3.1:8b", "Ollama model name"],
932
+ ],
933
+ [50, 55, 85],
934
+ )
935
+
936
+ # ── 11. Deployment ────────────────────────────────────────────────────
937
+ pdf.add_page()
938
+ pdf.chapter_title("11. Deployment")
939
+
940
+ pdf.section_title("HuggingFace Spaces (Single Container)")
941
+ pdf.body_text(
942
+ "The Dockerfile builds a single container with Kafka, all Python services, and nginx. "
943
+ "HuggingFace Spaces exposes port 7860. The entrypoint.sh starts services sequentially.\n\n"
944
+ "Key dependencies added for RL: stable-baselines3 (pulls PyTorch ~200MB), "
945
+ "numpy (<2.0), pandas, scikit-learn, huggingface_hub.\n\n"
946
+ "The RL model is downloaded from HuggingFace Hub on first use and cached at "
947
+ "/app/data/rl_model/."
948
+ )
949
+
950
+ pdf.section_title("Docker Compose (Multi-Container)")
951
+ pdf.body_text(
952
+ "docker-compose.yml defines separate containers for each service. The clearing_house "
953
+ "service gets its own Dockerfile with the RL dependencies. Volumes persist SQLite databases.\n\n"
954
+ "Set CH_AI_STRATEGY in the environment section or .env file."
955
+ )
956
+
957
+ pdf.section_title("Local Development")
958
+ pdf.code_block("""\
959
+ # Prerequisites: Python 3.11+, Kafka on localhost:9092
960
+ pip install kafka-python Flask requests quickfix \\
961
+ stable-baselines3 huggingface_hub pandas scikit-learn
962
+
963
+ export PYTHONPATH=$(pwd)
964
+ export KAFKA_BOOTSTRAP=localhost:9092
965
+ export MATCHER_URL=http://localhost:6000
966
+ export CH_AI_STRATEGY=hybrid
967
+
968
+ # Terminal 1-4: matcher, md_feeder, dashboard, clearing_house""")
969
+
970
+ pdf.section_title("CI/CD Pipeline")
971
+ pdf.body_text(
972
+ "GitHub Actions workflows:\n"
973
+ " - ci.yml: Run matcher unit tests + Docker build check on every push\n"
974
+ " - deploy-hf.yml: Auto-deploy to HuggingFace Spaces on push to main (after tests pass)\n\n"
975
+ "The container log prints the StockEx version at startup for deployment verification."
976
+ )
977
+
978
+ # ── Save ──────────────────────────────────────────────────────────────
979
+ pdf.output(OUT_FILE)
980
+ print(f"PDF generated: {OUT_FILE}")
981
+ return OUT_FILE
982
+
983
+
984
+ if __name__ == "__main__":
985
+ build_pdf()