#!/usr/bin/env python3 """ SAFE GUARD IMPLEMENTATION - entry_signal_rate tracking - FAILED_ZERO_SIGNAL marking - HIGH_CHURN flag (not prohibition, just marking) """ import json import subprocess import zipfile from pathlib import Path from datetime import datetime FREQTRADE_BASE = Path("/opt/docker/freqtrade/shared_data/user_data") STRATEGIES_DIR = FREQTRADE_BASE / "strategies" CONFIGS_DIR = FREQTRADE_BASE / "configs" BACKTEST_BASE = FREQTRADE_BASE / "backtest_results" DOCKER_ENV = {"DOCKER_HOST": "tcp://socket-proxy:2375"} def create_breakout_fixed() -> str: """Fixed breakout_vol with relaxed gates + debug logging.""" return '''from freqtrade.strategy import IStrategy from pandas import DataFrame import talib.abstract as ta class BREAKOUT_FIXED(IStrategy): """ Breakout Volatility - FIXED VERSION - Relaxed gates to allow trades - Debug logging for gate pass rates - Safety guards for monitoring """ timeframe = '4h' stoploss = -0.045 trailing_stop = True trailing_stop_positive = 0.02 minimal_roi = {"0": 0.035, "60": 0.02} max_open_trades = 2 # Safety tracking entry_signals_total = 0 candles_total = 0 def populate_indicators(self, dataframe: DataFrame, metadata: dict): # BBANDS with error handling try: bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2, nbdevdn=2) dataframe['bb_upper'] = bb['upperband'] dataframe['bb_middle'] = bb['middleband'] dataframe['bb_lower'] = bb['lowerband'] except Exception as e: print(f"[SAFEGUARD] BBANDS error: {e}") dataframe['bb_upper'] = dataframe['close'] * 1.02 dataframe['bb_middle'] = dataframe['close'] dataframe['bb_lower'] = dataframe['close'] * 0.98 dataframe['bb_width'] = (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle'] dataframe['ema_50'] = ta.EMA(dataframe, timeperiod=50) dataframe['volume_ma'] = ta.SMA(dataframe['volume'], timeperiod=20) dataframe['volume_spike'] = dataframe['volume'] / dataframe['volume_ma'] dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) dataframe['atr_pct'] = dataframe['atr'] / dataframe['close'] * 100 return dataframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict): dataframe.loc[:, 'buy'] = 0 # FIXED GATES - much more relaxed # 1. Volume condition - RELAXED (was 0.015-0.035, now 0.005-) vol_ok = dataframe['bb_width'] > 0.005 vol_rate = (vol_ok).sum() / len(dataframe) # 2. Breakout - RELAXED (was 0.994-0.999, now allow at upper) breakout = dataframe['close'] > dataframe['bb_upper'] * 0.998 breakout_rate = (breakout).sum() / len(dataframe) # 3. Trend - RELAXED (was 0.98, now 0.95) trend_ok = dataframe['close'] > dataframe['ema_50'] * 0.95 # 4. Volume spike - RELAXED (was 1.0-1.25, now 0.8) volume_ok = dataframe['volume_spike'] > 0.8 # 5. ATR - RELAXED (was 0.3-0.5, now 0.15) atr_ok = dataframe['atr_pct'] > 0.15 # DEBUG: Log gate rates (once per backtest) if not hasattr(self, '_debugged'): print(f"[SAFEGUARD] Gate pass rates:") print(f" Vol >0.5%: {vol_rate:.1%}") print(f" Breakout: {breakout_rate:.1%}") print(f" Volume >0.8x: {(volume_ok).sum()/len(dataframe):.1%}") # WARN if gates too strict if vol_rate < 0.1: print("[WARN] Vol gate too strict! <10% pass rate") if breakout_rate < 0.01: print("[WARN] Breakout gate too strict! <1% pass rate") self._debugged = True buy = vol_ok & breakout & trend_ok & volume_ok & atr_ok # Track signals self.entry_signals_total += buy.sum() self.candles_total += len(dataframe) dataframe.loc[buy, 'buy'] = 1 return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict): dataframe.loc[:, 'sell'] = 0 # Exit to middle or trailing stop to_middle = dataframe['close'] < dataframe['bb_middle'] trend_broken = dataframe['close'] < dataframe['ema_50'] * 0.97 sell = to_middle | trend_broken dataframe.loc[sell, 'sell'] = 1 return dataframe ''' def run_smoke_test(): """Run smoke test with fixed breakout.""" print("="*70) print("๐Ÿงช SAFE GUARD + FIXED BREAKOUT SMOKE TEST") print("="*70) # Write strategy strat_code = create_breakout_fixed() strat_file = STRATEGIES_DIR / "BREAKOUT_FIXED.py" strat_file.write_text(strat_code) print(f"โœ“ Strategy written: {strat_file}") # Config config = { "max_open_trades": 2, "stake_currency": "USDT", "stake_amount": 100, "dry_run": True, "dry_run_wallet": 1000, "timeframe": "4h", "fee": 0.0015, "trading_mode": "spot", "exchange": {"name": "binance", "pair_whitelist": ["BTC/USDT","ETH/USDT"]}, "pairlists": [{"method": "StaticPairList"}], "entry_pricing": {"price_side": "other", "use_order_book": False}, "exit_pricing": {"price_side": "other", "use_order_book": False}, "telegram": {"enabled": False, "token": "x", "chat_id": "1"}, "api_server": {"enabled": False, "listen_ip_address": "127.0.0.1", "listen_port": 8080, "username": "a", "password": "a"}, } cfg_file = CONFIGS_DIR / "cfg_BREAKOUT_FIXED.json" cfg_file.write_text(json.dumps(config, indent=2)) print(f"โœ“ Config written: {cfg_file}") # Run docker_backtest_dir = "/freqtrade/user_data/backtest_results/smoke_breakout_fixed" cmd = [ "docker", "run", "--rm", "--network", "host", "-v", f"{FREQTRADE_BASE}:/freqtrade/user_data:rw", "freqtradeorg/freqtrade:stable", "backtesting", "--strategy", "BREAKOUT_FIXED", "--config", "/freqtrade/user_data/configs/cfg_BREAKOUT_FIXED.json", "--timeframe", "4h", "--timerange", "20230601-20231231", "--fee", "0.0015", "--export", "trades", "--backtest-directory", docker_backtest_dir, "--notes", "breakout_fixed_smoke", "--cache", "none", "--no-color" ] print(f"\nRunning smoke test...") print(f" Strategy: BREAKOUT_FIXED") print(f" Pairs: BTC/USDT, ETH/USDT") print(f" Period: 2023-06 to 2023-12") print(f" Fee: 0.15%") try: import subprocess proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180, env=DOCKER_ENV) # Parse result import time time.sleep(0.5) zip_files = list(BACKTEST_BASE.glob("smoke_breakout_fixed-*.zip")) if not zip_files: print("โŒ No ZIP generated - smoke test FAILED") return {"status": "FAILED", "trades": 0, "error": "No output"} zip_file = max(zip_files, key=lambda p: p.stat().st_mtime) with zipfile.ZipFile(zip_file, 'r') as zf: json_files = [f for f in zf.namelist() if f.endswith('.json') and '_config' not in f] with zf.open(json_files[0]) as f: data = json.load(f) strat_key = list(data['strategy'].keys())[0] s = data['strategy'][strat_key] trades = s.get('total_trades', 0) profit = s.get('profit_total', 0) * 100 if s.get('profit_total') else 0 # Calculate TPPPD pairs = 2 days = 214 tpppd = trades / pairs / days if pairs > 0 and days > 0 else 0 result = { "status": "SUCCESS", "trades": trades, "profit": round(profit, 2), "pf": round(s.get('profit_factor', 0), 2), "winrate": round((s.get('wins', 0) / trades * 100) if trades > 0 else 0, 1), "tpppd": round(tpppd, 3), "high_churn": tpppd > 0.5 } return result except Exception as e: print(f"โŒ Error: {e}") return {"status": "FAILED", "error": str(e)} def generate_report(result: dict): """Generate smoke test report.""" print("\n" + "="*70) print("๐Ÿ“Š SMOKE TEST RESULT") print("="*70) if result['status'] == 'SUCCESS': print(f"\nโœ… Smoke test PASSED") print(f"\nMetrics:") print(f" Trades: {result['trades']}") print(f" Profit: {result['profit']:.2f}%") print(f" PF: {result['pf']}") print(f" Winrate: {result['winrate']}%") print(f" TPPPD: {result['tpppd']}") print(f" High Churn: {'โš ๏ธ YES' if result['high_churn'] else 'โœ… NO'}") if result['trades'] == 0: print(f"\nโŒ FAILED_ZERO_SIGNAL - 0 trades generated") print(" - Gates still too strict") print(" - Requires further relaxation") elif result['trades'] < 50: print(f"\nโš ๏ธ LOW_SAMPLE - {result['trades']} trades") print(" - Working but few opportunities") else: print(f"\n๐ŸŽฏ VALID - {result['trades']} trades") print(" - Ready for Anchor Sprint #002") if result['profit'] >= -5 and result['pf'] > 0.8: print(" - Candidate for POSITIVE_WEAK!") else: print(f"\nโŒ Smoke test FAILED: {result.get('error', 'Unknown')}") # Write report report_path = Path("/home/node/.openclaw/workspace/bob_quant/debug_analysis/smoke_test_breakout_fixed.md") with open(report_path, 'w') as f: f.write("# Safe Guard + Fixed Breakout Smoke Test\n\n") f.write(f"**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n") f.write(f"**Strategy:** BREAKOUT_FIXED\n\n") f.write("## Changes Made\n\n") f.write("1. **BB_WIDTH_MIN:** 0.005 (was 0.015-0.035)\n") f.write("2. **BREAKOUT_MULT:** 0.998 (was 0.994-0.999)\n") f.write("3. **TREND_MULT:** 0.95 (was 0.98)\n") f.write("4. **VOLUME_MULT:** 0.8 (was 1.0-1.25)\n") f.write("5. **ATR_MIN:** 0.15 (was 0.3-0.5)\n\n") f.write("## Result\n\n") if result['status'] == 'SUCCESS': f.write(f"| Metric | Value |\n") f.write(f"|--------|-------|\n") f.write(f"| Status | {result['status']} |\n") f.write(f"| Trades | {result['trades']} |\n") f.write(f"| Profit | {result['profit']:.2f}% |\n") f.write(f"| PF | {result['pf']} |\n") f.write(f"| Winrate | {result['winrate']}% |\n") f.write(f"| TPPPD | {result['tpppd']} |\n") f.write(f"| High Churn | {result['high_churn']} |\n") if result['trades'] == 0: f.write(f"\n**Verdict:** โŒ FAILED_ZERO_SIGNAL\n") elif result['trades'] < 50: f.write(f"\n**Verdict:** โš ๏ธ LOW_SAMPLE\n") else: f.write(f"\n**Verdict:** โœ… PASSED - Ready for Anchor Sprint #002\n") else: f.write(f"**Error:** {result.get('error', 'Unknown')}\n") print(f"\nโœ… Report written: {report_path}") def main(): result = run_smoke_test() generate_report(result) if __name__ == "__main__": main()