Seeking Alpha in Crypto Market Crash¶

Author: Beomgyu Joeng [Jaden] | Trade-Matrix
Date: February 2026


Executive Summary¶

This research investigates mean-reversion accumulation strategies for crash-buying crypto assets. We compare multiple strategies:

  1. S1 -- Daily RSI(30) + VWMA(20): 3-day mechanical accumulation on daily bars
  2. S2 -- 4H RSI(20) + VWMA(50) close-based: Same accumulation logic on 4-hour bars
  3. S3 -- 4H RSI(20) + VWMA(50) with limit order execution: T1 fills at threshold; T2/T3 conditional
  4. S4 (Section 10) -- T1-Anchored TP/SL: Same as S3 but TP/SL anchored to T1 entry price

Key Findings:

  • Mean-reversion strategies show strong performance during crypto crashes
  • Volume-weighted moving average (VWMA) provides better crash detection than simple MA
  • Limit order execution (S3/S4) significantly improves fill rates and entry prices
  • T1-anchored TP/SL (S4) provides more consistent risk management

# =============================================================================
# Cell 1: Imports and Data Loading
# =============================================================================

import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass, field
from enum import Enum
import warnings

warnings.filterwarnings('ignore')

# CRITICAL: Set plotly renderer for HTML export compatibility
pio.renderers.default = "notebook_connected"
pio.templates.default = "plotly_dark"

# Color scheme for assets
COLORS = {
    'BTC': '#F7931A',
    'ETH': '#627EEA',
    'SOL': '#9945FF'
}

# Data paths
DATA_DIR = Path('/home/jaden/Documents/projects/trade-matrix-mvp/src/main/data/bybit')

def load_data(symbol: str, timeframe: str) -> pd.DataFrame:
    """Load parquet data for given symbol and timeframe."""
    file_path = DATA_DIR / timeframe / f"{symbol}USDT_BYBIT_{timeframe}_latest.parquet"
    df = pd.read_parquet(file_path)
    
    # Standardize column names
    df.columns = df.columns.str.lower()
    
    # Ensure datetime index
    if 'timestamp' in df.columns:
        df['timestamp'] = pd.to_datetime(df['timestamp'])
        df = df.set_index('timestamp')
    elif not isinstance(df.index, pd.DatetimeIndex):
        df.index = pd.to_datetime(df.index)
    
    return df.sort_index()

# Load 1D data
print("Loading 1D data...")
data_1d = {
    'BTC': load_data('BTC', '1d'),
    'ETH': load_data('ETH', '1d'),
    'SOL': load_data('SOL', '1d')
}

# Load 4H data
print("Loading 4H data...")
data_4h = {
    'BTC': load_data('BTC', '4h'),
    'ETH': load_data('ETH', '4h'),
    'SOL': load_data('SOL', '4h')
}

# Print data coverage summary
print("\n" + "="*70)
print("DATA COVERAGE SUMMARY")
print("="*70)
print(f"\n{'Symbol':<8} {'Timeframe':<10} {'Start':<12} {'End':<12} {'Bars':>8}")
print("-"*50)
for symbol in ['BTC', 'ETH', 'SOL']:
    for tf, data_dict in [('1d', data_1d), ('4h', data_4h)]:
        df = data_dict[symbol]
        print(f"{symbol:<8} {tf:<10} {df.index[0].strftime('%Y-%m-%d'):<12} "
              f"{df.index[-1].strftime('%Y-%m-%d'):<12} {len(df):>8,}")
print("="*70)
Loading 1D data...
Loading 4H data...

======================================================================
DATA COVERAGE SUMMARY
======================================================================

Symbol   Timeframe  Start        End              Bars
--------------------------------------------------
BTC      1d         2020-03-25   2026-01-31      2,139
BTC      4h         2022-01-01   2026-01-31      8,952
ETH      1d         2021-03-15   2026-01-31      1,784
ETH      4h         2022-01-01   2026-01-31      8,952
SOL      1d         2021-10-15   2026-01-31      1,570
SOL      4h         2022-01-01   2026-01-31      8,952
======================================================================

Section 2: Data Exploration¶

# =============================================================================
# Cell 2: Data Exploration - Price History
# =============================================================================

fig = make_subplots(rows=3, cols=1, shared_xaxes=True, 
                    subplot_titles=['BTC/USDT', 'ETH/USDT', 'SOL/USDT'],
                    vertical_spacing=0.05)

for i, symbol in enumerate(['BTC', 'ETH', 'SOL'], 1):
    df = data_1d[symbol]
    fig.add_trace(
        go.Scatter(x=df.index, y=df['close'], name=symbol,
                   line=dict(color=COLORS[symbol], width=1.5)),
        row=i, col=1
    )

fig.update_layout(
    title='Historical Price Data (1D)',
    height=800,
    showlegend=True,
    legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)

fig.update_yaxes(title_text='Price (USDT)', row=1, col=1)
fig.update_yaxes(title_text='Price (USDT)', row=2, col=1)
fig.update_yaxes(title_text='Price (USDT)', row=3, col=1)
fig.update_xaxes(title_text='Date', row=3, col=1)

fig.show()

Section 3: Technical Indicator Functions¶

# =============================================================================
# Cell 3: Helper Functions - VWMA and RSI
# =============================================================================

def calculate_vwma(df: pd.DataFrame, period: int = 20) -> pd.Series:
    """
    Calculate Volume Weighted Moving Average (VWMA).
    
    VWMA = Sum(Close * Volume, n) / Sum(Volume, n)
    
    Parameters:
    -----------
    df : pd.DataFrame
        DataFrame with 'close' and 'volume' columns
    period : int
        Lookback period (default: 20)
        
    Returns:
    --------
    pd.Series : VWMA values
    """
    cv = df['close'] * df['volume']
    return cv.rolling(window=period).sum() / df['volume'].rolling(window=period).sum()


def calculate_rsi(series: pd.Series, period: int = 14) -> pd.Series:
    """
    Calculate Relative Strength Index (RSI) using Wilder's smoothing.
    
    RSI = 100 - (100 / (1 + RS))
    where RS = Average Gain / Average Loss
    
    Parameters:
    -----------
    series : pd.Series
        Price series (typically close prices)
    period : int
        Lookback period (default: 14)
        
    Returns:
    --------
    pd.Series : RSI values (0-100)
    """
    delta = series.diff()
    gains = delta.clip(lower=0)
    losses = (-delta).clip(lower=0)
    
    # Wilder's smoothing (equivalent to alpha = 1/period)
    avg_gain = gains.ewm(alpha=1.0/period, min_periods=period, adjust=False).mean()
    avg_loss = losses.ewm(alpha=1.0/period, min_periods=period, adjust=False).mean()
    
    rs = avg_gain / avg_loss
    rsi = 100.0 - (100.0 / (1.0 + rs))
    
    return rsi


