feat: implement dual-pipeline signal engine service
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
New service at services/signal_engine/ implementing concurrent heuristic (deterministic scoring) and probabilistic (Bayesian inference) pipelines that evaluate technical signals across 6 timeframes (M30-M) and produce independent BUY/WATCH/SKIP verdicts per ticker per evaluation tick. Components: - Input Normalizer: multi-source data assembly with sentinel fallbacks - Signal Library: Fibonacci, MA Stack, RSI, Cup & Handle, Elliott Wave - Multi-Timeframe Confluence Engine: weighted scoring with D/W/M anchors - Hard Filter Engine: macro_bias, valuation, earnings proximity gating - Heuristic Pipeline: S_total scoring with confidence-gated verdicts - Probabilistic Pipeline: Bayesian log-odds with regime priors, entropy gating, EV_R calculation, and signal correlation penalty - Exit Engine: stop-loss, targets, trailing ATR-based stops - Delta Analyzer: pipeline agreement tracking with rolling Redis metrics - Output Formatter: SignalOutput contract + Recommendation schema mapping - Worker orchestrator: concurrent pipelines with failure isolation - Main entry point: queue polling with fail-safe config loading Infrastructure: - Migration 039: signal_engine_outputs table with 3 indexes - Helm chart: signalEngine service entry (processing tier) - Redis key: QUEUE_SIGNAL_ENGINE constant Tests: 390 tests (unit + property-based) covering all components Config: dual_pipeline_enabled=false by default (safe rollout)
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
"""Elliott Wave signal evaluator.
|
||||
|
||||
Detects Elliott Wave patterns — impulse waves (5-wave structure) and
|
||||
corrective waves (3-wave structure) — using a simplified zigzag pivot
|
||||
filter. Produces a signal with the current wave position and projected
|
||||
direction.
|
||||
|
||||
Wave detection algorithm (simplified):
|
||||
1. Find significant pivot points (local highs and lows) using a zigzag
|
||||
filter that identifies reversals of at least X% of the price range.
|
||||
2. Count alternating pivots to identify wave structure.
|
||||
3. Five alternating pivots = impulse wave (bullish if trending up,
|
||||
bearish if trending down).
|
||||
4. Three alternating pivots after an impulse = corrective wave.
|
||||
|
||||
Signal logic:
|
||||
- Impulse wave 3 or 5: strong signal in the trend direction.
|
||||
- Corrective wave (A, B, C): signal in the opposite direction
|
||||
(anticipating next impulse).
|
||||
- Ambiguous wave count: return ``None``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from services.signal_engine.models import OHLCVBar, SignalDirection, SignalResult
|
||||
from services.signal_engine.signals.base import validate_lookback
|
||||
|
||||
# Default minimum number of bars required for evaluation
|
||||
DEFAULT_MIN_BARS: int = 30
|
||||
|
||||
# Minimum zigzag reversal threshold as a fraction of the price range
|
||||
_DEFAULT_ZIGZAG_PCT: float = 0.05 # 5%
|
||||
|
||||
# Wave type labels
|
||||
WAVE_TYPE_IMPULSE: str = "impulse"
|
||||
WAVE_TYPE_CORRECTIVE: str = "corrective"
|
||||
|
||||
# Impulse wave positions (1-indexed)
|
||||
_IMPULSE_WAVE_COUNT: int = 5
|
||||
# Corrective wave positions
|
||||
_CORRECTIVE_WAVE_COUNT: int = 3
|
||||
|
||||
# Confidence multiplier for wave clarity
|
||||
_CONFIDENCE_MULTIPLIER: float = 0.85
|
||||
|
||||
|
||||
class ElliottWaveEvaluator:
|
||||
"""Elliott Wave pattern signal evaluator.
|
||||
|
||||
Satisfies the :class:`~services.signal_engine.signals.base.SignalEvaluator`
|
||||
protocol.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
min_bars:
|
||||
Minimum number of OHLCV bars required before the evaluator will
|
||||
produce a signal. Defaults to ``30``.
|
||||
zigzag_pct:
|
||||
Minimum reversal threshold as a fraction of the overall price
|
||||
range for the zigzag filter. Defaults to ``0.05`` (5%).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min_bars: int = DEFAULT_MIN_BARS,
|
||||
zigzag_pct: float = _DEFAULT_ZIGZAG_PCT,
|
||||
) -> None:
|
||||
self.min_bars = min_bars
|
||||
self.zigzag_pct = zigzag_pct
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API (SignalEvaluator protocol)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def evaluate(
|
||||
self,
|
||||
bars: list[OHLCVBar],
|
||||
timeframe: str,
|
||||
) -> SignalResult | None:
|
||||
"""Evaluate Elliott Wave pattern on *bars* for *timeframe*.
|
||||
|
||||
Returns ``None`` when there are fewer than :pyattr:`min_bars` bars,
|
||||
when the market is flat (no price range), or when the wave count
|
||||
is ambiguous.
|
||||
"""
|
||||
if not validate_lookback(bars, self.min_bars):
|
||||
return None
|
||||
|
||||
# Compute overall price range for the zigzag threshold
|
||||
overall_high = max(b.high for b in bars)
|
||||
overall_low = min(b.low for b in bars)
|
||||
price_range = overall_high - overall_low
|
||||
|
||||
if price_range <= 0:
|
||||
return None # flat market
|
||||
|
||||
zigzag_threshold = price_range * self.zigzag_pct
|
||||
|
||||
# Find zigzag pivots
|
||||
pivots = _find_zigzag_pivots(bars, zigzag_threshold)
|
||||
|
||||
if len(pivots) < _CORRECTIVE_WAVE_COUNT:
|
||||
return None # not enough pivots for any wave structure
|
||||
|
||||
# Try to identify wave structure from the pivots
|
||||
wave_info = _classify_waves(pivots, price_range)
|
||||
|
||||
if wave_info is None:
|
||||
return None # ambiguous wave count
|
||||
|
||||
wave_type = wave_info["wave_type"]
|
||||
current_position = wave_info["current_position"]
|
||||
trend_up = wave_info["trend_up"]
|
||||
clarity = wave_info["clarity"]
|
||||
|
||||
# Determine direction and strength based on wave type and position
|
||||
direction: SignalDirection
|
||||
strength: float
|
||||
|
||||
if wave_type == WAVE_TYPE_IMPULSE:
|
||||
# Impulse wave: signal in the trend direction
|
||||
direction = SignalDirection.BULLISH if trend_up else SignalDirection.BEARISH
|
||||
# Waves 3 and 5 are the strongest signal points
|
||||
if current_position in (3, 5):
|
||||
strength = min(1.0, clarity * 1.0)
|
||||
else:
|
||||
strength = min(1.0, clarity * 0.6)
|
||||
else:
|
||||
# Corrective wave: signal opposite to the correction
|
||||
# (anticipating next impulse in the original trend direction)
|
||||
direction = SignalDirection.BULLISH if trend_up else SignalDirection.BEARISH
|
||||
strength = min(1.0, clarity * 0.7)
|
||||
|
||||
confidence = min(1.0, clarity * _CONFIDENCE_MULTIPLIER)
|
||||
|
||||
# Build pivot list for metadata (index, price, type)
|
||||
pivot_meta = [
|
||||
{"index": p["index"], "price": p["price"], "type": p["type"]}
|
||||
for p in pivots
|
||||
]
|
||||
|
||||
return SignalResult(
|
||||
signal_type="elliott_wave",
|
||||
timeframe=timeframe,
|
||||
strength=strength,
|
||||
direction=direction,
|
||||
confidence=confidence,
|
||||
metadata={
|
||||
"wave_count": len(pivots),
|
||||
"wave_type": wave_type,
|
||||
"current_wave_position": current_position,
|
||||
"trend_up": trend_up,
|
||||
"clarity": round(clarity, 4),
|
||||
"pivots": pivot_meta,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _find_zigzag_pivots(
|
||||
bars: list[OHLCVBar],
|
||||
threshold: float,
|
||||
) -> list[dict]:
|
||||
"""Find significant pivot points using a zigzag filter.
|
||||
|
||||
A pivot is a local high or low where the price reverses by at least
|
||||
*threshold* from the last confirmed pivot.
|
||||
|
||||
Returns a list of dicts with keys: ``index``, ``price``, ``type``
|
||||
(``"high"`` or ``"low"``).
|
||||
"""
|
||||
if len(bars) < 2:
|
||||
return []
|
||||
|
||||
pivots: list[dict] = []
|
||||
|
||||
# Seed with the first bar's high and low as candidates
|
||||
last_high_idx = 0
|
||||
last_high = bars[0].high
|
||||
last_low_idx = 0
|
||||
last_low = bars[0].low
|
||||
|
||||
# Direction: 1 = looking for a high (trending up), -1 = looking for a low
|
||||
# Start by determining initial direction from first two bars
|
||||
if bars[1].close >= bars[0].close:
|
||||
direction = 1 # trending up, looking for a high
|
||||
else:
|
||||
direction = -1 # trending down, looking for a low
|
||||
|
||||
for i in range(1, len(bars)):
|
||||
bar = bars[i]
|
||||
|
||||
if direction == 1:
|
||||
# Trending up — track the highest high
|
||||
if bar.high >= last_high:
|
||||
last_high = bar.high
|
||||
last_high_idx = i
|
||||
# Check for reversal: price dropped by threshold from the high
|
||||
if last_high - bar.low >= threshold:
|
||||
# Confirm the high as a pivot
|
||||
pivots.append({
|
||||
"index": last_high_idx,
|
||||
"price": last_high,
|
||||
"type": "high",
|
||||
})
|
||||
# Switch direction: now looking for a low
|
||||
direction = -1
|
||||
last_low = bar.low
|
||||
last_low_idx = i
|
||||
else:
|
||||
# Trending down — track the lowest low
|
||||
if bar.low <= last_low:
|
||||
last_low = bar.low
|
||||
last_low_idx = i
|
||||
# Check for reversal: price rose by threshold from the low
|
||||
if bar.high - last_low >= threshold:
|
||||
# Confirm the low as a pivot
|
||||
pivots.append({
|
||||
"index": last_low_idx,
|
||||
"price": last_low,
|
||||
"type": "low",
|
||||
})
|
||||
# Switch direction: now looking for a high
|
||||
direction = 1
|
||||
last_high = bar.high
|
||||
last_high_idx = i
|
||||
|
||||
# Add the final unconfirmed pivot (the current trend endpoint)
|
||||
if direction == 1 and (not pivots or pivots[-1]["type"] != "high"):
|
||||
pivots.append({
|
||||
"index": last_high_idx,
|
||||
"price": last_high,
|
||||
"type": "high",
|
||||
})
|
||||
elif direction == -1 and (not pivots or pivots[-1]["type"] != "low"):
|
||||
pivots.append({
|
||||
"index": last_low_idx,
|
||||
"price": last_low,
|
||||
"type": "low",
|
||||
})
|
||||
|
||||
return pivots
|
||||
|
||||
|
||||
def _classify_waves(
|
||||
pivots: list[dict],
|
||||
price_range: float,
|
||||
) -> dict | None:
|
||||
"""Classify the pivot sequence as impulse or corrective waves.
|
||||
|
||||
Returns a dict with ``wave_type``, ``current_position``, ``trend_up``,
|
||||
and ``clarity``, or ``None`` if the wave count is ambiguous.
|
||||
"""
|
||||
n = len(pivots)
|
||||
|
||||
if n < _CORRECTIVE_WAVE_COUNT:
|
||||
return None
|
||||
|
||||
# Determine overall trend from first to last pivot
|
||||
first_price = pivots[0]["price"]
|
||||
last_price = pivots[-1]["price"]
|
||||
trend_up = last_price > first_price
|
||||
|
||||
# Try impulse wave (5 pivots) first, then corrective (3 pivots)
|
||||
if n >= _IMPULSE_WAVE_COUNT:
|
||||
# Use the last 5 pivots for impulse wave detection
|
||||
impulse_pivots = pivots[-_IMPULSE_WAVE_COUNT:]
|
||||
impulse_result = _check_impulse(impulse_pivots, trend_up, price_range)
|
||||
if impulse_result is not None:
|
||||
return impulse_result
|
||||
|
||||
# Check if there's a corrective wave after an impulse
|
||||
# (need at least 5 + 3 = 8 pivots for impulse + corrective)
|
||||
if n >= _IMPULSE_WAVE_COUNT + _CORRECTIVE_WAVE_COUNT:
|
||||
# Check if the first 5 pivots form an impulse
|
||||
early_impulse = pivots[:_IMPULSE_WAVE_COUNT]
|
||||
early_result = _check_impulse(early_impulse, trend_up, price_range)
|
||||
if early_result is not None:
|
||||
# The remaining pivots may form a corrective wave
|
||||
corrective_pivots = pivots[_IMPULSE_WAVE_COUNT:_IMPULSE_WAVE_COUNT + _CORRECTIVE_WAVE_COUNT]
|
||||
corrective_result = _check_corrective(
|
||||
corrective_pivots, trend_up, price_range,
|
||||
)
|
||||
if corrective_result is not None:
|
||||
return corrective_result
|
||||
|
||||
# Try corrective wave (3 pivots) from the tail
|
||||
if n >= _CORRECTIVE_WAVE_COUNT:
|
||||
corrective_pivots = pivots[-_CORRECTIVE_WAVE_COUNT:]
|
||||
corrective_result = _check_corrective(
|
||||
corrective_pivots, trend_up, price_range,
|
||||
)
|
||||
if corrective_result is not None:
|
||||
return corrective_result
|
||||
|
||||
return None # ambiguous
|
||||
|
||||
|
||||
def _check_impulse(
|
||||
pivots: list[dict],
|
||||
trend_up: bool,
|
||||
price_range: float,
|
||||
) -> dict | None:
|
||||
"""Check if 5 pivots form a valid impulse wave.
|
||||
|
||||
For a bullish impulse (trend_up=True):
|
||||
- Wave 1 (low→high): price rises
|
||||
- Wave 2 (high→low): price falls but stays above wave 1 start
|
||||
- Wave 3 (low→high): price rises above wave 1 high (wave 3 is longest)
|
||||
- Wave 4 (high→low): price falls but stays above wave 1 high
|
||||
- Wave 5 (low→high): price rises to new high
|
||||
|
||||
For bearish impulse, the pattern is inverted.
|
||||
"""
|
||||
if len(pivots) != _IMPULSE_WAVE_COUNT:
|
||||
return None
|
||||
|
||||
prices = [p["price"] for p in pivots]
|
||||
|
||||
if trend_up:
|
||||
# Bullish impulse: alternating low-high-low-high-low or high-low-high-low-high
|
||||
# Check for generally ascending pattern with higher highs
|
||||
valid = _validate_bullish_impulse(prices)
|
||||
else:
|
||||
# Bearish impulse: generally descending pattern with lower lows
|
||||
valid = _validate_bearish_impulse(prices)
|
||||
|
||||
if not valid:
|
||||
return None
|
||||
|
||||
# Compute clarity: how clean the wave structure is
|
||||
clarity = _compute_impulse_clarity(prices, trend_up, price_range)
|
||||
|
||||
# Current position is wave 5 (the last wave in the impulse)
|
||||
return {
|
||||
"wave_type": WAVE_TYPE_IMPULSE,
|
||||
"current_position": 5,
|
||||
"trend_up": trend_up,
|
||||
"clarity": clarity,
|
||||
}
|
||||
|
||||
|
||||
def _validate_bullish_impulse(prices: list[float]) -> bool:
|
||||
"""Validate a 5-pivot sequence as a bullish impulse.
|
||||
|
||||
Simplified rules:
|
||||
- The overall trend is up (last > first).
|
||||
- Wave 3 (pivot 2 to pivot 3) should be the largest move or
|
||||
at least not the shortest.
|
||||
- Wave 2 should not retrace below wave 1 start.
|
||||
- Wave 4 should not overlap wave 1 end.
|
||||
"""
|
||||
if len(prices) != 5:
|
||||
return False
|
||||
|
||||
# Overall upward trend
|
||||
if prices[-1] <= prices[0]:
|
||||
return False
|
||||
|
||||
# Compute wave magnitudes
|
||||
waves = [abs(prices[i + 1] - prices[i]) for i in range(4)]
|
||||
|
||||
# Wave 3 (index 2) should not be the shortest impulse wave
|
||||
# Impulse waves are waves 0, 2, 4 (odd-indexed moves in 0-based)
|
||||
impulse_waves = [waves[0], waves[2]]
|
||||
if len(waves) > 3:
|
||||
impulse_waves.append(waves[3])
|
||||
|
||||
# Wave 3 (waves[2]) should be significant
|
||||
if waves[2] < min(waves[0], waves[2]) * 0.5:
|
||||
return False
|
||||
|
||||
# The pattern should show alternating direction
|
||||
# Check that consecutive pivots alternate in direction
|
||||
for i in range(3):
|
||||
move_a = prices[i + 1] - prices[i]
|
||||
move_b = prices[i + 2] - prices[i + 1]
|
||||
# Consecutive moves should be in opposite directions
|
||||
if move_a * move_b >= 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _validate_bearish_impulse(prices: list[float]) -> bool:
|
||||
"""Validate a 5-pivot sequence as a bearish impulse.
|
||||
|
||||
Mirror of bullish validation with inverted price direction.
|
||||
"""
|
||||
if len(prices) != 5:
|
||||
return False
|
||||
|
||||
# Overall downward trend
|
||||
if prices[-1] >= prices[0]:
|
||||
return False
|
||||
|
||||
# Compute wave magnitudes
|
||||
waves = [abs(prices[i + 1] - prices[i]) for i in range(4)]
|
||||
|
||||
# Wave 3 (waves[2]) should be significant
|
||||
if waves[2] < min(waves[0], waves[2]) * 0.5:
|
||||
return False
|
||||
|
||||
# Check alternating direction
|
||||
for i in range(3):
|
||||
move_a = prices[i + 1] - prices[i]
|
||||
move_b = prices[i + 2] - prices[i + 1]
|
||||
if move_a * move_b >= 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _compute_impulse_clarity(
|
||||
prices: list[float],
|
||||
trend_up: bool,
|
||||
price_range: float,
|
||||
) -> float:
|
||||
"""Compute wave clarity for an impulse wave.
|
||||
|
||||
Clarity is based on:
|
||||
- How well the pivots alternate (already validated).
|
||||
- How proportional the wave magnitudes are.
|
||||
- How significant the waves are relative to the price range.
|
||||
"""
|
||||
if price_range <= 0:
|
||||
return 0.0
|
||||
|
||||
waves = [abs(prices[i + 1] - prices[i]) for i in range(4)]
|
||||
total_movement = sum(waves)
|
||||
|
||||
# Significance: total wave movement relative to price range
|
||||
significance = min(1.0, total_movement / (price_range * 2.0))
|
||||
|
||||
# Proportionality: wave 3 should be the largest or close to it
|
||||
max_wave = max(waves)
|
||||
if max_wave <= 0:
|
||||
return 0.0
|
||||
|
||||
wave3_ratio = waves[2] / max_wave # 1.0 if wave 3 is the largest
|
||||
|
||||
# Overall clarity
|
||||
clarity = 0.5 * significance + 0.5 * wave3_ratio
|
||||
return max(0.0, min(1.0, clarity))
|
||||
|
||||
|
||||
def _check_corrective(
|
||||
pivots: list[dict],
|
||||
trend_up: bool,
|
||||
price_range: float,
|
||||
) -> dict | None:
|
||||
"""Check if 3 pivots form a valid corrective wave (A-B-C).
|
||||
|
||||
A corrective wave moves against the main trend:
|
||||
- For a bullish main trend: corrective wave moves down (A down, B up, C down).
|
||||
- For a bearish main trend: corrective wave moves up (A up, B down, C up).
|
||||
"""
|
||||
if len(pivots) != _CORRECTIVE_WAVE_COUNT:
|
||||
return None
|
||||
|
||||
prices = [p["price"] for p in pivots]
|
||||
|
||||
# Check alternating direction
|
||||
move_a = prices[1] - prices[0]
|
||||
move_b = prices[2] - prices[1]
|
||||
|
||||
# Moves must be in opposite directions
|
||||
if move_a * move_b >= 0:
|
||||
return None
|
||||
|
||||
# For a bullish main trend, the corrective wave should move down overall
|
||||
if trend_up:
|
||||
if prices[2] >= prices[0]:
|
||||
return None # not a downward correction
|
||||
else:
|
||||
if prices[2] <= prices[0]:
|
||||
return None # not an upward correction
|
||||
|
||||
# Compute clarity
|
||||
waves = [abs(prices[1] - prices[0]), abs(prices[2] - prices[1])]
|
||||
total_movement = sum(waves)
|
||||
|
||||
if price_range <= 0:
|
||||
return 0.0
|
||||
|
||||
significance = min(1.0, total_movement / price_range)
|
||||
clarity = significance * 0.8 # corrective waves are inherently less clear
|
||||
|
||||
# Current position is wave C (the last wave in the correction)
|
||||
return {
|
||||
"wave_type": WAVE_TYPE_CORRECTIVE,
|
||||
"current_position": 3, # wave C
|
||||
"trend_up": trend_up,
|
||||
"clarity": max(0.0, min(1.0, clarity)),
|
||||
}
|
||||
Reference in New Issue
Block a user