feat: 6 user-facing upgrades + realistic demo metrics + animated hero
Demo + assets
- Synthetic backtests now occasionally fail (regime failures, OOS degradation)
so verdicts span RECOMMENDED / RISKY / NOT_RELIABLE realistically. Phase 1
has ~35% failure rate, Phase 2 AI loop ~10%, OOS has 30% chance of severe
degradation — matches what real markets look like
- screenshots/dashboard.png + best_result_modal.png regenerated against the
current UI; screenshots/apex_demo.gif (6-frame autonomous-run timelapse)
embedded in the README
FEATURE 1 — Live AI token streaming
- AIReasoner._call_claude() now streams via SSE when a callback is
registered. Each text delta forwards to the dashboard as
`ai_thinking_chunk` events
- The Live AI Thinking Feed renders a single growing bubble with a blinking
cursor while text streams in, finalising on `end`. Looks and feels like
watching the AI type
FEATURE 2 — Pre-flight check on /setup
- New /api/preflight endpoint runs 5–7 probes: config readable, API key
set, MT5 paths exist (skipped in demo), EA registered, reports folder
writable. Returns {ok, blocking_count, checks[]}
- Setup page renders a colour-coded checklist on load and refocus.
Replaces "click Start, wait 5s, see generic error"
FEATURE 3 — Hot-reload settings into the running pipeline
- pipeline.reload_config() applies AI model / timeout / API-key swaps to
the live reasoner mid-run. Threshold changes surface for next run
- /api/settings POST detects a running pipeline and calls reload_config(),
returning the changed keys plus a "hot-reloaded into the running
optimization" note
FEATURE 4 — Replay scrubber on Best Result
- Evolution path now renders as an interactive scrubber: range slider +
prev/next/play buttons. Each step shows the run ID, phase, score, full
metrics grid, parameter changes for that step, and the AI's analysis
text — auto-plays at 700ms/step
FEATURE 5 — Compare runs on /reports
- Each card has a checkbox; selecting 2–4 reveals a floating Compare bar.
Compare modal renders a side-by-side table with metric winners
highlighted (Calmar / PF / profit favour higher; DD favours lower)
and a parameter-diff section showing changed values
FEATURE 6 — Discord / Slack / generic webhook on completion
- New `notifications.webhook_url` + `webhook_style` config keys
- Auto-detects Discord vs Slack from the URL host. Posts a one-line
summary on `optimization_complete`: verdict + best run + PF/Calmar/DD/
profit/trades/elapsed
This commit is contained in:
+58
-14
@@ -205,31 +205,75 @@ Be direct and technical. The user is an experienced forex trader. Max 2-3 sugges
|
||||
# ── API call ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _call_claude(self, prompt: str) -> str:
|
||||
"""
|
||||
Call Claude. If a token-stream callback was registered via
|
||||
`set_stream_callback`, use the SSE streaming endpoint and forward each
|
||||
text delta via the callback so the dashboard can render the AI's
|
||||
reasoning as it types.
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": self.api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
stream_cb = getattr(self, "_stream_cb", None)
|
||||
|
||||
if stream_cb is None:
|
||||
# ── Non-streaming path (used when no UI is attached) ──
|
||||
body = {
|
||||
"model": self.MODEL,
|
||||
"max_tokens": 1024,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
resp = requests.post(self.API_URL, headers=headers, json=body, timeout=self.TIMEOUT)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Claude API returned {resp.status_code}: {resp.text[:300]}")
|
||||
return resp.json()["content"][0]["text"]
|
||||
|
||||
# ── Streaming path: parses SSE events, accumulates text, fires callback per delta ──
|
||||
body = {
|
||||
"model": self.MODEL,
|
||||
"max_tokens": 1024,
|
||||
"stream": True,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
try:
|
||||
stream_cb({"event": "start"})
|
||||
with requests.post(self.API_URL, headers=headers, json=body, timeout=self.TIMEOUT, stream=True) as resp:
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"Claude API returned {resp.status_code}: {resp.text[:300]}")
|
||||
full_text = []
|
||||
for raw in resp.iter_lines(decode_unicode=True):
|
||||
if not raw or not raw.startswith("data:"):
|
||||
continue
|
||||
payload = raw[5:].strip()
|
||||
if not payload or payload == "[DONE]":
|
||||
continue
|
||||
try:
|
||||
evt = json.loads(payload)
|
||||
except Exception:
|
||||
continue
|
||||
if evt.get("type") == "content_block_delta":
|
||||
delta = (evt.get("delta") or {}).get("text") or ""
|
||||
if delta:
|
||||
full_text.append(delta)
|
||||
try:
|
||||
stream_cb({"event": "delta", "text": delta})
|
||||
except Exception:
|
||||
pass
|
||||
elif evt.get("type") == "message_stop":
|
||||
break
|
||||
stream_cb({"event": "end"})
|
||||
return "".join(full_text)
|
||||
except Exception as e:
|
||||
try: stream_cb({"event": "error", "error": str(e)})
|
||||
except Exception: pass
|
||||
raise
|
||||
|
||||
resp = requests.post(
|
||||
self.API_URL,
|
||||
headers=headers,
|
||||
json=body,
|
||||
timeout=self.TIMEOUT,
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Claude API returned {resp.status_code}: {resp.text[:300]}"
|
||||
)
|
||||
|
||||
data = resp.json()
|
||||
return data["content"][0]["text"]
|
||||
def set_stream_callback(self, cb) -> None:
|
||||
"""Register a callback `cb(event_dict)` that receives token deltas
|
||||
during streaming Claude calls. Pass None to disable streaming."""
|
||||
self._stream_cb = cb
|
||||
|
||||
# ── Response parser ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user