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:
parent
c17b3b5167
commit
aba30f7718
Binary file not shown.
68
config/best_config.json
Normal file
68
config/best_config.json
Normal 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"
|
||||||
|
}
|
||||||
@ -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
21
config/llm_settings.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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">⚙ 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">₿</span> Accumulation Signal Optimizer</h1>
|
<h1><span class="btc">₿</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">₿</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>⚙ 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 — VPS → Windows GPU → 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."}
|
||||||
|
|||||||
Binary file not shown.
@ -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()
|
||||||
@ -196,7 +318,7 @@ Analyze these results and suggest 1-3 specific modifications to the config. Retu
|
|||||||
elif content[i] == "}":
|
elif content[i] == "}":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
parsed = json.loads(content[brace_start:i + 1])
|
parsed = json.loads(content[brace_start : i + 1])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ValueError("Could not find complete JSON in LLM response")
|
raise ValueError("Could not find complete JSON in LLM response")
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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])))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user