def calculate_max_drawdown(pnl_series: pd.Series) -> float:
    """
    Calculate maximum drawdown from cumulative P&L series.
    
    Parameters:
    -----------
    pnl_series : pd.Series
        Cumulative P&L values
        
    Returns:
    --------
    float : Maximum drawdown (negative value)
    """
    cummax = pnl_series.cummax()
    drawdown = pnl_series - cummax
    return drawdown.min()


print("Helper functions defined:")
print("  - calculate_vwma(df, period=20)")
print("  - calculate_rsi(series, period=14)")
print("  - calculate_max_drawdown(pnl_series)")
Helper functions defined:
  - calculate_vwma(df, period=20)
  - calculate_rsi(series, period=14)
  - calculate_max_drawdown(pnl_series)

Section 4-6: S1 -- Daily RSI(30) + VWMA(20) Strategy¶

Strategy Logic¶

Entry Conditions (all must be met):

  • RSI(14) < 30 (oversold)
  • Close < 80% × VWMA(20) (significant discount to volume-weighted average)

Accumulation:

  • Accumulate over 3 consecutive bars when entry conditions persist
  • $1M per tranche (T1, T2, T3)
  • Maximum position: $3M

Exit:

  • Take Profit: +10% from Bar 3 close
  • Stop Loss: -10% from Bar 3 close
# =============================================================================
# Cell 4: S1 - Signal Generation and Data Preparation
# =============================================================================

def prepare_s1_data(df: pd.DataFrame, 
                    vwma_period: int = 20, 
                    rsi_period: int = 14,
                    rsi_threshold: float = 30.0,
                    vwma_discount: float = 0.80) -> pd.DataFrame:
    """
    Prepare data for S1 strategy (Daily RSI + VWMA).
    
    Parameters:
    -----------
    df : pd.DataFrame
        OHLCV data
    vwma_period : int
        VWMA lookback period
    rsi_period : int
        RSI lookback period
    rsi_threshold : float
        RSI threshold for oversold condition
    vwma_discount : float
        Discount factor for VWMA threshold
        
    Returns:
    --------
    pd.DataFrame : Data with indicators and signals
    """
    result = df.copy()
    
    # Filter to Jan 2023+ to match 4H strategies
    result = result[result.index >= '2023-01-01']
    
    # Calculate indicators
    result['vwma'] = calculate_vwma(result, vwma_period)
    result['vwma_threshold'] = result['vwma'] * vwma_discount
    result['rsi'] = calculate_rsi(result['close'], rsi_period)
    
    # Generate entry signal
    result['entry_signal'] = (
        (result['rsi'] < rsi_threshold) & 
        (result['close'] < result['vwma_threshold'])
    )
    
    return result.dropna()


# Prepare S1 data for all symbols
s1_data = {}
for symbol in ['BTC', 'ETH', 'SOL']:
    s1_data[symbol] = prepare_s1_data(data_1d[symbol])
    signal_count = s1_data[symbol]['entry_signal'].sum()
    print(f"{symbol}: {signal_count} entry signals detected")

print("\nS1 data prepared with VWMA(20) and RSI(14) < 30 filter.")
BTC: 0 entry signals detected
ETH: 2 entry signals detected
SOL: 2 entry signals detected

S1 data prepared with VWMA(20) and RSI(14) < 30 filter.
# =============================================================================
# Cell 5: S1 - State Machine Backtester
# =============================================================================

class TradeState(Enum):
    """Trading state machine states."""
    IDLE = "IDLE"
    ACCUMULATING = "ACCUMULATING"
    HOLDING = "HOLDING"


@dataclass
class TradeSequence:
    """Record of a complete trade sequence."""
    symbol: str
    entry_dates: List[pd.Timestamp] = field(default_factory=list)
    entry_prices: List[float] = field(default_factory=list)
    exit_date: Optional[pd.Timestamp] = None
    exit_price: float = 0.0
    exit_type: str = ""  # 'TP' or 'SL'
    tranches_filled: int = 0
    pnl_usd: float = 0.0
    reference_price: float = 0.0
    tp_level: float = 0.0
    sl_level: float = 0.0


def backtest_s1_strategy(df: pd.DataFrame, 
                         symbol: str,
                         position_size: float = 1_000_000.0,
                         tp_pct: float = 0.10,
                         sl_pct: float = 0.10,
                         max_tranches: int = 3) -> List[TradeSequence]:
    """
    Backtest S1 strategy with 3-day accumulation.
    
    State Machine:
    - IDLE: Wait for entry signal
    - ACCUMULATING: Fill tranches on consecutive signal days
    - HOLDING: Wait for TP/SL hit
    
    Parameters:
    -----------
    df : pd.DataFrame
        Prepared data with 'entry_signal' column
    symbol : str
        Asset symbol
    position_size : float
        Size per tranche in USD
    tp_pct : float
        Take profit percentage
    sl_pct : float
        Stop loss percentage
    max_tranches : int
        Maximum number of tranches to accumulate
        
    Returns:
    --------
    List[TradeSequence] : Completed trade sequences
    """
    sequences = []
    state = TradeState.IDLE
    current_sequence: Optional[TradeSequence] = None
    
    for idx, row in df.iterrows():
        if state == TradeState.IDLE:
            if row['entry_signal']:
                # Start new accumulation sequence
                current_sequence = TradeSequence(symbol=symbol)
                current_sequence.entry_dates.append(idx)
                current_sequence.entry_prices.append(row['close'])
                current_sequence.tranches_filled = 1
                state = TradeState.ACCUMULATING
                
        elif state == TradeState.ACCUMULATING:
            if row['entry_signal'] and current_sequence.tranches_filled < max_tranches:
                # Add another tranche
                current_sequence.entry_dates.append(idx)
                current_sequence.entry_prices.append(row['close'])
                current_sequence.tranches_filled += 1
                
                if current_sequence.tranches_filled >= max_tranches:
                    # Full accumulation complete, set TP/SL
                    reference = row['close']
                    current_sequence.reference_price = reference
                    current_sequence.tp_level = reference * (1 + tp_pct)
                    current_sequence.sl_level = reference * (1 - sl_pct)
                    state = TradeState.HOLDING
            else:
                # Signal lost before full accumulation - close partial
                # Use last entry price as reference
                if current_sequence.entry_prices:
                    reference = current_sequence.entry_prices[-1]
                    current_sequence.reference_price = reference
                    current_sequence.tp_level = reference * (1 + tp_pct)
                    current_sequence.sl_level = reference * (1 - sl_pct)
                    state = TradeState.HOLDING
                    
        elif state == TradeState.HOLDING:
            # Check for TP/SL hit using high/low
            if row['high'] >= current_sequence.tp_level:
                # Take profit hit
                current_sequence.exit_date = idx
                current_sequence.exit_price = current_sequence.tp_level
                current_sequence.exit_type = 'TP'
                
                # Calculate P&L
                avg_entry = np.mean(current_sequence.entry_prices)
                total_position = position_size * current_sequence.tranches_filled
                pct_return = (current_sequence.exit_price - avg_entry) / avg_entry
                current_sequence.pnl_usd = total_position * pct_return
                
                sequences.append(current_sequence)
                current_sequence = None
                state = TradeState.IDLE
                
            elif row['low'] <= current_sequence.sl_level:
                # Stop loss hit
                current_sequence.exit_date = idx
                current_sequence.exit_price = current_sequence.sl_level
                current_sequence.exit_type = 'SL'
                
                # Calculate P&L
                avg_entry = np.mean(current_sequence.entry_prices)
                total_position = position_size * current_sequence.tranches_filled
                pct_return = (current_sequence.exit_price - avg_entry) / avg_entry
                current_sequence.pnl_usd = total_position * pct_return
                
                sequences.append(current_sequence)
                current_sequence = None
                state = TradeState.IDLE
    
    return sequences


