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:
co-authored by
Claude Opus 4.6
parent
a21e635d9f
commit
560863fa0d
+88
-98
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user