pivot: rewrite as BTC accumulation signal optimizer

Replace day-trading bot with long-term accumulation signal model.
Predicts optimal BUY times using forward return analysis at 7d/30d/90d
horizons, scoring each candle 0-100. Primary metric is now
cost_basis_improvement_pct (model buy price vs DCA).

- train_and_backtest.py: regression models (XGBoost/LSTM hybrid),
  accumulation-focused features (price position, momentum, volatility,
  volume, cycle), forward return targets, signal quality backtesting
- orchestrator.py: cost improvement scoring, signal count validation
- analyzer.py: accumulation-focused LLM system prompt
- dashboard: cost improvement display, signal metrics table
- config: new accumulation-focused parameters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BizzleBot
2026-03-19 23:51:43 +00:00
co-authored by Claude Opus 4.6
parent a21e635d9f
commit 560863fa0d
5 changed files with 829 additions and 797 deletions
+88 -98
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
BTC ML Trading Strategy Optimizer Orchestrator
BTC Accumulation Signal Optimizer -- Orchestrator
Coordinates the optimization loop across VPS, Windows PC (GPU), and Mac Mini (LLM).
"""
@@ -28,7 +28,8 @@ MAC_MINI_HOST = "bizzle@bizzles-mac-mini-1"
MAX_ITERATIONS = 50
CONVERGENCE_WINDOW = 5
CONVERGENCE_THRESHOLD = 0.01 # 1% improvement
TARGET_SHARPE = 3.0
TARGET_COST_IMPROVEMENT = 20.0 # 20% cost basis improvement = exceptional
MIN_SIGNAL_COUNT = 30 # Minimum strong buy signals for valid results
ML_TIMEOUT = 600 # 10 minutes
# Colors
@@ -98,7 +99,6 @@ def run_ml_training():
)
if result.returncode != 0:
raise RuntimeError(f"ML training failed:\n{result.stderr}\n{result.stdout}")
# Print training output
for line in result.stdout.strip().split("\n"):
log(f" {C.DIM}{line}", C.DIM)
return True
@@ -127,45 +127,53 @@ def check_convergence(history):
if len(history) < CONVERGENCE_WINDOW + 1:
return False, "Not enough iterations"
recent = history[-CONVERGENCE_WINDOW:]
sharpes = [h["sharpe"] for h in recent]
# Only consider valid results (enough signals)
valid = [h for h in history if h.get("signal_count", 0) >= MIN_SIGNAL_COUNT]
# Check if best sharpe exceeds target
best_sharpe = max(h["sharpe"] for h in history)
if best_sharpe >= TARGET_SHARPE:
return True, f"Target Sharpe reached: {best_sharpe:.3f}"
if not valid:
return False, "No valid results yet"
recent = history[-CONVERGENCE_WINDOW:]
scores = [h.get("cost_improvement", 0) for h in recent]
# Check if best score exceeds target
best_score = max(h.get("cost_improvement", 0) for h in valid)
if best_score >= TARGET_COST_IMPROVEMENT:
return True, f"Target cost improvement reached: {best_score:.1f}%"
# Check if improvement has stalled
best_recent = max(sharpes)
worst_recent = min(sharpes)
best_recent = max(scores)
worst_recent = min(scores)
if best_recent > 0 and (best_recent - worst_recent) / best_recent < CONVERGENCE_THRESHOLD:
return True, f"Converged: Sharpe variance < {CONVERGENCE_THRESHOLD*100}% over {CONVERGENCE_WINDOW} iterations"
return True, f"Converged: variance < {CONVERGENCE_THRESHOLD*100}% over {CONVERGENCE_WINDOW} iterations"
return False, ""
def print_header():
print(f"""
{C.BOLD}{C.CYAN}╔══════════════════════════════════════════════════╗
BTC ML Trading Strategy Optimizer ║
VPS Windows GPU Mac Mini LLM Loop
╚══════════════════════════════════════════════════╝{C.RESET}
{C.BOLD}{C.CYAN}========================================================
BTC Accumulation Signal Optimizer
VPS -> Windows GPU -> Mac Mini LLM -> Loop
========================================================{C.RESET}
""")
def print_results(results, iteration):
sharpe = results.get("sharpe_ratio", 0)
sharpe_color = C.GREEN if sharpe > 1.5 else C.YELLOW if sharpe > 1.0 else C.RED
cost_imp = results.get("cost_basis_improvement_pct", 0)
color = C.GREEN if cost_imp > 15 else C.YELLOW if cost_imp > 10 else C.RED
print(f"""
{C.BOLD}━━━ Iteration {iteration} Results ━━━{C.RESET}
Sharpe Ratio: {sharpe_color}{C.BOLD}{sharpe:.3f}{C.RESET}
Total Return: {results.get('total_return_pct', 0):.1f}%
Max Drawdown: {results.get('max_drawdown_pct', 0):.1f}%
Win Rate: {results.get('win_rate', 0):.1%}
Trade Count: {results.get('trade_count', 0)}
Profit Factor: {results.get('profit_factor', 0):.3f}
Avg Duration: {results.get('avg_trade_duration_candles', 0):.1f} candles
Window Sharpes: {results.get('per_window_sharpe', [])}
{C.BOLD}--- Iteration {iteration} Results ---{C.RESET}
Cost Improvement: {color}{C.BOLD}{cost_imp:.1f}%{C.RESET}
Avg Cost (Model): ${results.get('avg_cost_basis_model', 0):,.2f}
Avg Cost (DCA): ${results.get('avg_cost_basis_dca', 0):,.2f}
Strong Signals: {results.get('strong_buy_signal_count', 0)}
Signal Frequency: {results.get('signal_frequency_pct', 0):.1f}%
Quality Score: {results.get('pct_quality_strong_buy', 0):.1%}
Model R2: {results.get('model_r2_score', 0):.4f}
Score@Bottoms: {results.get('avg_score_at_actual_bottoms', 0):.1f}
Score@Tops: {results.get('avg_score_at_actual_tops', 0):.1f}
Window Improvements: {results.get('per_window_cost_improvement', [])}
""")
@@ -173,14 +181,11 @@ def main():
print_header()
os.makedirs(RESULTS_DIR, exist_ok=True)
# Step 1: Ensure data
ensure_data()
# Step 2: Load or create initial config
config_path = os.path.join(CONFIG_DIR, "initial_config.json")
best_config_path = os.path.join(CONFIG_DIR, "best_config.json")
# Resume from best config if it exists
if os.path.exists(best_config_path):
log("Resuming from best_config.json", C.GREEN)
with open(best_config_path) as f:
@@ -191,29 +196,24 @@ def main():
history = load_iteration_history()
start_iter = len(history) + 1
best_sharpe = max((h["sharpe"] for h in history), default=0)
best_score = max((h.get("cost_improvement", 0) for h in history), default=0)
log(f"Starting at iteration {start_iter}, best Sharpe so far: {best_sharpe:.3f}", C.BOLD)
log(f"Starting at iteration {start_iter}, best cost improvement so far: {best_score:.1f}%", C.BOLD)
# Step 3: Setup Windows remote
setup_windows_remote()
# SCP the ML engine script (once)
log("Uploading ML engine to Windows...", C.CYAN)
scp_to_windows(os.path.join(BASE_DIR, "ml_engine", "train_and_backtest.py"), "train_and_backtest.py")
# SCP data files (once)
for tf in ["1h", "4h"]:
data_file = os.path.join(DATA_DIR, f"btc_{tf}.csv")
if os.path.exists(data_file):
log(f"Uploading btc_{tf}.csv to Windows...", C.CYAN)
scp_to_windows(data_file, f"btc_{tf}.csv")
# Import LLM analyzer
sys.path.insert(0, os.path.join(BASE_DIR, "llm_client"))
from analyzer import analyze_and_suggest
# Main optimization loop
for iteration in range(start_iter, MAX_ITERATIONS + 1):
log(f"\n{'='*50}", C.BOLD)
log(f"ITERATION {iteration}/{MAX_ITERATIONS}", f"{C.BOLD}{C.CYAN}")
@@ -222,13 +222,11 @@ def main():
f"Depth: {config.get('hyperparameters', {}).get('max_depth', '?')}", C.DIM)
log(f"{'='*50}", C.BOLD)
# Write current config to temp file and SCP
tmp_config = os.path.join(BASE_DIR, "config", "current_config.json")
with open(tmp_config, "w") as f:
json.dump(config, f, indent=2)
scp_to_windows(tmp_config, "config.json")
# Run ML training on Windows
try:
run_ml_training()
except (RuntimeError, subprocess.TimeoutExpired) as e:
@@ -238,7 +236,6 @@ def main():
config = history[-1].get("config", config)
continue
# Fetch results from Windows
results_local = os.path.join(RESULTS_DIR, f"results_iter_{iteration}.json")
scp_from_windows("results.json", results_local)
@@ -247,34 +244,35 @@ def main():
print_results(results, iteration)
# Track best
current_sharpe = results.get("sharpe_ratio", 0)
is_best = current_sharpe > best_sharpe
current_score = results.get("cost_basis_improvement_pct", 0)
signal_count = results.get("strong_buy_signal_count", 0)
is_best = current_score > best_score and signal_count >= MIN_SIGNAL_COUNT
if is_best:
best_sharpe = current_sharpe
best_score = current_score
with open(best_config_path, "w") as f:
json.dump(config, f, indent=2)
log(f"NEW BEST! Sharpe: {best_sharpe:.3f}", f"{C.BOLD}{C.GREEN}")
log(f"NEW BEST! Cost Improvement: {best_score:.1f}%", f"{C.BOLD}{C.GREEN}")
# Log iteration
iter_data = {
"iteration": iteration,
"timestamp": datetime.now(timezone.utc).isoformat(),
"sharpe": current_sharpe,
"return": results.get("total_return_pct", 0),
"max_drawdown": results.get("max_drawdown_pct", 0),
"win_rate": results.get("win_rate", 0),
"trades": results.get("trade_count", 0),
"profit_factor": results.get("profit_factor", 0),
"cost_improvement": current_score,
"avg_30d_return": results.get("avg_quality_score_strong_buy", 0),
"avg_90d_return": results.get("pct_quality_strong_buy", 0),
"signal_count": signal_count,
"signal_frequency": results.get("signal_frequency_pct", 0),
"r2_score": results.get("model_r2_score", 0),
"score_at_bottoms": results.get("avg_score_at_actual_bottoms", 0),
"score_at_tops": results.get("avg_score_at_actual_tops", 0),
"model_type": config.get("model_type", "unknown"),
"is_best": is_best,
"config": config,
"results": results,
}
save_iteration(iter_data)
history.append(iter_data)
# Check convergence
converged, reason = check_convergence(history)
if converged:
log(f"\nOptimization converged: {reason}", f"{C.BOLD}{C.GREEN}")
@@ -284,17 +282,15 @@ def main():
log(f"\nMax iterations ({MAX_ITERATIONS}) reached.", C.YELLOW)
break
# Ask LLM for next config
log("\nConsulting LLM for strategy modifications...", C.MAGENTA)
try:
summary_history = [
{
"iteration": h["iteration"],
"sharpe": h["sharpe"],
"return": h["return"],
"win_rate": h["win_rate"],
"trades": h["trades"],
"model_type": h["model_type"],
"cost_improvement": h.get("cost_improvement", 0),
"signal_count": h.get("signal_count", 0),
"r2_score": h.get("r2_score", 0),
"model_type": h.get("model_type", "unknown"),
}
for h in history
]
@@ -304,37 +300,34 @@ def main():
except Exception as e:
log(f"LLM call failed: {e}", C.RED)
log("Continuing with current config + random perturbation...", C.YELLOW)
# Small random perturbation as fallback
import random
hp = config.get("hyperparameters", {})
hp["learning_rate"] = hp.get("learning_rate", 0.05) * random.uniform(0.8, 1.2)
hp["max_depth"] = max(3, min(10, hp.get("max_depth", 6) + random.choice([-1, 0, 1])))
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])))
config["hyperparameters"] = hp
# Final summary
print(f"""
{C.BOLD}{C.GREEN}╔══════════════════════════════════════════════════╗
Optimization Complete!
╚══════════════════════════════════════════════════╝{C.RESET}
{C.BOLD}{C.GREEN}========================================================
Optimization Complete!
========================================================{C.RESET}
Total Iterations: {len(history)}
Best Sharpe: {C.BOLD}{best_sharpe:.3f}{C.RESET}
Best Config: {best_config_path}
Iteration Log: {ITERATIONS_LOG}
Total Iterations: {len(history)}
Best Cost Improvement: {C.BOLD}{best_score:.1f}%{C.RESET}
Best Config: {best_config_path}
Iteration Log: {ITERATIONS_LOG}
""")
# --- Library API for dashboard integration ---
# Shared state for dashboard
_stop_event = threading.Event()
_status = {
"state": "idle", # idle, running, completed, error
"state": "idle",
"iteration": 0,
"max_iterations": MAX_ITERATIONS,
"best_sharpe": 0.0,
"best_score": 0.0,
"error": None,
"llm_suggestions": [], # list of {iteration, reasoning, changes}
"llm_suggestions": [],
}
_status_lock = threading.Lock()
@@ -352,15 +345,9 @@ def update_status(**kwargs):
def run_optimization_loop(callback=None, config_override=None):
"""
Run the optimization loop. Designed to be called from a background thread.
Args:
callback: Called after each iteration with (iteration_number, iter_data_dict).
config_override: Optional dict to use instead of loading from disk.
"""
"""Run the optimization loop from a background thread."""
_stop_event.clear()
update_status(state="running", iteration=0, error=None, best_sharpe=0.0)
update_status(state="running", iteration=0, error=None, best_score=0.0)
try:
os.makedirs(RESULTS_DIR, exist_ok=True)
@@ -380,8 +367,8 @@ def run_optimization_loop(callback=None, config_override=None):
history = load_iteration_history()
start_iter = len(history) + 1
best_sharpe = max((h["sharpe"] for h in history), default=0)
update_status(best_sharpe=best_sharpe)
best_score = max((h.get("cost_improvement", 0) for h in history), default=0)
update_status(best_score=best_score)
setup_windows_remote()
scp_to_windows(os.path.join(BASE_DIR, "ml_engine", "train_and_backtest.py"), "train_and_backtest.py")
@@ -418,23 +405,26 @@ def run_optimization_loop(callback=None, config_override=None):
with open(results_local) as f:
results = json.load(f)
current_sharpe = results.get("sharpe_ratio", 0)
is_best = current_sharpe > best_sharpe
current_score = results.get("cost_basis_improvement_pct", 0)
signal_count = results.get("strong_buy_signal_count", 0)
is_best = current_score > best_score and signal_count >= MIN_SIGNAL_COUNT
if is_best:
best_sharpe = current_sharpe
best_score = current_score
with open(best_config_path, "w") as f:
json.dump(config, f, indent=2)
update_status(best_sharpe=best_sharpe)
update_status(best_score=best_score)
iter_data = {
"iteration": iteration,
"timestamp": datetime.now(timezone.utc).isoformat(),
"sharpe": current_sharpe,
"return": results.get("total_return_pct", 0),
"max_drawdown": results.get("max_drawdown_pct", 0),
"win_rate": results.get("win_rate", 0),
"trades": results.get("trade_count", 0),
"profit_factor": results.get("profit_factor", 0),
"cost_improvement": current_score,
"signal_count": signal_count,
"signal_frequency": results.get("signal_frequency_pct", 0),
"r2_score": results.get("model_r2_score", 0),
"score_at_bottoms": results.get("avg_score_at_actual_bottoms", 0),
"score_at_tops": results.get("avg_score_at_actual_tops", 0),
"quality": results.get("pct_quality_strong_buy", 0),
"model_type": config.get("model_type", "unknown"),
"is_best": is_best,
"config": config,
@@ -459,10 +449,10 @@ def run_optimization_loop(callback=None, config_override=None):
update_status(state="completed")
return
# LLM suggestion
try:
summary_history = [
{k: h[k] for k in ("iteration", "sharpe", "return", "win_rate", "trades", "model_type")}
{k: h[k] for k in ("iteration", "cost_improvement", "signal_count", "r2_score", "model_type")
if k in h}
for h in history
]
new_config, reasoning = analyze_and_suggest(config, results, summary_history)
@@ -475,8 +465,8 @@ def run_optimization_loop(callback=None, config_override=None):
except Exception:
import random
hp = config.get("hyperparameters", {})
hp["learning_rate"] = hp.get("learning_rate", 0.05) * random.uniform(0.8, 1.2)
hp["max_depth"] = max(3, min(10, hp.get("max_depth", 6) + random.choice([-1, 0, 1])))
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])))
config["hyperparameters"] = hp
update_status(state="completed")