#!/usr/bin/env python3 """ WORKING Policy Engine Batch Runner v3.0 Based on successful DEBUG_TEST backtest """ import json import subprocess import uuid import os import re import time from pathlib import Path from datetime import datetime from typing import Dict CONFIG = { "total_runs": 320, "target_valid": 300, "pairs": ["BTC/USDT", "ETH/USDT", "SOL/USDT", "ADA/USDT", "AVAX/USDT"], "fee": 0.0015, "min_trades": 200, "max_drawdown": 0.25, "min_pf": 1.10, "output_dir": "/home/node/.openclaw/workspace/bob_quant/policy_dataset_final", } class WorkingBatchRunner: def __init__(self): self.output_dir = Path(CONFIG["output_dir"]) self.output_dir.mkdir(parents=True, exist_ok=True) self.runs_dir = self.output_dir / "runs" self.runs_dir.mkdir(exist_ok=True) self.stats = {"total": 0, "completed": 0, "positive": 0, "negative": 0, "low": 0, "failed": 0} def get_params(self, run_num: int) -> Dict: """Generate varied params - simple EMA variations that produce trades""" import random random.seed(run_num * 42) # Different seed pattern # Key insight from DEBUG_TEST: 0.995/0.985 with EMA 20 gives 345 trades # Vary around this sweet spot return { "ema_period": random.choice([15, 18, 20, 22, 25, 30]), "entry_mult": round(random.uniform(0.993, 0.997), 4), # Tighter = more trades "exit_mult": round(random.uniform(0.983, 0.987), 4), "stoploss": round(random.uniform(-0.05, -0.02), 3), "trailing": round(random.uniform(0.008, 0.020), 3), "family": random.choice([ "ema_trend", "ema_relaxed", "ema_tight", "ema_volatility", "trend_follow", "momentum_ema", "breakout_ema", "hybrid_ema" ]), "timeframe": random.choice(["1h", "2h", "4h"]), } def generate_strategy(self, params: Dict, run_id: str) -> str: """Generate EMA strategy - proven to produce trades""" class_name = f"POL{run_id}" return f'''from freqtrade.strategy import IStrategy from pandas import DataFrame import talib.abstract as ta class {class_name}(IStrategy): timeframe = '{params["timeframe"]}' stoploss = {params["stoploss"]:.4f} trailing_stop = True trailing_stop_positive = {params["trailing"]:.4f} minimal_roi = {{"0": 0.03, "30": 0.015, "60": 0.008}} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['ema'] = ta.EMA(dataframe, timeperiod={params["ema_period"]}) return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, 'buy'] = 0 dataframe.loc[dataframe['close'] > dataframe['ema'] * {params["entry_mult"]:.4f}, 'buy'] = 1 return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, 'sell'] = 0 dataframe.loc[dataframe['close'] < dataframe['ema'] * {params["exit_mult"]:.4f}, 'sell'] = 1 return dataframe ''' def run_backtest(self, params: Dict, run_id: str) -> Dict: """Execute backtest with proper parsing""" try: class_name = f"POL{run_id}" strategy_code = self.generate_strategy(params, run_id) # Write strategy strategy_path = Path(f"/opt/docker/freqtrade/shared_data/user_data/strategies/{class_name}.py") with open(strategy_path, 'w') as f: f.write(strategy_code) # Write config config = { "max_open_trades": 5, "stake_currency": "USDT", "stake_amount": 100, "dry_run": True, "dry_run_wallet": 1000, "timeframe": params["timeframe"], "fee": CONFIG["fee"], "trading_mode": "spot", "exchange": {"name": "binance", "pair_whitelist": CONFIG["pairs"]}, "pairlists": [{"method": "StaticPairList"}], "entry_pricing": {"price_side": "other", "use_order_book": False, "order_book_top": 1}, "exit_pricing": {"price_side": "other", "use_order_book": False, "order_book_top": 1}, "telegram": {"enabled": False, "token": "dummy", "chat_id": "123"}, "api_server": {"enabled": False, "listen_ip_address": "127.0.0.1", "listen_port": 8080, "username": "a", "password": "a"}, } config_path = Path(f"/opt/docker/freqtrade/shared_data/user_data/configs/cfg{run_id}.json") with open(config_path, 'w') as f: json.dump(config, f) # Run docker output_file = f"/tmp/bt{run_id}.txt" cmd = [ 'docker', 'run', '--rm', '--network', 'host', '-v', '/opt/docker/freqtrade/shared_data/user_data:/freqtrade/user_data:rw', 'freqtradeorg/freqtrade:stable', 'backtesting', '--strategy', class_name, '--config', str(config_path), '--timeframe', params["timeframe"], '--timerange', '20230601-20231231', '--fee', str(CONFIG["fee"]), '--export', 'none' ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=180) output = result.stdout + result.stderr # Cleanup for f in [strategy_path, config_path]: try: if os.path.exists(f): os.unlink(f) except: pass # Parse metrics - use patterns from actual output metrics = {"trades": 0, "net_profit": -100, "max_drawdown": 1.0, "winrate": 0} # Parse from actual DEBUG_TEST output format # Example: "Total/Daily Avg Trades: 345 / 1.62" trades_match = re.search(r'Total/Daily Avg Trades\s+([0-9]+)\s*/', output) if trades_match: metrics["trades"] = int(trades_match.group(1)) # "Total profit %: -11.08%" profit_match = re.search(r'Total profit %\s+([0-9\-.]+)%', output) if profit_match: metrics["net_profit"] = float(profit_match.group(1)) # "Max % of account underwater: 11.15%" dd_match = re.search(r'Max % of account underwater\s+([0-9.]+)%', output) if dd_match: metrics["max_drawdown"] = float(dd_match.group(1)) / 100 # "Win/Draw/Loss: 165 0 180" or check strategy summary table win_match = re.search(r'Win\s+Draw\s+Loss\s+Win%\s*\n.*?(\d+)\s+\d+\s+\d+\s+([0-9.]+)', output) if win_match: metrics["winrate"] = float(win_match.group(2)) # Alternative: Parse from summary table line # │ DEBUG_TEST │ 345 │ -0.32 │ -110.835 │ -11.08 │ summary_match = re.search(rf'{class_name}[^0-9]*([0-9]+)[^0-9\-]*([0-9\-.]+)[^0-9\-]*([0-9\-.]+)', output) if summary_match: metrics["trades"] = int(summary_match.group(1)) return { "status": "success" if metrics["trades"] > 0 else "no_trades", "metrics": metrics, "full_output": output[-500:] if len(output) > 500 else output, # Save last 500 chars for debug } except Exception as e: return {"status": "error", "error": str(e)} def label(self, metrics: Dict) -> str: if metrics["trades"] < CONFIG["min_trades"]: return "LOW_SAMPLE" if metrics["net_profit"] >= 0 and metrics["max_drawdown"] <= CONFIG["max_drawdown"]: return "POSITIVE" return "NEGATIVE" def save(self, run_id: str, params: Dict, result: Dict): """Save run results""" run_dir = self.runs_dir / run_id run_dir.mkdir(exist_ok=True) metrics = result.get("metrics", {}) label = self.label(metrics) with open(run_dir / "params.json", 'w') as f: json.dump(params, f) with open(run_dir / "metrics.json", 'w') as f: json.dump({**metrics, "label": label}, f) with open(run_dir / "meta.json", 'w') as f: json.dump({ "run_id": run_id, "timestamp": datetime.now().isoformat(), "family": params["family"] }, f) return label def run(self): """Main loop""" print("=" * 60) print("🚀 WORKING POLICY BATCH - v3.0") print("Based on successful DEBUG_TEST (345 trades)") print("=" * 60) for i in range(1, CONFIG["total_runs"] + 1): run_id = str(uuid.uuid4())[:6] params = self.get_params(i) result = self.run_backtest(params, run_id) if result.get("status") == "success": label = self.save(run_id, params, result) self.stats["completed"] += 1 self.stats[label.lower()] += 1 m = result["metrics"] status_emoji = "✅" if label == "POSITIVE" else ("📉" if label == "NEGATIVE" else "📊") print(f"[{i}/320] {run_id}: {status_emoji} {label} | {m['trades']} trades, {m['net_profit']:.1f}%, {m['max_drawdown']*100:.1f}% DD") else: self.stats["failed"] += 1 status = result.get('status', 'unknown') err = result.get('error', '')[:30] print(f"[{i}/320] {run_id}: ❌ {status} - {err}") self.stats["total"] += 1 # Progress every 20 if i % 20 == 0 or i == 1: print(f"\n📊 [{i}/320] Comp:{self.stats['completed']} Fail:{self.stats['failed']} | POS:{self.stats['positive']} NEG:{self.stats['negative']} LOW:{self.stats['low']}\n") # Final report manifest = { "version": "3.0-final", "timestamp": datetime.now().isoformat(), "stats": self.stats, } with open(self.output_dir / "manifest.json", 'w') as f: json.dump(manifest, f, indent=2) print("\n" + "=" * 60) print(f"✅ DONE: {self.stats['completed']}/{self.stats['total']}") print(f"POS:{self.stats['positive']} NEG:{self.stats['negative']} LOW:{self.stats['low']} FAIL:{self.stats['failed']}") print("=" * 60) if __name__ == "__main__": WorkingBatchRunner().run()