From aba30f771896d985ad920c9b61574f1194d5e05d Mon Sep 17 00:00:00 2001 From: BizzleBot Date: Fri, 20 Mar 2026 21:51:05 +0000 Subject: [PATCH] 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) --- __pycache__/orchestrator.cpython-313.pyc | Bin 27424 -> 28066 bytes config/best_config.json | 68 ++ config/current_config.json | 104 ++- config/llm_settings.json | 21 + dashboard/server.py | 630 +++++++++++++++++- .../__pycache__/analyzer.cpython-313.pyc | Bin 13363 -> 18025 bytes llm_client/analyzer.py | 188 +++++- orchestrator.py | 17 +- 8 files changed, 911 insertions(+), 117 deletions(-) create mode 100644 config/best_config.json create mode 100644 config/llm_settings.json diff --git a/__pycache__/orchestrator.cpython-313.pyc b/__pycache__/orchestrator.cpython-313.pyc index b604ee0faa8a6bf461e94b3afcad861500986995..b0bb0178c2b8ddd5415bd87f0af27fd8a99c8f58 100644 GIT binary patch delta 1945 zcmX|>T~HHO6vy}OX7dGv00{vEvLQr3NdOU$C`O=&R)vvm3?)PeAwVi5xI}GzX!>Ah z`d!=WR37@k*lEX^`cNnBw0&^KQD=>oImGTYCNmBZ$XcP|5|1kG=nGW%h7x=>A6`cTEhSBz9W82 zjKWiVZ%-*gSkv@8eM^Dg>Mahw-?UdGVRncq%A1J4we1L&@Sn14)>u`1X#pwW#YB~X zvSxWjjm#byzS&xZ_F_w$juADvNUKMevHis$`UEGpm7q)Uj0d~gsu~{FFj}ey=_vbt z@`Csyp&F4hjSQyXX`b0t>dlU~W!@ZqgWbL2vMMUjvTo_Yw^P&wXIGZkw|z%8sTHWV(_JQBSMexu%ieq;tP%s0{?)A zUhbYHI+oPmKhPn#okKpa$J-lAfY3QO*za+73qKIUQQFKMP2f0NZA;V6rbZG-8d_SK zTg=>{T++?mdF=#uXo!?@g45gW8{h`rg8z`v<@9@e-tEGd#3OtKfP*Upt^`WCf(RfAI188r#0X+y&rrvZ+b`UNi~~>s*Z`QqEgi*yX^{N@C*U2x z@?`^c3ot_vllcXw%iZO49f*niuaJFFLf6`O*P!6>`a9e&yM)`2JO=oQpooh}UC#dg zcvZ5?=k4+IcK8mv1;Nwp7Vdy8@H3EHUrK=MBXk3P0lW#g3%Cb}FD!l#eueNi!0!MF z;64Bjs&EDH2jBtVPrwm^7&;(4gy1g#oR;{yG6V(L|Cof#sbgTMSC}ImLmUBicIM!v zdKo^}sYy^1kDdI6^2PYGPLAG52fyolk3m{&>wepqNSUgR%Z5uvmFuGGONcU#Dhs2T zdMuvv&{URXVK+m`(ua42rG<+}@RRO$1F4iMXMsviDw!e1Y~z{kFUmgejhMEMnzn|F zmT!dfy7OtFtcK%~DW!TiVWe@SI+Si4Q<|d6%!pDys??7u4Y$;~Db<=#wsBNtil{0^ zRTWW#DOzAUedM$!RAmhr_RXb?Kz} z-e4xYqSaU{*(;Ucx|$&0C#A2dEks{aX>5h;we_`Ny^b_CGkZOwehUe1sKI+F$^>8jzY^A%u#LAb{lj9SqBUu4lwh2(~hpZT9B84-V&EA zMq=WUnW>AOFPh9`S>iXpkZO!ZKh4C*CMu;AiVpYU)cHaY!m_Za&(j8Na)0N4&VA0g zH}~AzE6eoq0yV80jWU5>?80Zm?Jd)$djWbj{vfcz(;IB5`2byu8;@o5^dbAZ{g&#%_kK1zs7v^@(}8rnYpf9oO2VRpH$yu_~faXr5?sXmd9^)9&hrEvDULs6C zAUBW|RO(x9^tMP2OwWJTZ0bq`eTh)I!moVuQGkM3!I)WDhnCKd5?_wunDFm^|ES}l7)KAHd%E_$*OF2 z$!fhJS)0vAY{61-VV6H>5mN;cRHiH^i|J|ss?sis*^Miuy>hsSPuH2lCVtr@hKtzx@XJ(U+Q=i~ zPI#Cr?7fi_G|5&+p3-Gs_BnQJbPru(L!;fkHQ3v<*n%I8CPA^bg#$wS#K36oU4_3j zlKYNu*R`E&6Xx3EC1ZoMvXP^vzuE+jS~4;b^3R`5@Fh!v{@fE8o(MM04paXH1D0mK diff --git a/config/best_config.json b/config/best_config.json new file mode 100644 index 0000000..b6bbe12 --- /dev/null +++ b/config/best_config.json @@ -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" +} \ No newline at end of file diff --git a/config/current_config.json b/config/current_config.json index 9ac6e87..b6bbe12 100644 --- a/config/current_config.json +++ b/config/current_config.json @@ -1,92 +1,68 @@ { - "model_type": "hybrid", + "model_type": "xgboost", "features": { - "technical_indicators": [ - "RSI_14", - "RSI_7", - "MACD_line", - "MACD_signal", - "MACD_hist", - "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, + "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": "classification", - "direction": "both", - "horizon_candles": 4, - "threshold_pct": 1.0 + "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.001, - "max_depth": 5, + "learning_rate": 0.01, + "max_depth": 4, "n_estimators": 300, "subsample": 0.8, "colsample_bytree": 0.8, - "min_child_weight": 5, + "min_child_weight": 20, "gamma": 0.3, - "reg_alpha": 0.1, - "reg_lambda": 5.0, + "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": 20, + "lstm_sequence_length": 30, "lstm_patience": 10 }, "strategy": { - "entry_threshold": 0.65, - "exit_type": "trailing_stop", - "stop_loss_pct": 2.0, - "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 + "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, - "rolling_window": true, - "rolling_train_size": 3000, - "rolling_test_size": 200 + "test_pct": 0.15 }, "timeframe": "4h" } \ No newline at end of file diff --git a/config/llm_settings.json b/config/llm_settings.json new file mode 100644 index 0000000..2e532c5 --- /dev/null +++ b/config/llm_settings.json @@ -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": "" + } + } +} diff --git a/dashboard/server.py b/dashboard/server.py index 72aa0e8..1df07ab 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -9,6 +9,7 @@ import os import sys import threading +import requests from fastapi import FastAPI from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from pydantic import BaseModel @@ -23,6 +24,7 @@ app = FastAPI(title="BTC Accumulation Signal Optimizer") CONFIG_DIR = os.path.join(BASE_DIR, "config") RESULTS_DIR = os.path.join(BASE_DIR, "results") ITERATIONS_LOG = os.path.join(RESULTS_DIR, "iterations.jsonl") +LLM_SETTINGS_PATH = os.path.join(CONFIG_DIR, "llm_settings.json") _opt_thread = None @@ -31,6 +33,64 @@ class ConfigUpdate(BaseModel): 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") def api_status(): return orchestrator.get_status() @@ -91,7 +151,11 @@ def api_best(): with open(best_path) as f: config = json.load(f) 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} @@ -110,32 +174,155 @@ def api_download_best_config(): return JSONResponse({"error": "No best config yet"}, status_code=404) -DASHBOARD_HTML = """ - - - - -BTC Accumulation Signal Optimizer - - - - @@ -190,14 +411,16 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--

Accumulation Signal Optimizer

-
+
Idle + """ + NAV_HTML + """
+
Best Cost Improvement
@@ -262,6 +485,8 @@ textarea.config-editor{width:100%;height:300px;background:var(--bg);color:var(--
+ +""" + + @app.get("/", response_class=HTMLResponse) def dashboard(): return DASHBOARD_HTML +@app.get("/settings", response_class=HTMLResponse) +def settings_page(): + return SETTINGS_HTML + + if __name__ == "__main__": import uvicorn 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."} diff --git a/llm_client/__pycache__/analyzer.cpython-313.pyc b/llm_client/__pycache__/analyzer.cpython-313.pyc index 8bf7f7af6ea8e5bf81bcbbcccf0be46ae14b6ea7..28a945a92505503347f89a42c16f7b0d3ef0d056 100644 GIT binary patch literal 18025 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.*?", "", 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] == "}": depth -= 1 if depth == 0: - parsed = json.loads(content[brace_start:i + 1]) + parsed = json.loads(content[brace_start : i + 1]) break else: 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", []) 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: if key not in new_config: 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__": import sys + config_path = sys.argv[1] if len(sys.argv) > 1 else "config/initial_config.json" with open(config_path) as f: config = json.load(f) @@ -234,8 +364,18 @@ if __name__ == "__main__": "avg_score_at_actual_bottoms": 68.5, "avg_score_at_actual_tops": 35.2, "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}, - "feature_importances": {"dist_from_ath_pct": 0.18, "RSI_14": 0.12, "price_percentile_365": 0.10}, + "score_distribution": { + "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) diff --git a/orchestrator.py b/orchestrator.py index daec0f2..4234a64 100755 --- a/orchestrator.py +++ b/orchestrator.py @@ -461,9 +461,22 @@ def run_optimization_loop(callback=None, config_override=None): "iteration": iteration, "reasoning": reasoning, }) + # Also persist LLM suggestion to iteration log + iter_data["llm_reasoning"] = reasoning + iter_data["llm_applied"] = True config = new_config - except Exception: - import random + except Exception as e: + 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["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])))