print("S1 backtester defined with state machine: IDLE -> ACCUMULATING -> HOLDING -> IDLE")
S1 backtester defined with state machine: IDLE -> ACCUMULATING -> HOLDING -> IDLE
# =============================================================================
# Cell 6: S1 - Run Backtest and Print Results
# =============================================================================

def print_strategy_summary(sequences: Dict[str, List[TradeSequence]], strategy_name: str):
    """Print detailed summary statistics for strategy results."""
    
    print(f"\n{'='*80}")
    print(f"{strategy_name} RESULTS SUMMARY")
    print(f"{'='*80}")
    
    # Per-symbol stats
    all_sequences = []
    
    print(f"\n{'Symbol':<8} {'Seqs':>6} {'TP':>6} {'SL':>6} {'Win%':>8} {'Cumulative P&L':>18} {'Max Loss':>14}")
    print("-" * 80)
    
    for symbol in ['BTC', 'ETH', 'SOL']:
        seqs = sequences.get(symbol, [])
        all_sequences.extend(seqs)
        
        if not seqs:
            print(f"{symbol:<8} {'N/A':>6}")
            continue
            
        n_seqs = len(seqs)
        n_tp = sum(1 for s in seqs if s.exit_type == 'TP')
        n_sl = sum(1 for s in seqs if s.exit_type == 'SL')
        win_rate = (n_tp / n_seqs * 100) if n_seqs > 0 else 0
        total_pnl = sum(s.pnl_usd for s in seqs)
        max_loss = min(s.pnl_usd for s in seqs) if seqs else 0
        
        print(f"{symbol:<8} {n_seqs:>6} {n_tp:>6} {n_sl:>6} {win_rate:>7.1f}% "
              f"${total_pnl:>+16,.0f} ${max_loss:>+12,.0f}")
    
    # Total stats
    print("-" * 80)
    if all_sequences:
        n_total = len(all_sequences)
        n_tp_total = sum(1 for s in all_sequences if s.exit_type == 'TP')
        n_sl_total = sum(1 for s in all_sequences if s.exit_type == 'SL')
        win_rate_total = (n_tp_total / n_total * 100) if n_total > 0 else 0
        total_pnl = sum(s.pnl_usd for s in all_sequences)
        max_loss_total = min(s.pnl_usd for s in all_sequences)
        
        # Calculate max drawdown
        pnl_list = [s.pnl_usd for s in sorted(all_sequences, key=lambda x: x.entry_dates[0])]
        cumulative = pd.Series(pnl_list).cumsum()
        max_dd = calculate_max_drawdown(cumulative)
        
        print(f"{'TOTAL':<8} {n_total:>6} {n_tp_total:>6} {n_sl_total:>6} {win_rate_total:>7.1f}% "
              f"${total_pnl:>+16,.0f} ${max_loss_total:>+12,.0f}")
        print(f"\nMax Drawdown: ${max_dd:>+,.0f}")
        
        # Average tranches
        avg_tranches = np.mean([s.tranches_filled for s in all_sequences])
        print(f"Average Tranches Filled: {avg_tranches:.2f}")
    
    print(f"{'='*80}\n")


# Run S1 backtest
s1_results = {}
for symbol in ['BTC', 'ETH', 'SOL']:
    s1_results[symbol] = backtest_s1_strategy(s1_data[symbol], symbol)

print_strategy_summary(s1_results, "S1: Daily RSI(30) + VWMA(20)")
================================================================================
S1: Daily RSI(30) + VWMA(20) RESULTS SUMMARY
================================================================================

Symbol     Seqs     TP     SL     Win%     Cumulative P&L       Max Loss
--------------------------------------------------------------------------------
BTC         N/A
ETH           1      1      0   100.0% $        +100,000 $    +100,000
SOL           1      0      1     0.0% $        -100,000 $    -100,000
--------------------------------------------------------------------------------
TOTAL         2      1      1    50.0% $              +0 $    -100,000

Max Drawdown: $-100,000
Average Tranches Filled: 1.00
================================================================================

# =============================================================================
# Cell 7: S1 - Detailed Trade Sequences
# =============================================================================

def print_trade_details(sequences: Dict[str, List[TradeSequence]]):
    """Print detailed information for each trade sequence."""
    
    for symbol in ['BTC', 'ETH', 'SOL']:
        seqs = sequences.get(symbol, [])
        if not seqs:
            continue
            
        print(f"\n{symbol} Trade Sequences:")
        print("-" * 100)
        print(f"{'#':>3} {'Entry Date':<12} {'Avg Entry':>12} {'Exit Date':<12} {'Exit':>12} "
              f"{'Type':>6} {'Tranches':>9} {'P&L':>14}")
        print("-" * 100)
        
        for i, seq in enumerate(seqs, 1):
            avg_entry = np.mean(seq.entry_prices)
            entry_date = seq.entry_dates[0].strftime('%Y-%m-%d')
            exit_date = seq.exit_date.strftime('%Y-%m-%d') if seq.exit_date else 'N/A'
            
            print(f"{i:>3} {entry_date:<12} ${avg_entry:>10,.2f} {exit_date:<12} ${seq.exit_price:>10,.2f} "
                  f"{seq.exit_type:>6} {seq.tranches_filled:>9} ${seq.pnl_usd:>+12,.0f}")


print_trade_details(s1_results)
ETH Trade Sequences:
----------------------------------------------------------------------------------------------------
  # Entry Date      Avg Entry Exit Date            Exit   Type  Tranches            P&L
----------------------------------------------------------------------------------------------------
  1 2024-08-05   $  2,418.38 2024-08-08   $  2,660.22     TP         1 $    +100,000

SOL Trade Sequences:
----------------------------------------------------------------------------------------------------
  # Entry Date      Avg Entry Exit Date            Exit   Type  Tranches            P&L
----------------------------------------------------------------------------------------------------
  1 2025-02-24   $    141.77 2025-02-28   $    127.59     SL         1 $    -100,000
# =============================================================================
# Cell 8: S1 - Equity Curve Visualization
# =============================================================================

