Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ MACD_FAST=12 # Fast EMA for MACD line
MACD_SLOW=26 # Slow EMA for MACD line
MACD_SIGNAL=9 # Signal line smoothing period

# MACD Alignment for Buys (Momentum Confirmation)
# Requires MACD histogram to show non-declining momentum before buying
# Helps prevent buying during falling knife scenarios where RSI/Bollinger
# show oversold but momentum is still declining (common in flash crashes)
# Post-mortem analysis: All 4 losing trades had negative MACD histogram (-37 to -85)
REQUIRE_MACD_ALIGNMENT=false # Enable MACD alignment check for buys
MACD_ALIGNMENT_THRESHOLD=-0.5 # Histogram must be above this (-5.0 to 5.0)

# Bollinger Bands - Volatility and mean reversion
BOLLINGER_PERIOD=20 # SMA lookback period
BOLLINGER_STD=2.0 # Band width in standard deviations (2.0 = 95%)
Expand Down
15 changes: 15 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ class Settings(BaseSettings):
macd_slow: int = Field(default=26, ge=5, le=100)
macd_signal: int = Field(default=9, ge=2, le=50)

# MACD Alignment for Buy Signals (Momentum Confirmation)
# Requires MACD histogram to show non-declining momentum before buying
# Helps prevent buying during "falling knife" scenarios where RSI/Bollinger
# indicate oversold but momentum is still declining
require_macd_alignment: bool = Field(
default=False,
description="Require MACD histogram to be above threshold for buy signals"
)
macd_alignment_threshold: float = Field(
default=-0.5,
ge=-5.0,
le=5.0,
description="MACD histogram must be above this for buy signals (negative = allow slight decline)"
)

# Strategy Parameters - Bollinger Bands
bollinger_period: int = Field(default=20, ge=5, le=100)
bollinger_std: float = Field(default=2.0, ge=1.0, le=4.0)
Expand Down
2 changes: 2 additions & 0 deletions src/daemon/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ def __init__(self, settings: Settings):
adx_period=settings.adx_period,
adx_weak_threshold=settings.adx_weak_threshold,
adx_strong_threshold=settings.adx_strong_threshold,
require_macd_alignment=settings.require_macd_alignment,
macd_alignment_threshold=settings.macd_alignment_threshold,
)

self.position_sizer = PositionSizer(
Expand Down
42 changes: 42 additions & 0 deletions src/strategy/signal_scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def __init__(
adx_period: int = 14,
adx_weak_threshold: float = 20.0,
adx_strong_threshold: float = 25.0,
require_macd_alignment: bool = False,
macd_alignment_threshold: float = -0.5,
):
"""
Initialize signal scorer.
Expand Down Expand Up @@ -299,6 +301,10 @@ def __init__(
self.adx_weak_threshold = adx_weak_threshold
self.adx_strong_threshold = adx_strong_threshold

# MACD alignment parameters (momentum confirmation for buys)
self.require_macd_alignment = require_macd_alignment
self.macd_alignment_threshold = macd_alignment_threshold

def get_min_candles(self) -> int:
"""
Get minimum number of candles required for indicator calculations.
Expand Down Expand Up @@ -1201,6 +1207,42 @@ def calculate_score(
else:
action = "hold"

# MACD Alignment Check for Buy Signals (Momentum Confirmation)
# When enabled, requires MACD histogram to be above threshold to confirm
# momentum is not declining before allowing buy trades.
# This prevents buying during "falling knife" scenarios where RSI/Bollinger
# indicate oversold but momentum is still strongly negative.
macd_alignment_blocked = False
if self.require_macd_alignment and action == "buy":
macd_histogram = indicators.macd_histogram
if macd_histogram is not None:
if macd_histogram < self.macd_alignment_threshold:
# MACD histogram is below threshold - block the buy
macd_alignment_blocked = True
logger.info(
"macd_alignment_blocked_buy",
macd_histogram=round(macd_histogram, 2),
threshold=self.macd_alignment_threshold,
original_score=total_score,
action="downgraded to hold",
)
action = "hold"
else:
logger.debug(
"macd_alignment_passed",
macd_histogram=round(macd_histogram, 2),
threshold=self.macd_alignment_threshold,
)
else:
# No MACD data available - conservative: block buy
macd_alignment_blocked = True
logger.warning(
"macd_alignment_no_data",
action="downgraded to hold (no MACD data)",
)
action = "hold"
metadata["_macd_alignment_blocked"] = 1 if macd_alignment_blocked else 0

# Calculate confidence with confluence factor
# Combines magnitude with how many indicators agree
if action != "hold":
Expand Down
Loading