fix: LLM analysis + new run button + settings page support

- Fixed LLM failing silently (401 auth error on every iteration)
- Reset provider to Ollama (working) from broken OpenRouter config
- Added /api/clear endpoint + 'New Run' button to reset history
- LLM failures now logged visibly with error details
- LLM suggestions persisted to iteration data (survive restarts)
- Settings page support via llm_settings.json (multi-provider)
This commit is contained in:
BizzleBot 2026-03-20 21:51:05 +00:00
parent c17b3b5167
commit aba30f7718
8 changed files with 911 additions and 117 deletions

68
config/best_config.json Normal file
View File

@ -0,0 +1,68 @@
{
"model_type": "xgboost",
"features": {
"use_price_position": true,
"use_momentum": true,
"use_volatility": true,
"use_volume": true,
"use_cycle": true,
"use_pca": false,
"pca_variance": 0.95,
"use_scaler": true
},
"target": {
"type": "regression",
"forward_periods_1h": [
168,
720,
2160
],
"forward_periods_4h": [
42,
180,
540
],
"weights": [
0.2,
0.3,
0.5
],
"score_range": [
0,
100
]
},
"hyperparameters": {
"learning_rate": 0.01,
"max_depth": 4,
"n_estimators": 300,
"subsample": 0.8,
"colsample_bytree": 0.8,
"min_child_weight": 20,
"gamma": 0.3,
"reg_alpha": 0.5,
"reg_lambda": 3.0,
"lstm_hidden_size": 128,
"lstm_num_layers": 2,
"lstm_dropout": 0.3,
"lstm_epochs": 100,
"lstm_batch_size": 64,
"lstm_sequence_length": 30,
"lstm_patience": 10
},
"strategy": {
"strong_buy_threshold": 65,
"good_buy_threshold": 55,
"poor_threshold": 35
},
"training": {
"rolling_window": true,
"rolling_train_size": 2500,
"rolling_test_size": 300,
"walk_forward_windows": 5,
"train_pct": 0.7,
"validation_pct": 0.15,
"test_pct": 0.15
},
"timeframe": "4h"
}

View File

@ -1,92 +1,68 @@
{ {
"model_type": "hybrid", "model_type": "xgboost",
"features": { "features": {
"technical_indicators": [ "use_price_position": true,
"RSI_14", "use_momentum": true,
"RSI_7", "use_volatility": true,
"MACD_line", "use_volume": true,
"MACD_signal", "use_cycle": true,
"MACD_hist", "use_pca": false,
"BB_upper",
"BB_lower",
"BB_width",
"ATR_14",
"SMA_20",
"SMA_50",
"EMA_10",
"EMA_20",
"OBV",
"stoch_k",
"stoch_d",
"williams_r",
"CCI_20",
"ROC_10"
],
"lookback_periods": [
3,
5,
10,
20
],
"use_volume_features": true,
"use_volatility_features": true,
"use_candle_patterns": false,
"use_lag_features": true,
"lag_periods": [
1,
2,
3,
5
],
"use_pca": true,
"pca_variance": 0.95, "pca_variance": 0.95,
"use_scaler": true "use_scaler": true
}, },
"target": { "target": {
"type": "classification", "type": "regression",
"direction": "both", "forward_periods_1h": [
"horizon_candles": 4, 168,
"threshold_pct": 1.0 720,
2160
],
"forward_periods_4h": [
42,
180,
540
],
"weights": [
0.2,
0.3,
0.5
],
"score_range": [
0,
100
]
}, },
"hyperparameters": { "hyperparameters": {
"learning_rate": 0.001, "learning_rate": 0.01,
"max_depth": 5, "max_depth": 4,
"n_estimators": 300, "n_estimators": 300,
"subsample": 0.8, "subsample": 0.8,
"colsample_bytree": 0.8, "colsample_bytree": 0.8,
"min_child_weight": 5, "min_child_weight": 20,
"gamma": 0.3, "gamma": 0.3,
"reg_alpha": 0.1, "reg_alpha": 0.5,
"reg_lambda": 5.0, "reg_lambda": 3.0,
"lstm_hidden_size": 128, "lstm_hidden_size": 128,
"lstm_num_layers": 2, "lstm_num_layers": 2,
"lstm_dropout": 0.3, "lstm_dropout": 0.3,
"lstm_epochs": 100, "lstm_epochs": 100,
"lstm_batch_size": 64, "lstm_batch_size": 64,
"lstm_sequence_length": 20, "lstm_sequence_length": 30,
"lstm_patience": 10 "lstm_patience": 10
}, },
"strategy": { "strategy": {
"entry_threshold": 0.65, "strong_buy_threshold": 65,
"exit_type": "trailing_stop", "good_buy_threshold": 55,
"stop_loss_pct": 2.0, "poor_threshold": 35
"take_profit_pct": 4.0,
"trailing_stop_pct": 1.5,
"position_sizing": "confidence_scaled",
"max_position_pct": 100,
"min_confidence_to_trade": 0.5,
"dynamic_sl_tp": true,
"atr_sl_multiplier": 1.2,
"atr_tp_multiplier": 3.0
}, },
"training": { "training": {
"rolling_window": true,
"rolling_train_size": 2500,
"rolling_test_size": 300,
"walk_forward_windows": 5, "walk_forward_windows": 5,
"train_pct": 0.7, "train_pct": 0.7,
"validation_pct": 0.15, "validation_pct": 0.15,
"test_pct": 0.15, "test_pct": 0.15
"rolling_window": true,
"rolling_train_size": 3000,
"rolling_test_size": 200
}, },
"timeframe": "4h" "timeframe": "4h"
} }

21
config/llm_settings.json Normal file
View File

@ -0,0 +1,21 @@
{
"provider": "ollama",
"model": "qwen3.5:27b",
"providers": {
"ollama": {
"base_url": "http://100.100.242.21:11434"
},
"lmstudio": {
"base_url": "http://100.100.242.21:1234"
},
"openai": {
"api_key": ""
},
"anthropic": {
"api_key": ""
},
"openrouter": {
"api_key": ""
}
}
}