def plot_equity_curves(sequences: Dict[str, List[TradeSequence]], strategy_name: str):
    """Plot cumulative P&L equity curves for each symbol."""
    
    fig = go.Figure()
    
    for symbol in ['BTC', 'ETH', 'SOL']:
        seqs = sequences.get(symbol, [])
        if not seqs:
            continue
            
        # Sort by exit date and calculate cumulative P&L
        sorted_seqs = sorted([s for s in seqs if s.exit_date], key=lambda x: x.exit_date)
        dates = [s.exit_date for s in sorted_seqs]
        pnl = [s.pnl_usd for s in sorted_seqs]
        cumulative = np.cumsum(pnl)
        
        fig.add_trace(go.Scatter(
            x=dates, y=cumulative, name=symbol,
            line=dict(color=COLORS[symbol], width=2),
            mode='lines+markers'
        ))
    
    fig.update_layout(
        title=f'{strategy_name} - Cumulative P&L',
        xaxis_title='Date',
        yaxis_title='Cumulative P&L (USD)',
        height=500,
        hovermode='x unified'
    )
    
    # Add zero line
    fig.add_hline(y=0, line_dash='dash', line_color='gray', opacity=0.5)
    
    fig.show()


plot_equity_curves(s1_results, "S1: Daily RSI(30) + VWMA(20)")
# =============================================================================
# Cell 9: S1 - Price Chart with Signals
# =============================================================================

def plot_strategy_signals(df: pd.DataFrame, sequences: List[TradeSequence], 
                          symbol: str, strategy_name: str):
    """Plot price chart with VWMA, RSI, and trade signals."""
    
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                        vertical_spacing=0.1, row_heights=[0.7, 0.3],
                        subplot_titles=[f'{symbol} Price with VWMA', 'RSI(14)'])
    
    # Price candlestick
    fig.add_trace(go.Candlestick(
        x=df.index, open=df['open'], high=df['high'],
        low=df['low'], close=df['close'], name='Price'
    ), row=1, col=1)
    
    # VWMA
    fig.add_trace(go.Scatter(
        x=df.index, y=df['vwma'], name='VWMA(20)',
        line=dict(color='cyan', width=1)
    ), row=1, col=1)
    
    # VWMA threshold
    fig.add_trace(go.Scatter(
        x=df.index, y=df['vwma_threshold'], name='80% VWMA',
        line=dict(color='yellow', width=1, dash='dash')
    ), row=1, col=1)
    
    # Entry signals
    for seq in sequences:
        for i, (date, price) in enumerate(zip(seq.entry_dates, seq.entry_prices)):
            fig.add_trace(go.Scatter(
                x=[date], y=[price], mode='markers',
                marker=dict(symbol='triangle-up', size=12, color='lime'),
                name=f'Entry T{i+1}', showlegend=(i == 0)
            ), row=1, col=1)
        
        # Exit marker
        if seq.exit_date:
            color = 'lime' if seq.exit_type == 'TP' else 'red'
            fig.add_trace(go.Scatter(
                x=[seq.exit_date], y=[seq.exit_price], mode='markers',
                marker=dict(symbol='triangle-down', size=12, color=color),
                name=seq.exit_type, showlegend=False
            ), row=1, col=1)
    
    # RSI
    fig.add_trace(go.Scatter(
        x=df.index, y=df['rsi'], name='RSI(14)',
        line=dict(color='orange', width=1)
    ), row=2, col=1)
    
    # RSI thresholds
    fig.add_hline(y=30, line_dash='dash', line_color='lime', row=2, col=1)
    fig.add_hline(y=70, line_dash='dash', line_color='red', row=2, col=1)
    
    fig.update_layout(
        title=f'{strategy_name} - {symbol}',
        height=700,
        showlegend=True,
        xaxis_rangeslider_visible=False
    )
    
    fig.show()


# Plot for BTC only (to avoid too many charts)
plot_strategy_signals(s1_data['BTC'], s1_results['BTC'], 'BTC', 'S1: Daily RSI(30) + VWMA(20)')

Section 8: S2 -- 4H RSI(20) + VWMA(50) Close-Based Accumulation¶

Strategy Logic (S9 Equivalent from vwma20 notebook)¶

Entry Conditions:

  • RSI(14) < 20 (extreme oversold)
  • Close < 80% x VWMA(50) (significant discount to volume-weighted average)

Accumulation:

  • T1 Entry: Close price of trigger bar
  • T2 Entry: Close price of bar 2 (always executes)
  • T3 Entry: Close price of bar 3 (always executes)
  • All 3 tranches always execute

Exit:

  • Take Profit: +10% from Bar 3 close
  • Stop Loss: -10% from Bar 3 close

Position Size: $1M per tranche, $3M total

# =============================================================================
# Cell: S2 Data Preparation - VWMA(50) on 4H
# =============================================================================

print("=" * 80)
print("S2: 4H CLOSE-BASED ACCUMULATION - DATA PREPARATION")
print("=" * 80)

s2_data = {}

for symbol in ['BTC', 'ETH', 'SOL']:
    df = data_4h[symbol].copy()
    
    # Filter to Jan 2023+
    df = df[df.index >= '2023-01-01'].reset_index(drop=True)
    
    # VWMA(50) - Volume-Weighted Moving Average
    df['vwma50'] = calculate_vwma(df, period=50)
    
    # Threshold: 80% of VWMA(50)
    df['vwma50_threshold'] = df['vwma50'] * 0.80
    
    # RSI(14)
    df['rsi'] = calculate_rsi(df['close'], period=14)
    
    # S2 Signal: CLOSE < threshold AND RSI < 20
    df['s2_signal'] = (df['close'] < df['vwma50_threshold']) & (df['rsi'] < 20)
    
    # Also add S8 signal for comparison (LOW < threshold)
    df['s8_signal'] = (df['low'] < df['vwma50_threshold']) & (df['rsi'] < 20)
    
    # Drop warmup
    df = df.dropna(subset=['vwma50', 'rsi'])
    
    s2_data[symbol] = df
    print(f"{symbol}: {len(df):,} bars | S2 signals: {df['s2_signal'].sum()} | S8 signals: {df['s8_signal'].sum()}")
================================================================================
S2: 4H CLOSE-BASED ACCUMULATION - DATA PREPARATION
================================================================================
BTC: 6,713 bars | S2 signals: 0 | S8 signals: 1
ETH: 6,713 bars | S2 signals: 1 | S8 signals: 5
SOL: 6,713 bars | S2 signals: 1 | S8 signals: 2
# =============================================================================
# Cell: All-Tranches Backtester (for S2 and S8)
# =============================================================================

