Gap & Go Strategy
La Estrategia Rey de Small Caps
Gap & Go es la estrategia más conocida y rentable para small caps. Cuando un stock gappea arriba con volumen y continúa, puedes capturar movimientos explosivos. La clave está en el screening y timing.
⚠️ DISCLAIMER: Small caps son extremadamente volátiles. Esta estrategia requiere experiencia, capital suficiente y gestión de riesgo estricta. Los ejemplos son educativos, no consejos financieros.
Filosofía del Gap & Go
¿Por Qué Funciona?
- FOMO Retail: Gaps grandes atraen compradores retail
- Momentum Cascade: Un gap con volumen crea más compradores
- Short Squeeze: Shorts atrapados cubren posiciones
- Algorithmic Following: Bots siguen momentum con volumen
Tipos de Gaps
def classify_gap_type(gap_pct, volume_ratio, float_shares, catalyst):
"""Clasificar tipo de gap para strategy selection"""
gap_types = {
'explosive': {
'gap_range': (25, 100),
'volume_min': 10,
'float_max': 10_000_000,
'catalysts': ['fda_approval', 'buyout_rumor', 'major_contract']
},
'momentum': {
'gap_range': (10, 25),
'volume_min': 5,
'float_max': 30_000_000,
'catalysts': ['earnings_beat', 'analyst_upgrade', 'breakthrough']
},
'continuation': {
'gap_range': (5, 15),
'volume_min': 3,
'float_max': 50_000_000,
'catalysts': ['follow_through', 'sector_momentum']
},
'weak': {
'gap_range': (2, 8),
'volume_min': 1.5,
'float_max': 100_000_000,
'catalysts': ['technical', 'sympathy']
}
}
for gap_type, criteria in gap_types.items():
if (criteria['gap_range'][0] <= gap_pct <= criteria['gap_range'][1] and
volume_ratio >= criteria['volume_min'] and
float_shares <= criteria['float_max']):
return {
'type': gap_type,
'strength': 'high' if catalyst in criteria['catalysts'] else 'medium',
'expected_continuation': get_continuation_probability(gap_type, catalyst)
}
return {'type': 'no_play', 'strength': 'none', 'expected_continuation': 0}
def get_continuation_probability(gap_type, catalyst):
"""Historical continuation probabilities"""
probabilities = {
'explosive': {'fda_approval': 0.85, 'buyout_rumor': 0.75, 'major_contract': 0.70},
'momentum': {'earnings_beat': 0.65, 'analyst_upgrade': 0.55, 'breakthrough': 0.60},
'continuation': {'follow_through': 0.45, 'sector_momentum': 0.40},
'weak': {'technical': 0.25, 'sympathy': 0.20}
}
return probabilities.get(gap_type, {}).get(catalyst, 0.30)
Pre-Market Scanner
1. Core Screening Logic
class GapAndGoScanner:
def __init__(self):
self.min_gap_pct = 10
self.min_volume = 500_000
self.max_float = 50_000_000
self.min_price = 1.00
self.max_price = 50.00
def scan_premarket_gaps(self, scan_time='08:00'):
"""Scan pre-market para gap candidates"""
candidates = []
# Get pre-market movers
movers = self.get_premarket_movers()
for ticker in movers:
try:
data = self.get_stock_data(ticker)
# Basic filters
if not self.passes_basic_filters(data):
continue
# Calculate metrics
metrics = self.calculate_gap_metrics(data)
# Score the setup
score = self.score_gap_setup(data, metrics)
if score['total_score'] >= 7: # Minimum score
candidates.append({
'ticker': ticker,
'gap_pct': metrics['gap_pct'],
'premarket_volume': metrics['pm_volume'],
'rvol': metrics['rvol'],
'float': data['float_shares'],
'price': data['current_price'],
'catalyst': data.get('catalyst', 'unknown'),
'score': score['total_score'],
'rank': self.calculate_rank(metrics, score)
})
except Exception as e:
print(f"Error scanning {ticker}: {e}")
continue
# Sort by rank
return sorted(candidates, key=lambda x: x['rank'], reverse=True)
def passes_basic_filters(self, data):
"""Basic filtering criteria"""
return (
self.min_price <= data['current_price'] <= self.max_price and
data['gap_pct'] >= self.min_gap_pct and
data['premarket_volume'] >= self.min_volume and
data['float_shares'] <= self.max_float and
data['avg_volume_20d'] >= 100_000 # Minimum liquidity
)
def calculate_gap_metrics(self, data):
"""Calculate key gap metrics"""
prev_close = data['prev_close']
current_price = data['current_price']
pm_volume = data['premarket_volume']
avg_volume = data['avg_volume_20d']
return {
'gap_pct': (current_price - prev_close) / prev_close * 100,
'gap_dollars': current_price - prev_close,
'pm_volume': pm_volume,
'rvol': pm_volume / (avg_volume * 0.1), # Assuming 10% of volume pre-market
'price_vs_high': current_price / data['prev_day_high'],
'float_turnover': pm_volume / data['float_shares'] * 100
}
def score_gap_setup(self, data, metrics):
"""Score the gap setup quality"""
scores = {}
# Gap size score
gap_pct = metrics['gap_pct']
if gap_pct >= 30:
scores['gap_size'] = 10
elif gap_pct >= 20:
scores['gap_size'] = 8
elif gap_pct >= 15:
scores['gap_size'] = 6
else:
scores['gap_size'] = 4
# Volume score
rvol = metrics['rvol']
if rvol >= 20:
scores['volume'] = 10
elif rvol >= 10:
scores['volume'] = 8
elif rvol >= 5:
scores['volume'] = 6
else:
scores['volume'] = 3
# Float score
float_shares = data['float_shares']
if float_shares < 5_000_000:
scores['float'] = 10
elif float_shares < 15_000_000:
scores['float'] = 8
elif float_shares < 30_000_000:
scores['float'] = 6
else:
scores['float'] = 3
# Catalyst score
catalyst = data.get('catalyst', 'none')
catalyst_scores = {
'fda_approval': 10, 'buyout_rumor': 9, 'major_contract': 8,
'earnings_beat': 7, 'analyst_upgrade': 6, 'breakthrough': 6,
'follow_through': 4, 'technical': 3, 'sympathy': 2, 'none': 1
}
scores['catalyst'] = catalyst_scores.get(catalyst, 1)
# Price level score
price = data['current_price']
if 5 <= price <= 20: # Sweet spot
scores['price_level'] = 10
elif 2 <= price < 5 or 20 < price <= 30:
scores['price_level'] = 7
else:
scores['price_level'] = 4
# Calculate weighted total
weights = {
'gap_size': 0.25,
'volume': 0.30,
'float': 0.20,
'catalyst': 0.15,
'price_level': 0.10
}
total_score = sum(scores[factor] * weights[factor] for factor in scores)
return {
'total_score': total_score,
'individual_scores': scores,
'weights': weights
}
2. Real-Time Monitoring
class GapMonitor:
def __init__(self, watchlist):
self.watchlist = watchlist
self.alerts = []
self.position_tracker = {}
def monitor_gaps(self):
"""Monitor gap stocks during market hours"""
for ticker in self.watchlist:
current_data = self.get_real_time_data(ticker)
# Entry signals
if self.check_entry_signal(ticker, current_data):
self.send_entry_alert(ticker, current_data)
# Position management
if ticker in self.position_tracker:
self.manage_position(ticker, current_data)
def check_entry_signal(self, ticker, data):
"""Check for gap continuation entry signals"""
# Must be above pre-market high
if data['price'] <= data['premarket_high']:
return False
# Volume confirmation
if data['volume'] < data['avg_volume'] * 2:
return False
# Time filter (usually first 2 hours work best)
current_time = pd.Timestamp.now().time()
if current_time > pd.Timestamp('11:30').time():
return False
# Momentum check
if data['price'] < data['vwap']:
return False
return True
def manage_position(self, ticker, data):
"""Manage existing gap position"""
position = self.position_tracker[ticker]
# Trailing stop management
if data['price'] > position['highest_price']:
position['highest_price'] = data['price']
# Update trailing stop
new_stop = data['price'] * 0.85 # 15% trailing stop
if new_stop > position['stop_price']:
position['stop_price'] = new_stop
self.send_stop_update_alert(ticker, new_stop)
# Exit signals
if self.check_exit_signal(ticker, data, position):
self.send_exit_alert(ticker, data)
def check_exit_signal(self, ticker, data, position):
"""Check for exit signals"""
# Stop loss hit
if data['price'] <= position['stop_price']:
return True
# VWAP loss
if data['price'] < data['vwap'] and data['volume'] > data['avg_volume']:
return True
# End of day exit
current_time = pd.Timestamp.now().time()
if current_time >= pd.Timestamp('15:45').time():
return True
return False
Entry Strategies
1. Breakout Entry
def breakout_entry_strategy(data, position_type='aggressive'):
"""Entry on breakout of pre-market high"""
entry_signals = []
# Aggressive entry: break of pre-market high
if position_type == 'aggressive':
if (data['price'] > data['premarket_high'] and
data['volume'] > data['avg_volume'] * 3):
entry_signals.append({
'type': 'aggressive_breakout',
'entry_price': data['premarket_high'] + 0.01,
'stop_loss': data['vwap'] * 0.97,
'target': data['premarket_high'] * 1.20,
'confidence': 0.75
})
# Conservative entry: pullback to VWAP then breakout
elif position_type == 'conservative':
if (data['price'] > data['premarket_high'] and
data['previous_candle_low'] <= data['vwap'] and
data['price'] > data['vwap']):
entry_signals.append({
'type': 'vwap_reclaim_breakout',
'entry_price': data['vwap'] + 0.01,
'stop_loss': data['vwap'] * 0.98,
'target': data['premarket_high'] * 1.15,
'confidence': 0.85
})
return entry_signals
def layer_entry_strategy(data, total_size):
"""Layered entry approach"""
entries = []
# Entry 1: 30% on initial breakout
entries.append({
'percentage': 0.30,
'trigger': data['premarket_high'],
'stop': data['vwap'] * 0.97
})
# Entry 2: 40% on continuation above resistance
resistance_level = data['premarket_high'] * 1.05
entries.append({
'percentage': 0.40,
'trigger': resistance_level,
'stop': data['premarket_high'] * 0.98
})
# Entry 3: 30% on strong momentum
momentum_level = data['premarket_high'] * 1.15
entries.append({
'percentage': 0.30,
'trigger': momentum_level,
'stop': resistance_level * 0.95
})
return entries
2. VWAP Strategy Integration
class GapVWAPStrategy:
def __init__(self):
self.positions = {}
self.alerts = []
def analyze_gap_vwap_setup(self, ticker, data):
"""Analyze gap stock usando VWAP"""
analysis = {
'ticker': ticker,
'gap_pct': data['gap_pct'],
'current_price': data['price'],
'vwap': data['vwap'],
'signals': []
}
# Signal 1: Holding above VWAP after gap
if (data['price'] > data['vwap'] and
data['volume'] > data['avg_volume'] * 2):
analysis['signals'].append({
'type': 'vwap_hold',
'strength': 'medium',
'entry': data['price'],
'stop': data['vwap'] * 0.99,
'target': data['price'] * 1.10
})
# Signal 2: VWAP reclaim after pullback
if (data['price'] > data['vwap'] and
data['low_of_day'] < data['vwap'] and
data['volume'] > data['avg_volume'] * 1.5):
analysis['signals'].append({
'type': 'vwap_reclaim',
'strength': 'high',
'entry': data['vwap'] + 0.01,
'stop': data['low_of_day'] * 0.99,
'target': data['premarket_high'] * 1.05
})
# Signal 3: VWAP rejection (short setup)
if (data['price'] < data['vwap'] and
data['high_of_day'] > data['vwap'] and
data['volume'] > data['avg_volume'] * 2):
analysis['signals'].append({
'type': 'vwap_rejection',
'strength': 'medium',
'entry': data['vwap'] - 0.01,
'stop': data['vwap'] * 1.02,
'target': data['price'] * 0.90,
'direction': 'short'
})
return analysis
Risk Management para Gap & Go
1. Position Sizing Específico
def calculate_gap_position_size(account_value, gap_data, risk_tolerance=0.015):
"""Position sizing específico para gaps"""
base_risk = account_value * risk_tolerance
# Adjust risk based on gap characteristics
risk_multipliers = {
'gap_size': 1.0,
'volume': 1.0,
'float': 1.0,
'time': 1.0
}
# Gap size adjustment
gap_pct = gap_data['gap_pct']
if gap_pct > 50:
risk_multipliers['gap_size'] = 0.5 # Reduce risk on huge gaps
elif gap_pct > 30:
risk_multipliers['gap_size'] = 0.75
elif gap_pct < 10:
risk_multipliers['gap_size'] = 1.25 # Increase risk on smaller gaps
# Volume adjustment
rvol = gap_data['rvol']
if rvol < 2:
risk_multipliers['volume'] = 0.5 # Low volume = lower confidence
elif rvol > 10:
risk_multipliers['volume'] = 1.25
# Float adjustment
float_shares = gap_data['float_shares']
if float_shares < 5_000_000:
risk_multipliers['float'] = 0.75 # Micro float = more risk
elif float_shares > 30_000_000:
risk_multipliers['float'] = 1.25 # Larger float = less risk
# Time adjustment
current_time = pd.Timestamp.now().time()
if current_time > pd.Timestamp('11:00').time():
risk_multipliers['time'] = 0.75 # Later in day = less reliable
# Final risk calculation
final_multiplier = 1.0
for factor, multiplier in risk_multipliers.items():
final_multiplier *= multiplier
adjusted_risk = base_risk * final_multiplier
# Calculate shares
entry_price = gap_data['entry_price']
stop_price = gap_data['stop_price']
risk_per_share = entry_price - stop_price
if risk_per_share <= 0:
return 0
shares = int(adjusted_risk / risk_per_share)
# Position limits
max_position_value = account_value * 0.10 # 10% max en small caps volátiles
max_shares = int(max_position_value / entry_price)
return min(shares, max_shares)
2. Stop Loss Strategies
class GapStopManager:
def __init__(self):
self.stop_strategies = ['vwap', 'premarket_low', 'trailing', 'time']
def calculate_stop_levels(self, data, entry_price, strategy='adaptive'):
"""Calculate multiple stop levels"""
stops = {}
# VWAP stop
stops['vwap'] = data['vwap'] * 0.99
# Pre-market low stop
stops['premarket_low'] = data['premarket_low'] * 0.98
# Percentage stop
stops['percentage'] = entry_price * 0.95 # 5% stop
# ATR stop
if 'atr' in data:
stops['atr'] = entry_price - (data['atr'] * 1.5)
# Time-based stop
stops['time_based'] = self.get_time_based_stop(data, entry_price)
if strategy == 'adaptive':
# Choose best stop based on context
chosen_stop = self.choose_optimal_stop(stops, data)
else:
chosen_stop = stops.get(strategy, stops['vwap'])
return {
'recommended_stop': chosen_stop,
'all_stops': stops,
'stop_strategy': strategy
}
def choose_optimal_stop(self, stops, data):
"""Choose optimal stop based on gap characteristics"""
# For small gaps, use tighter stops
if data['gap_pct'] < 15:
return max(stops['vwap'], stops['percentage'])
# For large gaps, use wider stops
elif data['gap_pct'] > 30:
return min(stops['premarket_low'], stops['atr'])
# For medium gaps, use VWAP
else:
return stops['vwap']
def get_time_based_stop(self, data, entry_price):
"""Stop based on time of day"""
current_time = pd.Timestamp.now().time()
if current_time < pd.Timestamp('10:30').time():
# Early morning - use wider stops
return entry_price * 0.93
elif current_time < pd.Timestamp('14:00').time():
# Mid-day - normal stops
return entry_price * 0.95
else:
# Late day - tighter stops
return entry_price * 0.97
Exit Strategies
1. Profit Taking Levels
def calculate_profit_targets(entry_price, gap_data):
"""Calculate multiple profit targets"""
targets = {}
# Technical targets
targets['r1'] = entry_price * 1.05 # 5% quick profit
targets['r2'] = entry_price * 1.10 # 10% target
targets['r3'] = entry_price * 1.20 # Extension target
# Gap-specific targets
gap_pct = gap_data['gap_pct']
# Target based on gap size
if gap_pct > 30:
targets['gap_extension'] = entry_price * 1.50 # Large gaps can run far
elif gap_pct > 15:
targets['gap_extension'] = entry_price * 1.25
else:
targets['gap_extension'] = entry_price * 1.15
# Resistance levels
if 'resistance_levels' in gap_data:
targets['resistance'] = gap_data['resistance_levels'][0]
# Previous day high
if 'prev_day_high' in gap_data:
if gap_data['prev_day_high'] > entry_price:
targets['prev_high'] = gap_data['prev_day_high']
# Float-based targets
float_shares = gap_data['float_shares']
if float_shares < 10_000_000:
# Low float - can run further
targets['float_adjusted'] = entry_price * 1.30
else:
targets['float_adjusted'] = entry_price * 1.15
# Sort targets by price
sorted_targets = sorted(
[(name, price) for name, price in targets.items() if price > entry_price],
key=lambda x: x[1]
)
return {
'targets': targets,
'sorted_targets': sorted_targets,
'recommended_sequence': sorted_targets[:3] # First 3 targets
}
def scaling_exit_strategy(position_size, targets):
"""Scaling exit strategy"""
exit_plan = []
remaining_size = position_size
# Target 1: Take 25% at first resistance
t1_size = int(position_size * 0.25)
exit_plan.append({
'target_price': targets['sorted_targets'][0][1],
'shares_to_sell': t1_size,
'remaining': remaining_size - t1_size,
'reason': 'quick_profit'
})
remaining_size -= t1_size
# Target 2: Take 50% at second target
t2_size = int(remaining_size * 0.50)
exit_plan.append({
'target_price': targets['sorted_targets'][1][1] if len(targets['sorted_targets']) > 1 else targets['sorted_targets'][0][1] * 1.1,
'shares_to_sell': t2_size,
'remaining': remaining_size - t2_size,
'reason': 'main_target'
})
remaining_size -= t2_size
# Target 3: Let remaining ride with trailing stop
exit_plan.append({
'target_price': 'trailing_stop',
'shares_to_sell': remaining_size,
'remaining': 0,
'reason': 'let_winners_run'
})
return exit_plan
Backtesting Gap & Go
1. Historical Performance Analysis
def backtest_gap_and_go(historical_data, start_date, end_date):
"""Backtest gap and go strategy"""
results = {
'trades': [],
'daily_pnl': [],
'metrics': {}
}
scanner = GapAndGoScanner()
# Iterate through each trading day
for date in pd.date_range(start_date, end_date, freq='B'): # Business days
# Get gap candidates for this day
daily_candidates = scanner.scan_historical_gaps(date)
for candidate in daily_candidates:
# Simulate trade
trade_result = simulate_gap_trade(candidate, historical_data[date])
if trade_result:
results['trades'].append(trade_result)
results['daily_pnl'].append(trade_result['pnl'])
# Calculate metrics
results['metrics'] = calculate_gap_strategy_metrics(results['trades'])
return results
def simulate_gap_trade(candidate, intraday_data):
"""Simulate a single gap trade"""
ticker = candidate['ticker']
if ticker not in intraday_data:
return None
data = intraday_data[ticker]
# Entry logic
entry_price = None
entry_time = None
# Look for breakout of pre-market high
for timestamp, bar in data.iterrows():
if (bar['high'] > candidate['premarket_high'] and
bar['volume'] > candidate['avg_volume'] * 2):
entry_price = candidate['premarket_high'] + 0.01
entry_time = timestamp
break
if not entry_price:
return None # No entry signal
# Exit logic
stop_price = candidate['vwap'] * 0.97
target_price = entry_price * 1.15
exit_price = None
exit_time = None
exit_reason = None
# Look for exit
for timestamp, bar in data[data.index > entry_time].iterrows():
# Stop loss
if bar['low'] <= stop_price:
exit_price = stop_price
exit_time = timestamp
exit_reason = 'stop_loss'
break
# Target hit
if bar['high'] >= target_price:
exit_price = target_price
exit_time = timestamp
exit_reason = 'target_hit'
break
# End of day exit
if timestamp.time() >= pd.Timestamp('15:45').time():
exit_price = bar['close']
exit_time = timestamp
exit_reason = 'eod_exit'
break
if not exit_price:
exit_price = data.iloc[-1]['close']
exit_time = data.index[-1]
exit_reason = 'eod_exit'
# Calculate results
pnl_pct = (exit_price - entry_price) / entry_price
hold_time = (exit_time - entry_time).total_seconds() / 60 # minutes
return {
'ticker': ticker,
'date': entry_time.date(),
'entry_time': entry_time,
'exit_time': exit_time,
'entry_price': entry_price,
'exit_price': exit_price,
'pnl_pct': pnl_pct,
'hold_time': hold_time,
'exit_reason': exit_reason,
'gap_pct': candidate['gap_pct'],
'rvol': candidate['rvol'],
'float': candidate['float']
}
Mi Setup Personal
# gap_and_go_config.py
GAP_CONFIG = {
'screening': {
'min_gap_pct': 12,
'min_premarket_volume': 750_000,
'max_float': 40_000_000,
'min_price': 2.00,
'max_price': 40.00,
'min_rvol': 3.0
},
'entry': {
'strategy': 'layered', # 'aggressive', 'conservative', 'layered'
'entry_1_pct': 0.30, # 30% on breakout
'entry_2_pct': 0.50, # 50% on continuation
'entry_3_pct': 0.20, # 20% on momentum
'max_entry_time': '11:00'
},
'risk_management': {
'base_risk_pct': 0.015, # 1.5% para principiantes, 2.5% para avanzados
'max_position_pct': 0.10, # 10% max en small caps volátiles
'stop_strategy': 'adaptive',
'time_stop': '15:45',
'beginner_mode': True # Usar parámetros conservadores
},
'profit_targets': {
'quick_profit': 0.08, # 8% quick scalp
'main_target': 0.15, # 15% main target
'extension': 0.25, # 25% moon shot
'scaling': True
}
}
def my_gap_and_go_workflow():
"""Mi workflow completo de Gap & Go"""
# 1. Pre-market scan (7:30 AM)
scanner = GapAndGoScanner()
candidates = scanner.scan_premarket_gaps()
# 2. Filter top candidates
top_candidates = [c for c in candidates if c['score'] >= 8][:5]
# 3. Set alerts for market open
for candidate in top_candidates:
setup_entry_alerts(candidate)
# 4. Monitor and execute during market hours
return execute_gap_strategy(top_candidates)
Siguiente Paso
Continuemos con VWAP Reclaim, otra estrategia fundamental para small caps.