View File

@ -9,6 +9,7 @@ import os
import sys import sys
import threading import threading
import requests
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
@ -23,6 +24,7 @@ app = FastAPI(title="BTC Accumulation Signal Optimizer")
CONFIG_DIR = os.path.join(BASE_DIR, "config") CONFIG_DIR = os.path.join(BASE_DIR, "config")
RESULTS_DIR = os.path.join(BASE_DIR, "results") RESULTS_DIR = os.path.join(BASE_DIR, "results")
ITERATIONS_LOG = os.path.join(RESULTS_DIR, "iterations.jsonl") ITERATIONS_LOG = os.path.join(RESULTS_DIR, "iterations.jsonl")
LLM_SETTINGS_PATH = os.path.join(CONFIG_DIR, "llm_settings.json")
_opt_thread = None _opt_thread = None
@ -31,6 +33,64 @@ class ConfigUpdate(BaseModel):
config: dict config: dict
class LLMSettingsUpdate(BaseModel):
provider: str
model: str
providers: dict
class TestConnectionRequest(BaseModel):
provider: str
providers: dict
class FetchModelsRequest(BaseModel):
provider: str
providers: dict
def _load_llm_settings():
if os.path.exists(LLM_SETTINGS_PATH):
with open(LLM_SETTINGS_PATH) as f:
return json.load(f)
return {
"provider": "ollama",
"model": "qwen3.5:27b",
"providers": {
"ollama": {"base_url": "http://100.100.242.21:11434"},
"lmstudio": {"base_url": "http://100.100.242.21:1234"},
"openai": {"api_key": ""},
"anthropic": {"api_key": ""},
"openrouter": {"api_key": ""},
},
}
def _mask_api_key(key):
if not key or len(key) < 8:
return ""
return "••••••••" + key[-4:]
def _safe_settings(settings):
"""Return settings with API keys masked."""
out = json.loads(json.dumps(settings))
for name, cfg in out.get("providers", {}).items():
if "api_key" in cfg:
cfg["api_key"] = _mask_api_key(cfg["api_key"])
return out
def _merge_api_keys(new_providers, existing_providers):
"""Preserve existing API keys when the incoming value is masked."""
for name, cfg in new_providers.items():
if "api_key" in cfg:
masked = cfg["api_key"]
if masked.startswith("••••") or masked == "":
existing_key = existing_providers.get(name, {}).get("api_key", "")
cfg["api_key"] = existing_key
@app.get("/api/status") @app.get("/api/status")
def api_status(): def api_status():
return orchestrator.get_status() return orchestrator.get_status()
@ -91,7 +151,11 @@ def api_best():
with open(best_path) as f: with open(best_path) as f:
config = json.load(f) config = json.load(f)
iterations = orchestrator.load_iteration_history() iterations = orchestrator.load_iteration_history()
best_iter = max(iterations, key=lambda x: x.get("cost_improvement", 0)) if iterations else {} best_iter = (
max(iterations, key=lambda x: x.get("cost_improvement", 0))
if iterations
else {}
)
return {"config": config, "best_iteration": best_iter} return {"config": config, "best_iteration": best_iter}
@ -110,32 +174,155 @@ def api_download_best_config():
return JSONResponse({"error": "No best config yet"}, status_code=404) return JSONResponse({"error": "No best config yet"}, status_code=404)
DASHBOARD_HTML = """<!DOCTYPE html> # ── Settings API ──────────────────────────────────────────────────────────
<html lang="en">
<head> @app.get("/api/settings")
<meta charset="UTF-8"> def api_get_settings():
<meta name="viewport" content="width=device-width, initial-scale=1.0"> settings = _load_llm_settings()
<title>BTC Accumulation Signal Optimizer</title> return _safe_settings(settings)
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script> @app.post("/api/settings")
<style> def api_save_settings(body: LLMSettingsUpdate):
existing = _load_llm_settings()
new_settings = {
"provider": body.provider,
"model": body.model,
"providers": body.providers,
}
_merge_api_keys(new_settings["providers"], existing.get("providers", {}))
with open(LLM_SETTINGS_PATH, "w") as f:
json.dump(new_settings, f, indent=2)
return {"ok": True, "message": "Settings saved"}
@app.post("/api/settings/test")
def api_test_connection(body: TestConnectionRequest):
"""Test connection to a provider and return available models."""
existing = _load_llm_settings()
providers = json.loads(json.dumps(body.providers))
_merge_api_keys(providers, existing.get("providers", {}))
provider = body.provider
try:
models = _fetch_models(provider, providers)
return {"ok": True, "models": models, "message": f"Connected — {len(models)} model(s) found"}
except requests.exceptions.ConnectionError:
return JSONResponse(
{"ok": False, "error": "Connection refused — is the server running?"},
status_code=502,
)
except requests.exceptions.Timeout:
return JSONResponse(
{"ok": False, "error": "Connection timed out"},
status_code=504,
)
except Exception as e:
return JSONResponse(
{"ok": False, "error": str(e)},
status_code=500,
)
@app.post("/api/settings/models")
def api_fetch_models(body: FetchModelsRequest):
"""Fetch available models for a provider (proxied to avoid CORS)."""
existing = _load_llm_settings()
providers = json.loads(json.dumps(body.providers))
_merge_api_keys(providers, existing.get("providers", {}))
try:
models = _fetch_models(body.provider, providers)
return {"ok": True, "models": models}
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
def _fetch_models(provider, providers):
"""Fetch model list from a provider. Returns list of {id, name}."""
cfg = providers.get(provider, {})
if provider == "ollama":
base_url = cfg.get("base_url", "http://100.100.242.21:11434")
resp = requests.get(f"{base_url}/api/tags", timeout=10)
resp.raise_for_status()
data = resp.json()
return [{"id": m["name"], "name": m["name"]} for m in data.get("models", [])]
elif provider == "lmstudio":
base_url = cfg.get("base_url", "http://100.100.242.21:1234")
resp = requests.get(f"{base_url}/v1/models", timeout=10)
resp.raise_for_status()
data = resp.json()
return [{"id": m["id"], "name": m["id"]} for m in data.get("data", [])]
elif provider == "openai":
api_key = cfg.get("api_key", "")
if not api_key:
raise ValueError("OpenAI API key is required")
resp = requests.get(
"https://api.openai.com/v1/models",
headers={"Authorization": f"Bearer {api_key}"},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
models = [m for m in data.get("data", []) if m["id"].startswith("gpt-") or "chat" in m.get("id", "")]
models.sort(key=lambda m: m["id"])
return [{"id": m["id"], "name": m["id"]} for m in models]
elif provider == "anthropic":
api_key = cfg.get("api_key", "")
if not api_key:
raise ValueError("Anthropic API key is required")
resp = requests.get(
"https://api.anthropic.com/v1/models",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
return [{"id": m["id"], "name": m.get("display_name", m["id"])} for m in data.get("data", [])]
elif provider == "openrouter":
resp = requests.get("https://openrouter.ai/api/v1/models", timeout=15)
resp.raise_for_status()
data = resp.json()
models = data.get("data", [])
models.sort(key=lambda m: m.get("id", ""))
return [{"id": m["id"], "name": m.get("name", m["id"])} for m in models[:200]]
else:
raise ValueError(f"Unknown provider: {provider}")
# ── HTML Pages ────────────────────────────────────────────────────────────
# Shared CSS used by both pages
SHARED_CSS = """
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0f172a;--card:#1e293b;--card-hover:#253349;--text:#e2e8f0;--text-dim:#94a3b8;--accent:#f7931a;--green:#22c55e;--red:#ef4444;--yellow:#eab308;--border:#334155;--mono:'JetBrains Mono','Fira Code','Courier New',monospace} :root{--bg:#0f172a;--card:#1e293b;--card-hover:#253349;--text:#e2e8f0;--text-dim:#94a3b8;--accent:#f7931a;--green:#22c55e;--red:#ef4444;--yellow:#eab308;--border:#334155;--mono:'JetBrains Mono','Fira Code','Courier New',monospace;--cyan:#22d3ee}
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh} body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.container{max-width:1400px;margin:0 auto;padding:16px} .container{max-width:1400px;margin:0 auto;padding:16px}
h1{font-size:1.5rem;font-weight:700;display:flex;align-items:center;gap:10px} h1{font-size:1.5rem;font-weight:700;display:flex;align-items:center;gap:10px}
h1 .btc{color:var(--accent);font-size:1.8rem} h1 .btc{color:var(--accent);font-size:1.8rem}
h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:.05em;font-size:.8rem} h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-transform:uppercase;letter-spacing:.05em;font-size:.8rem}
.header{display:flex;justify-content:space-between;align-items:center;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:16px;flex-wrap:wrap;gap:12px} .header{display:flex;justify-content:space-between;align-items:center;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:16px;flex-wrap:wrap;gap:12px}
.nav{display:flex;gap:4px;align-items:center}
.nav a{color:var(--text-dim);text-decoration:none;font-size:.85rem;font-weight:600;padding:6px 14px;border-radius:6px;transition:all .15s}
.nav a:hover{color:var(--text);background:var(--card)}
.nav a.active{color:var(--cyan);background:var(--card);border:1px solid var(--border)}
.controls{display:flex;gap:8px;align-items:center} .controls{display:flex;gap:8px;align-items:center}
.btn{padding:8px 18px;border:none;border-radius:6px;font-family:inherit;font-weight:600;font-size:.85rem;cursor:pointer;transition:all .15s} .btn{padding:8px 18px;border:none;border-radius:6px;font-family:inherit;font-weight:600;font-size:.85rem;cursor:pointer;transition:all .15s}
.btn-start{background:var(--green);color:#000}.btn-start:hover{background:#16a34a} .btn-start{background:var(--green);color:#000}.btn-start:hover{background:#16a34a}
.btn-stop{background:var(--red);color:#fff}.btn-stop:hover{background:#dc2626} .btn-stop{background:var(--red);color:#fff}.btn-stop:hover{background:#dc2626}
.btn-secondary{background:var(--border);color:var(--text)}.btn-secondary:hover{background:var(--card-hover)} .btn-secondary{background:var(--border);color:var(--text)}.btn-secondary:hover{background:var(--card-hover)}
.btn-accent{background:var(--accent);color:#000}.btn-accent:hover{background:#e8850f}
.btn-cyan{background:var(--cyan);color:#000}.btn-cyan:hover{background:#06b6d4}
.btn:disabled{opacity:.4;cursor:not-allowed} .btn:disabled{opacity:.4;cursor:not-allowed}
.status-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;font-size:.8rem;font-weight:600} .status-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;font-size:.8rem;font-weight:600}
.status-idle{background:#1e3a5f;color:#60a5fa} .status-idle{background:#1e3a5f;color:#60a5fa}
.status-running{background:#1a3a2a;color:var(--green)} .status-running{background:#1a3a2a;color:var(--green)}
@ -143,17 +330,57 @@ h2{font-size:1rem;font-weight:600;color:var(--text-dim);margin-bottom:12px;text-
.status-error{background:#3a1a1a;color:var(--red)} .status-error{background:#3a1a1a;color:var(--red)}
.pulse{width:8px;height:8px;border-radius:50%;background:currentColor;animation:pulse 1.5s infinite} .pulse{width:8px;height:8px;border-radius:50%;background:currentColor;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.best-score{text-align:right} .best-score{text-align:right}
.best-score .label{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim)} .best-score .label{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim)}
.best-score .value{font-size:2.2rem;font-weight:700;color:var(--accent);font-family:var(--mono)} .best-score .value{font-size:2.2rem;font-weight:700;color:var(--accent);font-family:var(--mono)}
.best-score .unit{font-size:1rem;color:var(--text-dim)} .best-score .unit{font-size:1rem;color:var(--text-dim)}
.card{background:var(--card);border-radius:10px;padding:16px;border:1px solid var(--border)}
.footer{text-align:center;color:var(--text-dim);font-size:.75rem;padding:20px 0;margin-top:16px;border-top:1px solid var(--border)}
.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:8px;font-size:.85rem;font-weight:600;z-index:9999;opacity:0;transform:translateY(-10px);transition:all .3s;pointer-events:none}
.toast.show{opacity:1;transform:translateY(0)}
.toast-success{background:var(--green);color:#000}
.toast-error{background:var(--red);color:#fff}
"""
# Shared HTML head
SHARED_HEAD = """<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">"""
# Navigation bar HTML (parameterized via JS to highlight active page)
NAV_HTML = """<div class="nav">
<a href="/" id="nav-dashboard">Dashboard</a>
<a href="/settings" id="nav-settings">&#9881; Settings</a>
</div>"""
# Toast JS helper (shared)
TOAST_JS = """
function showToast(msg, type) {
let t = document.getElementById('toast');
if (!t) {
t = document.createElement('div');
t.id = 'toast';
t.className = 'toast';
document.body.appendChild(t);
}
t.textContent = msg;
t.className = 'toast toast-' + type + ' show';
setTimeout(() => { t.classList.remove('show'); }, 3500);
}
"""
DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
""" + SHARED_HEAD + """
<title>BTC Accumulation Signal Optimizer</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
""" + SHARED_CSS + """
.grid{display:grid;grid-template-columns:1fr 360px;gap:16px} .grid{display:grid;grid-template-columns:1fr 360px;gap:16px}
@media(max-width:900px){.grid{grid-template-columns:1fr}} @media(max-width:900px){.grid{grid-template-columns:1fr}}
.card{background:var(--card);border-radius:10px;padding:16px;border:1px solid var(--border)}
.table-wrap{overflow-x:auto;max-height:400px;overflow-y:auto} .table-wrap{overflow-x:auto;max-height:400px;overflow-y:auto}
table{width:100%;border-collapse:collapse;font-size:.82rem} table{width:100%;border-collapse:collapse;font-size:.82rem}
th{position:sticky;top:0;background:var(--card);text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;border-bottom:2px solid var(--border);font-size:.75rem;text-transform:uppercase;letter-spacing:.04em} th{position:sticky;top:0;background:var(--card);text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;border-bottom:2px solid var(--border);font-size:.75rem;text-transform:uppercase;letter-spacing:.04em}
@ -161,13 +388,10 @@ td{padding:7px 10px;border-bottom:1px solid var(--border);font-family:var(--mono
tr.best-row{background:rgba(34,197,94,.1)} tr.best-row{background:rgba(34,197,94,.1)}
tr.best-row td:first-child{border-left:3px solid var(--green)} tr.best-row td:first-child{border-left:3px solid var(--green)}
tr:hover{background:var(--card-hover)} tr:hover{background:var(--card-hover)}
.chart-container{position:relative;height:260px} .chart-container{position:relative;height:260px}
.llm-panel{max-height:500px;overflow-y:auto} .llm-panel{max-height:500px;overflow-y:auto}
.llm-entry{padding:10px;border-bottom:1px solid var(--border);font-size:.82rem;line-height:1.5} .llm-entry{padding:10px;border-bottom:1px solid var(--border);font-size:.82rem;line-height:1.5}
.llm-entry .iter-label{font-weight:600;color:var(--accent);font-size:.75rem;margin-bottom:4px} .llm-entry .iter-label{font-weight:600;color:var(--accent);font-size:.75rem;margin-bottom:4px}
.config-section{margin-top:16px} .config-section{margin-top:16px}
.config-toggle{cursor:pointer;user-select:none;display:flex;align-items:center;gap:6px} .config-toggle{cursor:pointer;user-select:none;display:flex;align-items:center;gap:6px}
.config-toggle .arrow{transition:transform .2s;font-size:.7rem} .config-toggle .arrow{transition:transform .2s;font-size:.7rem}
@ -176,12 +400,9 @@ tr:hover{background:var(--card-hover)}
.config-body.open{display:block} .config-body.open{display:block}
textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:12px;font-family:var(--mono);font-size:.8rem;resize:vertical} textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:12px;font-family:var(--mono);font-size:.8rem;resize:vertical}
.config-actions{display:flex;gap:8px;margin-top:8px} .config-actions{display:flex;gap:8px;margin-top:8px}
.downloads{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap} .downloads{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
.downloads a{color:var(--accent);text-decoration:none;font-size:.82rem;padding:6px 12px;border:1px solid var(--accent);border-radius:6px;transition:all .15s} .downloads a{color:var(--accent);text-decoration:none;font-size:.82rem;padding:6px 12px;border:1px solid var(--accent);border-radius:6px;transition:all .15s}
.downloads a:hover{background:var(--accent);color:#000} .downloads a:hover{background:var(--accent);color:#000}
.footer{text-align:center;color:var(--text-dim);font-size:.75rem;padding:20px 0;margin-top:16px;border-top:1px solid var(--border)}
</style> </style>
</head> </head>
<body> <body>
@ -190,14 +411,16 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
<div class="header"> <div class="header">
<div> <div>
<h1><span class="btc">&#x20BF;</span> Accumulation Signal Optimizer</h1> <h1><span class="btc">&#x20BF;</span> Accumulation Signal Optimizer</h1>
<div style="margin-top:8px"> <div style="margin-top:8px;display:flex;align-items:center;gap:12px">
<span id="statusBadge" class="status-badge status-idle"><span class="pulse"></span> Idle</span> <span id="statusBadge" class="status-badge status-idle"><span class="pulse"></span> Idle</span>
""" + NAV_HTML + """
</div> </div>
</div> </div>
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap">
<div class="controls"> <div class="controls">
<button id="btnStart" class="btn btn-start" onclick="startOpt()">Start Optimization</button> <button id="btnStart" class="btn btn-start" onclick="startOpt()">Start Optimization</button>
<button id="btnStop" class="btn btn-stop" onclick="stopOpt()" disabled>Stop</button> <button id="btnStop" class="btn btn-stop" onclick="stopOpt()" disabled>Stop</button>
<button id="btnClear" class="btn btn-secondary" onclick="clearHistory()" title="Clear all iteration history and start fresh">New Run</button>
</div> </div>
<div class="best-score"> <div class="best-score">
<div class="label">Best Cost Improvement</div> <div class="label">Best Cost Improvement</div>
@ -262,6 +485,8 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
</div> </div>
<script> <script>
document.getElementById('nav-dashboard').classList.add('active');
""" + TOAST_JS + """
let chart = null; let chart = null;
let pollInterval = null; let pollInterval = null;
@ -324,6 +549,7 @@ function updateStatusBadge(status) {
document.getElementById('btnStart').disabled = (state === 'running'); document.getElementById('btnStart').disabled = (state === 'running');
document.getElementById('btnStop').disabled = (state !== 'running'); document.getElementById('btnStop').disabled = (state !== 'running');
document.getElementById('btnClear').disabled = (state === 'running');
document.getElementById('bestScore').innerHTML = (status.best_score || 0).toFixed(1) + '<span class="unit">%</span>'; document.getElementById('bestScore').innerHTML = (status.best_score || 0).toFixed(1) + '<span class="unit">%</span>';
} }
@ -406,6 +632,18 @@ async function stopOpt() {
setTimeout(poll, 500); setTimeout(poll, 500);
} }
async function clearHistory() {
if (!confirm('Clear ALL iteration history and start a fresh run? This cannot be undone.')) return;
try {
const r = await fetch('/api/clear', { method: 'POST' });
const d = await r.json();
if (d.ok) {
document.getElementById('llmPanel').innerHTML = '<div style="color:var(--text-dim);font-size:.82rem;padding:10px">History cleared. Ready for fresh run.</div>';
setTimeout(poll, 500);
} else { alert('Failed: ' + (d.detail || 'unknown')); }
} catch(e) { alert('Failed: ' + e); }
}
function toggleConfig() { function toggleConfig() {
const toggle = document.getElementById('configToggle'); const toggle = document.getElementById('configToggle');
const body = document.getElementById('configBody'); const body = document.getElementById('configBody');
@ -432,8 +670,8 @@ async function updateConfig() {
body: JSON.stringify({ config }) body: JSON.stringify({ config })
}); });
const d = await r.json(); const d = await r.json();
if (d.ok) alert('Config updated!'); if (d.ok) showToast('Config updated!', 'success');
} catch(e) { alert('Invalid JSON or error: ' + e); } } catch(e) { showToast('Invalid JSON or error: ' + e, 'error'); }
} }
async function resetConfig() { async function resetConfig() {
@ -449,12 +687,350 @@ pollInterval = setInterval(poll, 10000);
</html>""" </html>"""
SETTINGS_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
""" + SHARED_HEAD + """
<title>Settings BTC Accumulation Signal Optimizer</title>
<style>
""" + SHARED_CSS + """
.settings-grid{display:grid;grid-template-columns:320px 1fr;gap:16px;margin-top:16px}
@media(max-width:800px){.settings-grid{grid-template-columns:1fr}}
.provider-list{display:flex;flex-direction:column;gap:6px}
.provider-option{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:8px;border:1px solid var(--border);cursor:pointer;transition:all .15s;background:var(--card)}
.provider-option:hover{border-color:var(--text-dim)}
.provider-option.selected{border-color:var(--cyan);background:#0f2a3a}
.provider-option input[type=radio]{accent-color:var(--cyan);width:16px;height:16px}
.provider-option .provider-name{font-weight:600;font-size:.9rem}
.provider-option .provider-type{font-size:.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em}
.field-group{margin-bottom:16px}
.field-group label{display:block;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);margin-bottom:6px}
.field-group input,.field-group select{width:100%;padding:10px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-family:var(--mono);font-size:.85rem}
.field-group input:focus,.field-group select:focus{outline:none;border-color:var(--cyan)}
.field-group select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2394a3b8' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px}
.model-select-wrap{position:relative}
.model-spinner{display:none;position:absolute;right:36px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--cyan);border-radius:50%;animation:spin .6s linear infinite}
.model-spinner.active{display:block}
@keyframes spin{to{transform:translateY(-50%) rotate(360deg)}}
.btn-row{display:flex;gap:8px;margin-top:20px;flex-wrap:wrap}
.current-provider{font-size:.8rem;color:var(--text-dim);margin-top:4px;font-family:var(--mono)}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1><span class="btc">&#x20BF;</span> Accumulation Signal Optimizer</h1>
<div style="margin-top:8px;display:flex;align-items:center;gap:12px">
""" + NAV_HTML + """
</div>
</div>
</div>
<div class="card">
<h2>&#9881; LLM Provider Settings</h2>
<p class="current-provider" id="currentProvider"></p>
<div class="settings-grid">
<div>
<h2 style="margin-top:8px">Provider</h2>
<div class="provider-list" id="providerList">
<label class="provider-option" data-provider="ollama">
<input type="radio" name="provider" value="ollama">
<div><div class="provider-name">Ollama</div><div class="provider-type">Local</div></div>
</label>
<label class="provider-option" data-provider="lmstudio">
<input type="radio" name="provider" value="lmstudio">
<div><div class="provider-name">LM Studio</div><div class="provider-type">Local</div></div>
</label>
<label class="provider-option" data-provider="openai">
<input type="radio" name="provider" value="openai">
<div><div class="provider-name">OpenAI</div><div class="provider-type">Cloud</div></div>
</label>
<label class="provider-option" data-provider="anthropic">
<input type="radio" name="provider" value="anthropic">
<div><div class="provider-name">Anthropic</div><div class="provider-type">Cloud</div></div>
</label>
<label class="provider-option" data-provider="openrouter">
<input type="radio" name="provider" value="openrouter">
<div><div class="provider-name">OpenRouter</div><div class="provider-type">Cloud</div></div>
</label>
</div>
</div>
<div>
<h2 style="margin-top:8px">Connection</h2>
<div class="field-group" id="fieldBaseUrl" style="display:none">
<label>Base URL</label>
<input type="text" id="inputBaseUrl" placeholder="http://localhost:11434">
</div>
<div class="field-group" id="fieldApiKey" style="display:none">
<label>API Key</label>
<input type="password" id="inputApiKey" placeholder="sk-...">
</div>
<div class="field-group">
<label>Model</label>
<div class="model-select-wrap">
<select id="selectModel"><option value=""> select provider first </option></select>
<div class="model-spinner" id="modelSpinner"></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-cyan" onclick="testConnection()">Test Connection</button>
<button class="btn btn-accent" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</div>
</div>
<div class="footer">BTC Accumulation Signal Optimizer &mdash; VPS &rarr; Windows GPU &rarr; Mac Mini LLM</div>
</div>
<script>
document.getElementById('nav-settings').classList.add('active');
""" + TOAST_JS + """
let settings = null;
const PROVIDER_FIELDS = {
ollama: { baseUrl: true, apiKey: false, defaultUrl: 'http://100.100.242.21:11434' },
lmstudio: { baseUrl: true, apiKey: false, defaultUrl: 'http://100.100.242.21:1234' },
openai: { baseUrl: false, apiKey: true },
anthropic: { baseUrl: false, apiKey: true },
openrouter: { baseUrl: false, apiKey: true },
};
function getSelectedProvider() {
const r = document.querySelector('input[name=provider]:checked');
return r ? r.value : null;
}
function buildProviders() {
// Build providers dict from current UI state, merging with loaded settings
const p = settings ? JSON.parse(JSON.stringify(settings.providers)) : {};
const prov = getSelectedProvider();
if (!prov) return p;
if (!p[prov]) p[prov] = {};
const fields = PROVIDER_FIELDS[prov];
if (fields.baseUrl) {
p[prov].base_url = document.getElementById('inputBaseUrl').value;
}
if (fields.apiKey) {
const v = document.getElementById('inputApiKey').value;
if (v) p[prov].api_key = v;
}
return p;
}
function selectProvider(prov) {
// Highlight the selected option
document.querySelectorAll('.provider-option').forEach(el => {
el.classList.toggle('selected', el.dataset.provider === prov);
});
document.querySelector('input[name=provider][value="' + prov + '"]').checked = true;
const fields = PROVIDER_FIELDS[prov];
const baseUrlField = document.getElementById('fieldBaseUrl');
const apiKeyField = document.getElementById('fieldApiKey');
baseUrlField.style.display = fields.baseUrl ? 'block' : 'none';
apiKeyField.style.display = fields.apiKey ? 'block' : 'none';
// Populate from settings
if (settings && settings.providers[prov]) {
const cfg = settings.providers[prov];
if (fields.baseUrl) {
document.getElementById('inputBaseUrl').value = cfg.base_url || fields.defaultUrl || '';
}
if (fields.apiKey) {
document.getElementById('inputApiKey').value = cfg.api_key || '';
}
} else {
if (fields.baseUrl) document.getElementById('inputBaseUrl').value = fields.defaultUrl || '';
if (fields.apiKey) document.getElementById('inputApiKey').value = '';
}
// Reset model dropdown
document.getElementById('selectModel').innerHTML = '<option value="">— click Test Connection to load models —</option>';
}
// Provider click handlers
document.querySelectorAll('.provider-option').forEach(el => {
el.addEventListener('click', () => selectProvider(el.dataset.provider));
});
async function loadSettings() {
try {
const r = await fetch('/api/settings');
settings = await r.json();
document.getElementById('currentProvider').textContent =
'Current: ' + settings.provider + ' / ' + settings.model;
selectProvider(settings.provider);
} catch(e) {
console.error('Failed to load settings:', e);
}
}
async function fetchModels(providerOverride) {
const prov = providerOverride || getSelectedProvider();
if (!prov) return;
const spinner = document.getElementById('modelSpinner');
const sel = document.getElementById('selectModel');
spinner.classList.add('active');
sel.innerHTML = '<option value="">Loading models...</option>';
try {
const r = await fetch('/api/settings/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: prov, providers: buildProviders() })
});
const data = await r.json();
if (!data.ok) {
sel.innerHTML = '<option value="">Error: ' + (data.error||'unknown') + '</option>';
return;
}
sel.innerHTML = '';
if (!data.models.length) {
sel.innerHTML = '<option value="">No models found</option>';
return;
}
for (const m of data.models) {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name !== m.id ? m.name + ' (' + m.id + ')' : m.id;
sel.appendChild(opt);
}
// Select current model if it's in the list
if (settings && settings.model) {
sel.value = settings.model;
}
} catch(e) {
sel.innerHTML = '<option value="">Fetch failed: ' + e + '</option>';
} finally {
spinner.classList.remove('active');
}
}
async function testConnection() {
const prov = getSelectedProvider();
if (!prov) { showToast('Select a provider first', 'error'); return; }
showToast('Testing connection...', 'success');
try {
const r = await fetch('/api/settings/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: prov, providers: buildProviders() })
});
const data = await r.json();
if (data.ok) {
showToast(data.message, 'success');
// Populate model dropdown
const sel = document.getElementById('selectModel');
sel.innerHTML = '';
for (const m of data.models) {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name !== m.id ? m.name + ' (' + m.id + ')' : m.id;
sel.appendChild(opt);
}
if (settings && settings.model) sel.value = settings.model;
} else {
showToast(data.error || 'Connection failed', 'error');
}
} catch(e) {
showToast('Connection failed: ' + e, 'error');
}
}
async function saveSettings() {
const prov = getSelectedProvider();
if (!prov) { showToast('Select a provider first', 'error'); return; }
const model = document.getElementById('selectModel').value;
if (!model) { showToast('Select a model first (use Test Connection to load models)', 'error'); return; }
try {
const r = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: prov,
model: model,
providers: buildProviders()
})
});
const data = await r.json();
if (data.ok) {
showToast('Settings saved!', 'success');
document.getElementById('currentProvider').textContent = 'Current: ' + prov + ' / ' + model;
// Reload settings to get masked keys
loadSettings();
} else {
showToast(data.error || 'Save failed', 'error');
}
} catch(e) {
showToast('Save failed: ' + e, 'error');
}
}
loadSettings();
</script>
</body>
</html>"""
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def dashboard(): def dashboard():
return DASHBOARD_HTML return DASHBOARD_HTML
@app.get("/settings", response_class=HTMLResponse)
def settings_page():
return SETTINGS_HTML
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3088) uvicorn.run(app, host="0.0.0.0", port=3088)
@app.post("/api/clear")
def api_clear():
"""Clear iteration history for a fresh run."""
import glob
results_dir = os.path.join(BASE_DIR, "results")
# Clear iterations log
log_path = os.path.join(results_dir, "iterations.jsonl")
if os.path.exists(log_path):
os.remove(log_path)
# Clear individual result files
for f in glob.glob(os.path.join(results_dir, "results_iter_*.json")):
os.remove(f)
# Reset best config to initial
initial = os.path.join(BASE_DIR, "config", "initial_config.json")
best = os.path.join(BASE_DIR, "config", "best_config.json")
current = os.path.join(BASE_DIR, "config", "current_config.json")
if os.path.exists(initial):
import shutil
shutil.copy(initial, best)
shutil.copy(initial, current)
# Clear in-memory status
orchestrator._status["llm_suggestions"] = []
orchestrator._status["best_score"] = 0.0
orchestrator._status["iteration"] = 0
return {"ok": True, "message": "History cleared. Ready for fresh run."}

View File

@ -1,15 +1,36 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
LLM Accumulation Signal Analyzer -- Calls Ollama on Mac Mini to analyze results LLM Accumulation Signal Analyzer -- Calls LLM to analyze results
and suggest config modifications for the next iteration. and suggest config modifications for the next iteration.
Supports multiple providers: Ollama, LM Studio, OpenAI, Anthropic, OpenRouter.
""" """
import json import json
import os
import re import re
import requests import requests
OLLAMA_URL = "http://100.100.242.21:11434" BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODEL = "qwen3.5:27b" LLM_SETTINGS_PATH = os.path.join(BASE_DIR, "config", "llm_settings.json")
# Fallback defaults
DEFAULT_OLLAMA_URL = "http://100.100.242.21:11434"
DEFAULT_MODEL = "qwen3.5:27b"
def load_llm_settings():
"""Load LLM settings from config file, with fallback to defaults."""
if os.path.exists(LLM_SETTINGS_PATH):
with open(LLM_SETTINGS_PATH) as f:
return json.load(f)
return {
"provider": "ollama",
"model": DEFAULT_MODEL,
"providers": {
"ollama": {"base_url": DEFAULT_OLLAMA_URL},
},
}
SYSTEM_PROMPT = """You are a quantitative analyst optimizing a BTC ACCUMULATION SIGNAL model. The goal is NOT day-trading -- it is finding statistically optimal times to BUY BTC for long-term holding. SYSTEM_PROMPT = """You are a quantitative analyst optimizing a BTC ACCUMULATION SIGNAL model. The goal is NOT day-trading -- it is finding statistically optimal times to BUY BTC for long-term holding.
@ -120,6 +141,119 @@ You MUST respond with ONLY a JSON object (no markdown, no explanation outside th
The "config" field must contain the COMPLETE config so it can be used directly.""" The "config" field must contain the COMPLETE config so it can be used directly."""
def _call_ollama(settings, messages):
"""Call Ollama API."""
provider_cfg = settings.get("providers", {}).get("ollama", {})
base_url = provider_cfg.get("base_url", DEFAULT_OLLAMA_URL)
model = settings.get("model", DEFAULT_MODEL)
payload = {
"model": model,
"messages": messages,
"stream": False,
"think": False,
"options": {"temperature": 0.7, "num_predict": 4096},
}
print(f" Calling LLM ({model} via Ollama at {base_url})...")
resp = requests.post(f"{base_url}/api/chat", json=payload, timeout=600)
resp.raise_for_status()
return resp.json()["message"]["content"]
def _call_openai_compatible(settings, messages, provider_name):
"""Call OpenAI-compatible API (LM Studio, OpenAI, OpenRouter)."""
provider_cfg = settings.get("providers", {}).get(provider_name, {})
model = settings.get("model", "")
if provider_name == "lmstudio":
base_url = provider_cfg.get("base_url", "http://100.100.242.21:1234")
url = f"{base_url}/v1/chat/completions"
headers = {"Content-Type": "application/json"}
elif provider_name == "openai":
url = "https://api.openai.com/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {provider_cfg.get('api_key', '')}",
}
elif provider_name == "openrouter":
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {provider_cfg.get('api_key', '')}",
}
else:
raise ValueError(f"Unknown OpenAI-compatible provider: {provider_name}")
payload = {
"model": model,
"messages": messages,
"temperature": 0.7,
"max_tokens": 4096,
}
print(f" Calling LLM ({model} via {provider_name})...")
resp = requests.post(url, json=payload, headers=headers, timeout=600)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def _call_anthropic(settings, messages):
"""Call Anthropic Messages API."""
provider_cfg = settings.get("providers", {}).get("anthropic", {})
model = settings.get("model", "claude-sonnet-4-20250514")
api_key = provider_cfg.get("api_key", "")
# Anthropic uses system as a top-level param, not in messages
system_msg = ""
api_messages = []
for m in messages:
if m["role"] == "system":
system_msg = m["content"]
else:
api_messages.append(m)
payload = {
"model": model,
"max_tokens": 4096,
"messages": api_messages,
}
if system_msg:
payload["system"] = system_msg
headers = {
"Content-Type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
}
print(f" Calling LLM ({model} via Anthropic)...")
resp = requests.post(
"https://api.anthropic.com/v1/messages",
json=payload,
headers=headers,
timeout=600,
)
resp.raise_for_status()
data = resp.json()
# Extract text from content blocks
return "".join(
block["text"] for block in data.get("content", []) if block.get("type") == "text"
)
def call_llm(messages):
"""Route LLM call to the configured provider."""
settings = load_llm_settings()
provider = settings.get("provider", "ollama")
if provider == "ollama":
return _call_ollama(settings, messages)
elif provider in ("lmstudio", "openai", "openrouter"):
return _call_openai_compatible(settings, messages, provider)
elif provider == "anthropic":
return _call_anthropic(settings, messages)
else:
raise ValueError(f"Unknown LLM provider: {provider}")
def analyze_and_suggest(current_config, results, iteration_history=None): def analyze_and_suggest(current_config, results, iteration_history=None):
""" """
Send current results to LLM and get suggested config modifications. Send current results to LLM and get suggested config modifications.
@ -161,24 +295,12 @@ def analyze_and_suggest(current_config, results, iteration_history=None):
{history_text} {history_text}
Analyze these results and suggest 1-3 specific modifications to the config. Return ONLY valid JSON.""" Analyze these results and suggest 1-3 specific modifications to the config. Return ONLY valid JSON."""
payload = { messages = [
"model": MODEL,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT}, {"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}, {"role": "user", "content": user_prompt},
], ]
"stream": False,
"think": False,
"options": {
"temperature": 0.7,
"num_predict": 4096,
},
}
print(f" Calling LLM ({MODEL} on Mac Mini)...") content = call_llm(messages)
resp = requests.post(f"{OLLAMA_URL}/api/chat", json=payload, timeout=600)
resp.raise_for_status()
content = resp.json()["message"]["content"]
# Strip thinking tags if present # Strip thinking tags if present
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip() content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
@ -207,7 +329,14 @@ Analyze these results and suggest 1-3 specific modifications to the config. Retu
changes = parsed.get("changes", []) changes = parsed.get("changes", [])
new_config = parsed.get("config", current_config) new_config = parsed.get("config", current_config)
required_keys = ["model_type", "features", "target", "hyperparameters", "strategy", "training"] required_keys = [
"model_type",
"features",
"target",
"hyperparameters",
"strategy",
"training",
]
for key in required_keys: for key in required_keys:
if key not in new_config: if key not in new_config:
new_config[key] = current_config[key] new_config[key] = current_config[key]
@ -218,6 +347,7 @@ Analyze these results and suggest 1-3 specific modifications to the config. Retu
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
config_path = sys.argv[1] if len(sys.argv) > 1 else "config/initial_config.json" config_path = sys.argv[1] if len(sys.argv) > 1 else "config/initial_config.json"
with open(config_path) as f: with open(config_path) as f:
config = json.load(f) config = json.load(f)
@ -234,8 +364,18 @@ if __name__ == "__main__":
"avg_score_at_actual_bottoms": 68.5, "avg_score_at_actual_bottoms": 68.5,
"avg_score_at_actual_tops": 35.2, "avg_score_at_actual_tops": 35.2,
"per_window_cost_improvement": [7.1, 9.3, 8.8, 10.2, 7.0], "per_window_cost_improvement": [7.1, 9.3, 8.8, 10.2, 7.0],
"score_distribution": {"0-20": 80, "20-40": 150, "40-60": 200, "60-80": 130, "80-100": 40}, "score_distribution": {
"feature_importances": {"dist_from_ath_pct": 0.18, "RSI_14": 0.12, "price_percentile_365": 0.10}, "0-20": 80,
"20-40": 150,
"40-60": 200,
"60-80": 130,
"80-100": 40,
},
"feature_importances": {
"dist_from_ath_pct": 0.18,
"RSI_14": 0.12,
"price_percentile_365": 0.10,
},
} }
new_config, reasoning = analyze_and_suggest(config, dummy_results) new_config, reasoning = analyze_and_suggest(config, dummy_results)

View File

@ -461,9 +461,22 @@ def run_optimization_loop(callback=None, config_override=None):
"iteration": iteration, "iteration": iteration,
"reasoning": reasoning, "reasoning": reasoning,
}) })
# Also persist LLM suggestion to iteration log
iter_data["llm_reasoning"] = reasoning
iter_data["llm_applied"] = True
config = new_config config = new_config
except Exception: except Exception as e:
import random import random, traceback
err_msg = f"LLM call failed: {type(e).__name__}: {e}"
print(f" WARNING: {err_msg}")
traceback.print_exc()
with _status_lock:
_status["llm_suggestions"].append({
"iteration": iteration,
"reasoning": f"ERROR: {err_msg} — using random perturbation",
})
iter_data["llm_reasoning"] = err_msg
iter_data["llm_applied"] = False
hp = config.get("hyperparameters", {}) hp = config.get("hyperparameters", {})
hp["learning_rate"] = hp.get("learning_rate", 0.01) * random.uniform(0.8, 1.2) hp["learning_rate"] = hp.get("learning_rate", 0.01) * random.uniform(0.8, 1.2)
hp["max_depth"] = max(3, min(10, hp.get("max_depth", 5) + random.choice([-1, 0, 1]))) hp["max_depth"] = max(3, min(10, hp.get("max_depth", 5) + random.choice([-1, 0, 1])))