def backtest_all_tranches(
    df: pd.DataFrame,
    symbol: str,
    signal_col: str,
    entry_col: str = 'close',  # 'close' for S2/S9, 'low' for S8
    position_size: float = 1_000_000.0,
    tp_pct: float = 0.10,
    sl_pct: float = 0.10,
    max_bars: int = 3
) -> List[TradeSequence]:
    """
    All 3 tranches always execute.
    T1 at entry_col (low or close), T2/T3 at close.
    TP/SL from Bar 3's close.
    """
    df = df.copy().reset_index(drop=True)
    sequences = []
    state = TradeState.IDLE
    current: Optional[TradeSequence] = None
    bar_count = 0
    
    for i in range(len(df)):
        row = df.iloc[i]
        
        if state == TradeState.IDLE:
            if row[signal_col]:
                current = TradeSequence(symbol=symbol)
                current.entry_dates.append(i)
                current.entry_prices.append(row[entry_col])  # T1 at entry_col
                current.tranches_filled = 1
                bar_count = 1
                state = TradeState.ACCUMULATING
        
        elif state == TradeState.ACCUMULATING:
            bar_count += 1
            if bar_count <= max_bars:
                # T2, T3 always at close
                current.entry_dates.append(i)
                current.entry_prices.append(row['close'])
                current.tranches_filled += 1
            
            if bar_count >= max_bars:
                # Reference = Bar 3's close
                reference = current.entry_prices[-1]
                current.reference_price = reference
                current.tp_level = reference * (1 + tp_pct)
                current.sl_level = reference * (1 - sl_pct)
                state = TradeState.HOLDING
        
        elif state == TradeState.HOLDING:
            if row['high'] >= current.tp_level:
                current.exit_date = i
                current.exit_price = current.tp_level
                current.exit_type = 'TP'
                _calc_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
            
            elif row['low'] <= current.sl_level:
                current.exit_date = i
                current.exit_price = current.sl_level
                current.exit_type = 'SL'
                _calc_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
    
    return sequences


def _calc_pnl(seq: TradeSequence, position_size: float):
    """Calculate per-tranche P&L."""
    for i, entry in enumerate(seq.entry_prices):
        qty = position_size / entry
        pnl = qty * (seq.exit_price - entry)
        if i == 0:
            seq.t1_pnl_usd = pnl
        elif i == 1:
            seq.t2_pnl_usd = pnl
        elif i == 2:
            seq.t3_pnl_usd = pnl
    seq.pnl_usd = seq.t1_pnl_usd + seq.t2_pnl_usd + seq.t3_pnl_usd


print("All-tranches backtester defined.")
All-tranches backtester defined.
# =============================================================================
# Cell: Run S2 Backtest (Close-Price Entry)
# =============================================================================

s2_results = {}

for symbol in ['BTC', 'ETH', 'SOL']:
    s2_results[symbol] = backtest_all_tranches(
        s2_data[symbol], symbol, signal_col='s2_signal', entry_col='close'
    )

print_strategy_summary(s2_results, "S2: 4H Close-Based Accumulation (VWMA50)")
================================================================================
S2: 4H Close-Based Accumulation (VWMA50) RESULTS SUMMARY
================================================================================

Symbol     Seqs     TP     SL     Win%     Cumulative P&L       Max Loss
--------------------------------------------------------------------------------
BTC         N/A
ETH           1      1      0   100.0% $        +257,915 $    +257,915
SOL           1      1      0   100.0% $        +319,346 $    +319,346
--------------------------------------------------------------------------------
TOTAL         2      2      0   100.0% $        +577,261 $    +257,915

Max Drawdown: $+0
Average Tranches Filled: 3.00
================================================================================

# =============================================================================
# Cell: Run S8 Backtest (Low-Price Entry - Theoretical Best)
# =============================================================================

s8_results = {}

for symbol in ['BTC', 'ETH', 'SOL']:
    s8_results[symbol] = backtest_all_tranches(
        s2_data[symbol], symbol, signal_col='s8_signal', entry_col='low'
    )

print_strategy_summary(s8_results, "S8: 4H Low-Price Entry (Theoretical Best)")
================================================================================
S8: 4H Low-Price Entry (Theoretical Best) RESULTS SUMMARY
================================================================================

Symbol     Seqs     TP     SL     Win%     Cumulative P&L       Max Loss
--------------------------------------------------------------------------------
BTC           1      1      0   100.0% $        +484,645 $    +484,645
ETH           3      3      0   100.0% $      +1,361,936 $    +375,224
SOL           2      2      0   100.0% $        +841,109 $    +369,031
--------------------------------------------------------------------------------
TOTAL         6      6      0   100.0% $      +2,687,689 $    +369,031

Max Drawdown: $+0
Average Tranches Filled: 3.00
================================================================================


Section 9: S3 -- 4H Limit Order Accumulation Strategy¶

Key Innovation (S10 Equivalent)¶

Entry Conditions:

  • RSI(14) < 20 AND Low < 80% x VWMA(50) (same as S8)

T1 Entry: Limit order fills at threshold price = VWMA(50) * 0.80

T2/T3 Entry: Close price, BUT only if close < threshold

  • If close >= threshold, that tranche is SKIPPED

Exit:

  • TP/SL: +/-10% from Bar 3's close

Position Size: Dynamic $1M-$3M based on fills

Benefits:

  • Better T1 entry price (at threshold, not at close)
  • Reduced exposure in failed breakdowns (T2/T3 skipped)
  • Lower max single-trade loss
# =============================================================================
# Cell: S3 Limit Order Backtester
# =============================================================================

@dataclass
class LimitOrderSequence:
    """Trade sequence with conditional T2/T3 fills."""
    symbol: str
    entry_dates: List[pd.Timestamp] = field(default_factory=list)
    entry_prices: List[float] = field(default_factory=list)
    bar_closes: List[float] = field(default_factory=list)
    threshold_at_entry: float = 0.0
    exit_date: Optional[pd.Timestamp] = None
    exit_price: float = 0.0
    exit_type: str = ""
    tranches_filled: int = 0
    bar1_filled: bool = False
    bar2_filled: bool = False
    bar3_filled: bool = False
    pnl_usd: float = 0.0
    t1_pnl_usd: float = 0.0
    t2_pnl_usd: float = 0.0
    t3_pnl_usd: float = 0.0
    reference_price: float = 0.0
    tp_level: float = 0.0
    sl_level: float = 0.0


def backtest_limit_accumulation_strategy(
    df: pd.DataFrame,
    symbol: str,
    position_size: float = 1_000_000.0,
    tp_pct: float = 0.10,
    sl_pct: float = 0.10,
    max_bars: int = 3
) -> List[LimitOrderSequence]:
    """
    S3/S10 Limit Order Strategy:
    - T1 fills at threshold (limit order)
    - T2/T3 only if close < threshold
    - TP/SL from Bar 3's close
    """
    df = df.copy().reset_index(drop=True)
    sequences = []
    state = TradeState.IDLE
    current: Optional[LimitOrderSequence] = None
    bar_count = 0
    
    for i in range(len(df)):
        row = df.iloc[i]
        threshold = row['vwma50_threshold']
        rsi_ok = row['rsi'] < 20
        
        if state == TradeState.IDLE:
            # T1: RSI < 20 AND low < threshold
            if rsi_ok and row['low'] < threshold:
                current = LimitOrderSequence(symbol=symbol)
                current.threshold_at_entry = threshold
                current.entry_dates.append(i)
                current.entry_prices.append(threshold)  # T1 at threshold
                current.bar_closes.append(row['close'])
                current.bar1_filled = True
                current.tranches_filled = 1
                bar_count = 1
                state = TradeState.ACCUMULATING
        
        elif state == TradeState.ACCUMULATING:
            bar_count += 1
            current.bar_closes.append(row['close'])
            
            # T2/T3: only if close < threshold
            if rsi_ok and row['close'] < current.threshold_at_entry:
                current.entry_dates.append(i)
                current.entry_prices.append(row['close'])
                current.tranches_filled += 1
                if bar_count == 2:
                    current.bar2_filled = True
                elif bar_count == 3:
                    current.bar3_filled = True
            
            if bar_count >= max_bars:
                # Reference = Bar 3's close
                reference = current.bar_closes[-1]
                current.reference_price = reference
                current.tp_level = reference * (1 + tp_pct)
                current.sl_level = reference * (1 - sl_pct)
                state = TradeState.HOLDING
        
        elif state == TradeState.HOLDING:
            if row['high'] >= current.tp_level:
                current.exit_date = i
                current.exit_price = current.tp_level
                current.exit_type = 'TP'
                _calc_limit_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
            
            elif row['low'] <= current.sl_level:
                current.exit_date = i
                current.exit_price = current.sl_level
                current.exit_type = 'SL'
                _calc_limit_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
    
    return sequences


