""" ================================================================================ MarchBreaker30m — Competition Strategy ================================================================================ Target : >20% return in a 10-day spot-only long competition window Start : 2026-03-21 Platform : Roostoo / freqtrade (IStrategy v3) Timeframe: 30 min -------------------------------------------------------------------------------- STRATEGY OVERVIEW -------------------------------------------------------------------------------- MarchBreaker30m is a momentum-breakout strategy designed for a 10-day long-only competition on spot markets. The core idea is simple: 1. Wait for a coin to break out of its prior 6-hour high with meaningful volume 2. Confirm the move with RSI momentum and trend alignment (price > MA20) 3. Require BTC to be in a bullish regime (BTC > MA50) before entering any trade 4. Dynamically select only the top-6 coins by 48-hour momentum at each bar, so capital always flows to current market leaders rather than laggards 5. Exit on a 4-hour low break, a -5% hard stop, or an 8% trailing stop from peak 6. Close ALL positions and halt when portfolio equity reaches +20% — locking in the competition target -------------------------------------------------------------------------------- ENTRY CONDITIONS (all must be true simultaneously) -------------------------------------------------------------------------------- A. close > highest_high(12) — 6h breakout (shift-1, no look-ahead) B. close > MA(20) — above medium-term trend C. volume > 2.0 × vol_MA(20) — real volume surge (10h average) D. 48 < RSI(7) < 82 — momentum entering, not exhausted E. |Δclose| < 20% — anti-chase filter (no parabolic bars) F. BTC_close > BTC_MA(50) — macro bullish regime G. coin is in Top-6 by 48h momentum — dynamic rotation: only leaders enter Execution: signal fires at candle close → buy at NEXT candle open -------------------------------------------------------------------------------- EXIT CONDITIONS (first triggered wins) -------------------------------------------------------------------------------- 1. close < lowest_low(8) — 4h low break (trend failure) 2. current_price < entry × 0.95 — hard stop -5% 3. current_price < peak × 0.92 — trailing stop -8% from high 4. ROI: +20% immediate / +15% @30h / +10% @60h / +5% @120h 5. Portfolio equity ≥ 120% of start — close ALL positions, halt trading -------------------------------------------------------------------------------- UNIVERSE (20 coins, rotating selection at runtime) -------------------------------------------------------------------------------- AI / Infra : TAO, NEAR, FET, ENA, SEI High-β L1 : SOL, SUI, APT, ARB, AVAX Meme : PEPE, BONK, DOGE, WIF, FLOKI, SHIB All 20 coins are kept in the universe. The Top-6 by 48h momentum filter decides which ones are ELIGIBLE for new entries at each 30-min bar. Existing open positions are never force-closed by the rotation filter. -------------------------------------------------------------------------------- RISK PARAMETERS -------------------------------------------------------------------------------- Max open positions : 5 (equal capital allocation per slot) Hard stop loss : -5% per trade Trailing stop : -8% from trade peak Portfolio hard stop: -8% from portfolio peak (only active after +8% gain) Portfolio target TP: +20% → close all, halt -------------------------------------------------------------------------------- BACKTEST RESULTS (standalone engine, Binance 30m data, fee=0.1%, slip=0.05%) -------------------------------------------------------------------------------- Window Return MaxDD Sharpe WinRate Trades BTC Notes Jan 1-11 +20.43% * 2.96% 18.94 91.7% 12 +3.22% Target hit day 4 Mar 1-11 -2.06% 6.42% -1.39 35.7% 28 +4.91% Choppy recovery Mar 8-18 +3.63% 4.75% 3.09 50.0% 30 +9.55% Steady grind Mar 11-21 +4.45% 3.96% 6.31 63.2% 19 +5.97% Pre-comp window * Portfolio TP triggered on day 4 → equity locked at $1,204,339 Top contributors (Mar 11-21): FET : +$41,421 (2 trades, 100% win rate) TAO : +$11,539 (3 trades, 67% win rate) RENDER: +$8,615 (2 trades, 50% win rate) Key risk (Mar 1-11): BONK/NEAR gave false breakouts in choppy early-March Mitigated by: rotation filter (top-6 by 48h momentum) limits exposure -------------------------------------------------------------------------------- NOTES FOR QUANT DEVELOPER -------------------------------------------------------------------------------- * BTC informative pair must be included in pair_whitelist (already done in config) * The portfolio target-TP (+20%) is implemented via freqtrade ROI at 0 minutes and custom_stoploss. The "close all at once" behavior requires an external coordinator or a custom_exit wrapper if exact simultaneous close is required. * Dynamic rotation is NOT implementable cross-pair in a standard freqtrade IStrategy. The production version should either: (a) Use the standalone engine (run_march_competition.py) with Binance WS (b) Approximate with a per-coin 48h momentum indicator (see below) * Standalone backtest engine: hackathon_roostoo/backtest/run_march_competition.py ================================================================================ """ import pandas as pd from pandas import DataFrame from freqtrade.strategy import IStrategy try: import talib.abstract as ta HAS_TALIB = True except ImportError: HAS_TALIB = False class MarchBreaker30m(IStrategy): """ 6h Breakout + 4h Low Exit + 8% Trailing Stop + 20% Portfolio TP BTC > MA50 macro filter + 48h per-coin momentum filter. """ INTERFACE_VERSION: int = 3 can_short: bool = False timeframe = "30m" # ── ROI / Stop ─────────────────────────────────────────────────────────── minimal_roi = { "0": 0.20, # lock +20% immediately (competition target) "60": 0.15, # +15% after 30h "120": 0.10, # +10% after 60h "240": 0.05, # + 5% after 120h (safety net) } stoploss = -0.05 # hard stop -5% use_custom_stoploss = True # enables 8% trailing via custom_stoploss trailing_stop = False # handled in custom_stoploss startup_candle_count: int = 100 # warmup for MA + momentum indicators process_only_new_candles = True use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False # ── Strategy constants ─────────────────────────────────────────────────── BREAKOUT_WINDOW = 12 # 6h breakout lookback (12 × 30m) EXIT_LOW_WINDOW = 8 # 4h exit low lookback (8 × 30m) VOL_AVG_WINDOW = 20 # 10h volume baseline VOL_SURGE_THRESH = 2.0 # minimum volume multiplier RSI_PERIOD = 7 RSI_THRESHOLD = 48 # minimum RSI for entry RSI_MAX = 82 # maximum RSI (overbought guard) MA_PERIOD = 20 MAX_BAR_MOVE = 0.20 # max single-candle return (anti-chase) TRAILING_STOP_PCT = 0.08 # 8% trailing stop from peak BTC_MA_PERIOD = 50 # BTC MA50 (~25h) MOMENTUM_WINDOW = 96 # 48h momentum lookback (96 × 30m) MOMENTUM_MIN = 0.00 # coin must be up > 0% over 48h to enter plot_config = { "main_plot": { "ma20": {"color": "#1E90FF"}, "highest_high": {"color": "#32CD32", "type": "line"}, "lowest_low": {"color": "#FF6347", "type": "line"}, }, "subplots": { "RSI(7)": {"rsi7": {"color": "#9370DB"}}, "Vol x": {"vol_ratio": {"color": "#4682B4", "type": "bar"}}, "Mom48h": {"momentum_48h": {"color": "#FF8C00", "type": "line"}}, }, } # ── Informative pairs ──────────────────────────────────────────────────── def informative_pairs(self): return [("BTC/USD", "30m")] # ── Indicators ─────────────────────────────────────────────────────────── def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # --- Trend & momentum indicators ------------------------------------ if HAS_TALIB: dataframe["ma20"] = ta.SMA(dataframe, timeperiod=self.MA_PERIOD) dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=self.RSI_PERIOD) else: dataframe["ma20"] = dataframe["close"].rolling(self.MA_PERIOD).mean() delta = dataframe["close"].diff() gain = delta.clip(lower=0) loss = -delta.clip(upper=0) alpha = 1.0 / self.RSI_PERIOD avg_gain = gain.ewm(alpha=alpha, min_periods=self.RSI_PERIOD, adjust=False).mean() avg_loss = loss.ewm(alpha=alpha, min_periods=self.RSI_PERIOD, adjust=False).mean() rs = avg_gain / avg_loss dataframe["rsi7"] = 100 - (100 / (1 + rs)) # --- Breakout channel ----------------------------------------------- # shift(1) prevents look-ahead bias: today's high cannot break today's high dataframe["highest_high"] = ( dataframe["high"].rolling(self.BREAKOUT_WINDOW).max().shift(1) ) dataframe["lowest_low"] = ( dataframe["low"].rolling(self.EXIT_LOW_WINDOW).min().shift(1) ) # --- Volume ratio --------------------------------------------------- dataframe["vol_avg"] = dataframe["volume"].rolling(self.VOL_AVG_WINDOW).mean() dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_avg"] # --- Anti-chase: single-candle return magnitude --------------------- dataframe["bar_return_abs"] = dataframe["close"].pct_change().abs() # --- 48h per-coin momentum (rotation proxy) ------------------------- # Full dynamic rotation (cross-pair ranking) requires the standalone engine. # Here we use a per-coin 48h return as an approximation: coin must be # trending UP over the last 48h to be eligible for entry. dataframe["momentum_48h"] = ( dataframe["close"] / dataframe["close"].shift(self.MOMENTUM_WINDOW) - 1 ) # --- BTC macro regime filter ---------------------------------------- inf_pair = "BTC/USD" informative = self.dp.get_pair_dataframe(pair=inf_pair, timeframe="30m") if not informative.empty: informative["btc_ma50"] = ( informative["close"].rolling(self.BTC_MA_PERIOD).mean() ) informative["btc_bullish"] = ( informative["close"] > informative["btc_ma50"] ) informative = informative[["date", "btc_bullish"]].copy() dataframe = pd.merge_asof( dataframe.sort_values("date"), informative.sort_values("date"), on="date", direction="backward", ) if "btc_bullish" not in dataframe.columns: dataframe["btc_bullish"] = True else: dataframe["btc_bullish"] = True return dataframe # ── Entry signal ───────────────────────────────────────────────────────── def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ All conditions must be true. Trade executes at NEXT candle open. A) close > highest_high(12) 6h breakout confirmed B) close > MA20 above medium-term trend C) vol_ratio > 2.0 genuine volume surge D) 48 < RSI(7) < 82 momentum active, not exhausted E) |Δclose| < 20% no parabolic chasing F) BTC > MA50 macro bullish G) momentum_48h > 0% coin trending up last 48h (rotation proxy) """ btc_ok = dataframe.get( "btc_bullish", pd.Series(True, index=dataframe.index) ) conditions = ( (dataframe["close"] > dataframe["highest_high"]) # A & (dataframe["close"] > dataframe["ma20"]) # B & (dataframe["vol_ratio"] > self.VOL_SURGE_THRESH) # C & (dataframe["rsi7"] > self.RSI_THRESHOLD) # D & (dataframe["rsi7"] < self.RSI_MAX) # D & (dataframe["bar_return_abs"] < self.MAX_BAR_MOVE) # E & btc_ok # F & (dataframe["momentum_48h"] > self.MOMENTUM_MIN) # G & dataframe["highest_high"].notna() & dataframe["ma20"].notna() & dataframe["rsi7"].notna() & dataframe["momentum_48h"].notna() & (dataframe["volume"] > 0) ) dataframe.loc[conditions, "enter_long"] = 1 return dataframe # ── Exit signal ────────────────────────────────────────────────────────── def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Exit when close breaks below the 4h low (trend failure signal). Hard stop / trailing stop / ROI are handled by stoploss + minimal_roi. """ dataframe.loc[ (dataframe["close"] < dataframe["lowest_low"]) & dataframe["lowest_low"].notna() & (dataframe["volume"] > 0), "exit_long", ] = 1 return dataframe # ── Custom stoploss: 8% trailing from peak ─────────────────────────────── def custom_stoploss( self, pair: str, trade, current_time, current_rate: float, current_profit: float, after_fill: bool, **kwargs, ) -> float: """ Trailing stop: stop = max(peak × 0.92, entry × 0.95) Never exceeds the -5% hard floor. """ if hasattr(trade, "max_rate") and trade.max_rate > 0: max_gain = trade.max_rate / trade.open_rate - 1 trail_from_open = (1 + max_gain) * (1 - self.TRAILING_STOP_PCT) - 1 return max(trail_from_open, self.stoploss) return self.stoploss