def _calc_limit_pnl(seq: LimitOrderSequence, position_size: float):
    """Calculate per-tranche P&L for limit order strategy."""
    for i, entry in enumerate(seq.entry_prices):
        qty = position_size / entry
        pnl = qty * (seq.exit_price - entry)
        if i == 0:
            seq.t1_pnl_usd = pnl
        elif i == 1:
            seq.t2_pnl_usd = pnl
        elif i == 2:
            seq.t3_pnl_usd = pnl
    seq.pnl_usd = seq.t1_pnl_usd + seq.t2_pnl_usd + seq.t3_pnl_usd


print("S3 Limit Order backtester defined.")
S3 Limit Order backtester defined.
# =============================================================================
# Cell: Run S3 Backtest
# =============================================================================

s3_results = {}
for symbol in ['BTC', 'ETH', 'SOL']:
    s3_results[symbol] = backtest_limit_accumulation_strategy(s2_data[symbol], symbol)

# Print S3-specific summary with fill rates
def print_s3_summary(sequences: Dict[str, List[LimitOrderSequence]], strategy_name: str):
    print(f"\n{'='*100}")
    print(f"{strategy_name} RESULTS SUMMARY")
    print(f"{'='*100}")
    
    all_seqs = []
    print(f"\n{'Symbol':<8} {'Seqs':>6} {'TP':>6} {'SL':>6} {'Win%':>8} {'Cumulative P&L':>18} {'Avg Tranches':>13}")
    print("-" * 80)
    
    for symbol in ['BTC', 'ETH', 'SOL']:
        seqs = sequences.get(symbol, [])
        all_seqs.extend(seqs)
        
        if not seqs:
            print(f"{symbol:<8} {'N/A':>6}")
            continue
        
        n_tp = sum(1 for s in seqs if s.exit_type == 'TP')
        n_sl = sum(1 for s in seqs if s.exit_type == 'SL')
        wr = n_tp / len(seqs) * 100 if seqs else 0
        total_pnl = sum(s.pnl_usd for s in seqs)
        avg_tr = np.mean([s.tranches_filled for s in seqs])
        
        print(f"{symbol:<8} {len(seqs):>6} {n_tp:>6} {n_sl:>6} {wr:>7.1f}% ${total_pnl:>+16,.0f} {avg_tr:>13.2f}")
    
    if all_seqs:
        print("-" * 80)
        n_total = len(all_seqs)
        n_tp_total = sum(1 for s in all_seqs if s.exit_type == 'TP')
        total_pnl = sum(s.pnl_usd for s in all_seqs)
        avg_tr = np.mean([s.tranches_filled for s in all_seqs])
        
        t2_fills = sum(1 for s in all_seqs if s.bar2_filled)
        t3_fills = sum(1 for s in all_seqs if s.bar3_filled)
        
        print(f"{'TOTAL':<8} {n_total:>6} {n_tp_total:>6} {n_total-n_tp_total:>6} "
              f"{n_tp_total/n_total*100:>7.1f}% ${total_pnl:>+16,.0f} {avg_tr:>13.2f}")
        print(f"\nFill Rates: T1=100% | T2={t2_fills/n_total*100:.1f}% | T3={t3_fills/n_total*100:.1f}%")

print_s3_summary(s3_results, "S3: 4H Limit Order Accumulation")
====================================================================================================
S3: 4H Limit Order Accumulation RESULTS SUMMARY
====================================================================================================

Symbol     Seqs     TP     SL     Win%     Cumulative P&L  Avg Tranches
--------------------------------------------------------------------------------
BTC           1      1      0   100.0% $        +204,695          1.00
ETH           3      3      0   100.0% $        +598,182          1.67
SOL           2      2      0   100.0% $        +220,360          1.00
--------------------------------------------------------------------------------
TOTAL         6      6      0   100.0% $      +1,023,237          1.33

Fill Rates: T1=100% | T2=16.7% | T3=16.7%
# =============================================================================
# Cell: S2 vs S3 Comparison
# =============================================================================

def compare_strategies(s2_results: Dict, s3_results: Dict):
    print("\n" + "="*100)
    print("STRATEGY COMPARISON: S2 (Close-Based) vs S3 (Limit Order)")
    print("="*100)
    
    s2_all = [seq for seqs in s2_results.values() for seq in seqs]
    s3_all = [seq for seqs in s3_results.values() for seq in seqs]
    
    print(f"\n{'Metric':<25} {'S2 (Close-Based)':>25} {'S3 (Limit Order)':>25}")
    print("-" * 80)
    
    print(f"{'Sequences':<25} {len(s2_all):>25} {len(s3_all):>25}")
    
    s2_wr = sum(1 for s in s2_all if s.exit_type == 'TP') / len(s2_all) * 100 if s2_all else 0
    s3_wr = sum(1 for s in s3_all if s.exit_type == 'TP') / len(s3_all) * 100 if s3_all else 0
    print(f"{'Win Rate':<25} {s2_wr:>24.1f}% {s3_wr:>24.1f}%")
    
    s2_pnl = sum(s.pnl_usd for s in s2_all)
    s3_pnl = sum(s.pnl_usd for s in s3_all)
    print(f"{'Cumulative P&L':<25} ${s2_pnl:>+23,.0f} ${s3_pnl:>+23,.0f}")
    
    s2_max_loss = min(s.pnl_usd for s in s2_all) if s2_all else 0
    s3_max_loss = min(s.pnl_usd for s in s3_all) if s3_all else 0
    print(f"{'Max Single Loss':<25} ${s2_max_loss:>+23,.0f} ${s3_max_loss:>+23,.0f}")
    
    s3_avg_tr = np.mean([s.tranches_filled for s in s3_all]) if s3_all else 0
    print(f"{'Avg Tranches (S3 only)':<25} {'3.00':>25} {s3_avg_tr:>25.2f}")
    
    print("="*100)

compare_strategies(s2_results, s3_results)
====================================================================================================
STRATEGY COMPARISON: S2 (Close-Based) vs S3 (Limit Order)
====================================================================================================

Metric                             S2 (Close-Based)          S3 (Limit Order)
--------------------------------------------------------------------------------
Sequences                                         2                         6
Win Rate                                     100.0%                    100.0%
Cumulative P&L            $               +577,261 $             +1,023,237
Max Single Loss           $               +257,915 $                +95,932
Avg Tranches (S3 only)                         3.00                      1.33
====================================================================================================
# =============================================================================
# Cell: S3 Equity Curve
# =============================================================================

def plot_equity_curves(sequences: Dict[str, List], strategy_name: str):
    fig = go.Figure()
    
    for symbol in ['BTC', 'ETH', 'SOL']:
        seqs = sequences.get(symbol, [])
        if not seqs:
            continue
        
        sorted_seqs = sorted([s for s in seqs if s.exit_date], key=lambda x: x.exit_date)
        dates = [s.exit_date for s in sorted_seqs]
        pnl = [s.pnl_usd for s in sorted_seqs]
        cumulative = np.cumsum(pnl)
        
        fig.add_trace(go.Scatter(
            x=dates, y=cumulative, name=symbol,
            line=dict(color=COLORS[symbol], width=2),
            mode='lines+markers'
        ))
    
    fig.update_layout(
        title=f'{strategy_name} - Cumulative P&L',
        xaxis_title='Trade #', yaxis_title='Cumulative P&L (USD)',
        height=500, hovermode='x unified'
    )
    fig.add_hline(y=0, line_dash='dash', line_color='gray', opacity=0.5)
    fig.show()

plot_equity_curves(s3_results, "S3: 4H Limit Order Accumulation")

Section 10: S4 -- T1-Anchored TP/SL Variant¶

Key Modification from S3¶

S4 anchors TP/SL to the T1 entry price instead of Bar 3's close:

  • S3 Reference: reference_price = bar_closes[-1] (Bar 3's close)
  • S4 Reference: reference_price = entry_prices[0] (T1's limit order fill = threshold)

This provides more consistent risk management by locking in levels at the first entry point.

# =============================================================================
# Cell: S4 Backtester (T1-Anchored TP/SL)
# =============================================================================

def backtest_limit_t1_anchor(
    df: pd.DataFrame,
    symbol: str,
    position_size: float = 1_000_000.0,
    tp_pct: float = 0.10,
    sl_pct: float = 0.10,
    max_bars: int = 3
) -> List[LimitOrderSequence]:
    """
    S4: Same as S3 but TP/SL anchored to T1 entry price.
    reference_price = entry_prices[0] (T1 = threshold)
    """
    df = df.copy().reset_index(drop=True)
    sequences = []
    state = TradeState.IDLE
    current: Optional[LimitOrderSequence] = None
    bar_count = 0
    
    for i in range(len(df)):
        row = df.iloc[i]
        threshold = row['vwma50_threshold']
        rsi_ok = row['rsi'] < 20
        
        if state == TradeState.IDLE:
            if rsi_ok and row['low'] < threshold:
                current = LimitOrderSequence(symbol=symbol)
                current.threshold_at_entry = threshold
                current.entry_dates.append(i)
                current.entry_prices.append(threshold)
                current.bar_closes.append(row['close'])
                current.bar1_filled = True
                current.tranches_filled = 1
                bar_count = 1
                state = TradeState.ACCUMULATING
        
        elif state == TradeState.ACCUMULATING:
            bar_count += 1
            current.bar_closes.append(row['close'])
            
            if rsi_ok and row['close'] < current.threshold_at_entry:
                current.entry_dates.append(i)
                current.entry_prices.append(row['close'])
                current.tranches_filled += 1
                if bar_count == 2:
                    current.bar2_filled = True
                elif bar_count == 3:
                    current.bar3_filled = True
            
            if bar_count >= max_bars:
                # CRITICAL: Reference = T1 entry price (NOT Bar 3's close)
                reference = current.entry_prices[0]  # T1 = threshold
                current.reference_price = reference
                current.tp_level = reference * (1 + tp_pct)
                current.sl_level = reference * (1 - sl_pct)
                state = TradeState.HOLDING
        
        elif state == TradeState.HOLDING:
            if row['high'] >= current.tp_level:
                current.exit_date = i
                current.exit_price = current.tp_level
                current.exit_type = 'TP'
                _calc_limit_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
            
            elif row['low'] <= current.sl_level:
                current.exit_date = i
                current.exit_price = current.sl_level
                current.exit_type = 'SL'
                _calc_limit_pnl(current, position_size)
                sequences.append(current)
                current, bar_count, state = None, 0, TradeState.IDLE
    
    return sequences

print("S4 T1-Anchored backtester defined.")
S4 T1-Anchored backtester defined.
# =============================================================================
# Cell: Run S4 Backtest
# =============================================================================

s4_results = {}
for symbol in ['BTC', 'ETH', 'SOL']:
    s4_results[symbol] = backtest_limit_t1_anchor(s2_data[symbol], symbol)

print_s3_summary(s4_results, "S4: T1-Anchored TP/SL")
====================================================================================================
S4: T1-Anchored TP/SL RESULTS SUMMARY
====================================================================================================

Symbol     Seqs     TP     SL     Win%     Cumulative P&L  Avg Tranches
--------------------------------------------------------------------------------
BTC           1      1      0   100.0% $        +100,000          1.00
ETH           3      3      0   100.0% $        +534,586          1.67
SOL           2      2      0   100.0% $        +200,000          1.00
--------------------------------------------------------------------------------
TOTAL         6      6      0   100.0% $        +834,586          1.33

Fill Rates: T1=100% | T2=16.7% | T3=16.7%
# =============================================================================
# Cell: S3 vs S4 Comparison
# =============================================================================

def compare_s3_s4(s3_results: Dict, s4_results: Dict):
    print("\n" + "="*100)
    print("STRATEGY COMPARISON: S3 (Bar 3 Close Ref) vs S4 (T1 Anchor Ref)")
    print("="*100)
    
    s3_all = [seq for seqs in s3_results.values() for seq in seqs]
    s4_all = [seq for seqs in s4_results.values() for seq in seqs]
    
    print(f"\n{'Metric':<30} {'S3 (Bar 3 Close)':>25} {'S4 (T1 Anchor)':>25}")
    print("-" * 85)
    
    print(f"{'Sequences':<30} {len(s3_all):>25} {len(s4_all):>25}")
    
    s3_wr = sum(1 for s in s3_all if s.exit_type == 'TP') / len(s3_all) * 100 if s3_all else 0
    s4_wr = sum(1 for s in s4_all if s.exit_type == 'TP') / len(s4_all) * 100 if s4_all else 0
    print(f"{'Win Rate':<30} {s3_wr:>24.1f}% {s4_wr:>24.1f}%")
    
    s3_pnl = sum(s.pnl_usd for s in s3_all)
    s4_pnl = sum(s.pnl_usd for s in s4_all)
    print(f"{'Cumulative P&L':<30} ${s3_pnl:>+23,.0f} ${s4_pnl:>+23,.0f}")
    
    s3_t1 = sum(s.t1_pnl_usd for s in s3_all)
    s4_t1 = sum(s.t1_pnl_usd for s in s4_all)
    print(f"{'T1 Total P&L':<30} ${s3_t1:>+23,.0f} ${s4_t1:>+23,.0f}")
    
    s3_max = min(s.pnl_usd for s in s3_all) if s3_all else 0
    s4_max = min(s.pnl_usd for s in s4_all) if s4_all else 0
    print(f"{'Max Single Loss':<30} ${s3_max:>+23,.0f} ${s4_max:>+23,.0f}")
    
    print("="*100)
    print("\nKey Insight: T1-anchored TP/SL provides more predictable risk levels.")

compare_s3_s4(s3_results, s4_results)
====================================================================================================
STRATEGY COMPARISON: S3 (Bar 3 Close Ref) vs S4 (T1 Anchor Ref)
====================================================================================================

Metric                                  S3 (Bar 3 Close)            S4 (T1 Anchor)
-------------------------------------------------------------------------------------
Sequences                                              6                         6
Win Rate                                          100.0%                    100.0%
Cumulative P&L                 $             +1,023,237 $               +834,586
T1 Total P&L                   $               +854,692 $               +600,000
Max Single Loss                $                +95,932 $               +100,000
====================================================================================================

Key Insight: T1-anchored TP/SL provides more predictable risk levels.
# =============================================================================
# Cell: S4 Equity Curve
# =============================================================================

plot_equity_curves(s4_results, "S4: T1-Anchored TP/SL")
# =============================================================================
# Cell: Combined Equity Curve Comparison (All Strategies)
# =============================================================================

fig = go.Figure()

strategies = [
    ('S1: Daily RSI(30)+VWMA(20)', s1_results, 'rgba(255, 255, 255, 0.8)'),
    ('S2: 4H Close-Based', s2_results, 'rgba(0, 255, 255, 0.8)'),
    ('S3: 4H Limit Order', s3_results, 'rgba(255, 165, 0, 0.8)'),
    ('S4: T1-Anchored', s4_results, 'rgba(0, 255, 0, 0.8)')
]

for name, results, color in strategies:
    all_seqs = []
    for symbol_seqs in results.values():
        all_seqs.extend([s for s in symbol_seqs if hasattr(s, 'exit_date') and s.exit_date])
    
    if not all_seqs:
        continue
    
    sorted_seqs = sorted(all_seqs, key=lambda x: x.exit_date)
    dates = list(range(len(sorted_seqs)))
    pnl = [s.pnl_usd for s in sorted_seqs]
    cumulative = np.cumsum(pnl)
    
    fig.add_trace(go.Scatter(
        x=dates, y=cumulative, name=name,
        line=dict(color=color, width=2), mode='lines'
    ))

fig.update_layout(
    title='All Strategies - Cumulative P&L Comparison',
    xaxis_title='Trade #', yaxis_title='Cumulative P&L (USD)',
    height=600, hovermode='x unified',
    legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig.add_hline(y=0, line_dash='dash', line_color='gray', opacity=0.5)
fig.show()
# =============================================================================
# Cell: Final Summary Table
# =============================================================================

def print_final_summary():
    print("\n" + "="*130)
    print("FINAL SUMMARY: ALL STRATEGIES COMPARISON")
    print("="*130)
    
    strategies_data = [
        ('S1: Daily RSI(30)+VWMA(20)', s1_results),
        ('S2: 4H Close-Based', s2_results),
        ('S3: 4H Limit Order', s3_results),
        ('S4: T1-Anchored TP/SL', s4_results)
    ]
    
    print(f"\n{'Strategy':<30} {'Seqs':>8} {'Win%':>8} {'Cumulative P&L':>18} {'Max Loss':>14} {'Avg Tranches':>12}")
    print("-" * 100)
    
    for name, results in strategies_data:
        all_seqs = [seq for seqs in results.values() for seq in seqs]
        
        if not all_seqs:
            print(f"{name:<30} {'N/A':>8}")
            continue
        
        n_seqs = len(all_seqs)
        n_tp = sum(1 for s in all_seqs if s.exit_type == 'TP')
        wr = n_tp / n_seqs * 100 if n_seqs > 0 else 0
        total_pnl = sum(s.pnl_usd for s in all_seqs)
        max_loss = min(s.pnl_usd for s in all_seqs)
        avg_tr = np.mean([s.tranches_filled for s in all_seqs])
        
        print(f"{name:<30} {n_seqs:>8} {wr:>7.1f}% ${total_pnl:>+16,.0f} ${max_loss:>+12,.0f} {avg_tr:>12.2f}")
    
    print("="*130)

print_final_summary()
==================================================================================================================================
FINAL SUMMARY: ALL STRATEGIES COMPARISON
==================================================================================================================================

Strategy                           Seqs     Win%     Cumulative P&L       Max Loss Avg Tranches
----------------------------------------------------------------------------------------------------
S1: Daily RSI(30)+VWMA(20)            2    50.0% $              +0 $    -100,000         1.00
S2: 4H Close-Based                    2   100.0% $        +577,261 $    +257,915         3.00
S3: 4H Limit Order                    6   100.0% $      +1,023,237 $     +95,932         1.33
S4: T1-Anchored TP/SL                 6   100.0% $        +834,586 $    +100,000         1.33
==================================================================================================================================

Conclusion¶

Summary of Findings¶

This research compared four mean-reversion accumulation strategies for crash-buying crypto assets:

Strategy Timeframe Entry Logic TP/SL Reference Key Characteristics
S1 Daily Close < 80% VWMA(20), RSI < 30 Bar 3 close Captures macro crashes
S2 4H Close < 80% VWMA(50), RSI < 20 Bar 3 close More signals, higher frequency
S3 4H Low < threshold (T1), Close < threshold (T2/T3) Bar 3 close Better entry prices
S4 4H Same as S3 T1 entry price Predictable risk levels

Key Takeaways¶

  1. VWMA outperforms simple MA for crash detection due to volume weighting
  2. Limit order execution (S3/S4) captures better entry prices during volatile crashes
  3. T1-anchored TP/SL (S4) provides more consistent risk management
  4. Fill rate analysis is critical for realistic strategy evaluation
  5. Per-tranche P&L tracking reveals contribution of each accumulation layer

Recommended Strategy: S3/S4 (Limit Order Variants)¶

Parameters:

  • Timeframe: 4H
  • Entry Signal: RSI(14) < 20 AND Low < VWMA(50) * 0.80
  • T1 Entry: Limit order at threshold
  • T2/T3: Only if close < threshold, else skip
  • TP/SL: +/-10% from reference price
  • Position Size: $1M-$3M (dynamic based on fills)

End of Research Notebook

Trade-Matrix Quantitative Research | February 2026