deploy: 2026-03-20 07:49
This commit is contained in:
0
strategy/__init__.py
Normal file
0
strategy/__init__.py
Normal file
278
strategy/entry_rules.py
Normal file
278
strategy/entry_rules.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""ICT entry rules.
|
||||
|
||||
Evaluates bullish and bearish entry conditions and calculates
|
||||
stop-loss / take-profit levels based on market structure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from indicators.ict_engine import ICTSignals
|
||||
from indicators.multi_timeframe import TradeDirection
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntryResult:
|
||||
"""Result of an entry rule evaluation."""
|
||||
|
||||
is_valid: bool
|
||||
direction: TradeDirection
|
||||
conditions_met: List[str] = field(default_factory=list)
|
||||
conditions_failed: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class EntryRules:
|
||||
"""ICT-based entry rule evaluation.
|
||||
|
||||
Bullish entry (LONG):
|
||||
1. HTF: Higher Highs & Higher Lows
|
||||
2. Liquidity Sweep: previous low swept then bounce
|
||||
3. Order Block: price enters bullish OB zone
|
||||
4. FVG: price returns to bullish FVG
|
||||
5. BOS: upward break of structure
|
||||
|
||||
Bearish entry (SHORT): mirror logic.
|
||||
"""
|
||||
|
||||
def check_bullish_entry(
|
||||
self, signals: ICTSignals, price: float
|
||||
) -> EntryResult:
|
||||
"""Evaluate bullish (LONG) entry conditions."""
|
||||
met: List[str] = []
|
||||
failed: List[str] = []
|
||||
|
||||
# 1. BOS bullish
|
||||
if signals.latest_bos == 1:
|
||||
met.append("BOS bullish")
|
||||
else:
|
||||
failed.append("BOS bullish")
|
||||
|
||||
# 2. Order Block -- price in bullish OB zone
|
||||
obs = signals.active_order_blocks
|
||||
ob_hit = False
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bullish_obs = obs[obs["OB"] == 1]
|
||||
for _, row in bullish_obs.iterrows():
|
||||
bottom = row.get("Bottom", 0)
|
||||
top = row.get("Top", 0)
|
||||
if pd.notna(bottom) and pd.notna(top) and bottom <= price <= top:
|
||||
ob_hit = True
|
||||
break
|
||||
if ob_hit:
|
||||
met.append("Order Block")
|
||||
else:
|
||||
failed.append("Order Block")
|
||||
|
||||
# 3. FVG -- price in bullish FVG
|
||||
fvgs = signals.active_fvg
|
||||
fvg_hit = False
|
||||
if not fvgs.empty and "FVG" in fvgs.columns:
|
||||
bullish_fvg = fvgs[fvgs["FVG"] == 1]
|
||||
for _, row in bullish_fvg.iterrows():
|
||||
bottom = row.get("Bottom", 0)
|
||||
top = row.get("Top", 0)
|
||||
if pd.notna(bottom) and pd.notna(top) and bottom <= price <= top:
|
||||
fvg_hit = True
|
||||
break
|
||||
if fvg_hit:
|
||||
met.append("FVG")
|
||||
else:
|
||||
failed.append("FVG")
|
||||
|
||||
# 4. Liquidity swept (recent bearish liquidity = trap)
|
||||
liq_swept = False
|
||||
try:
|
||||
if not signals.liquidity.empty:
|
||||
liq_col = signals.liquidity.get("Liquidity", pd.Series(dtype=float))
|
||||
recent = liq_col.dropna().tail(3)
|
||||
if len(recent) > 0 and (recent == -1).any():
|
||||
liq_swept = True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if liq_swept:
|
||||
met.append("Liquidity Sweep")
|
||||
else:
|
||||
failed.append("Liquidity Sweep")
|
||||
|
||||
# 5. CHOCH bullish (optional extra confirmation)
|
||||
if signals.latest_choch == 1:
|
||||
met.append("CHOCH bullish")
|
||||
else:
|
||||
failed.append("CHOCH bullish")
|
||||
|
||||
return EntryResult(
|
||||
is_valid=len(met) >= 3,
|
||||
direction=TradeDirection.LONG,
|
||||
conditions_met=met,
|
||||
conditions_failed=failed,
|
||||
)
|
||||
|
||||
def check_bearish_entry(
|
||||
self, signals: ICTSignals, price: float
|
||||
) -> EntryResult:
|
||||
"""Evaluate bearish (SHORT) entry conditions."""
|
||||
met: List[str] = []
|
||||
failed: List[str] = []
|
||||
|
||||
# 1. BOS bearish
|
||||
if signals.latest_bos == -1:
|
||||
met.append("BOS bearish")
|
||||
else:
|
||||
failed.append("BOS bearish")
|
||||
|
||||
# 2. Order Block -- price in bearish OB
|
||||
obs = signals.active_order_blocks
|
||||
ob_hit = False
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bearish_obs = obs[obs["OB"] == -1]
|
||||
for _, row in bearish_obs.iterrows():
|
||||
bottom = row.get("Bottom", 0)
|
||||
top = row.get("Top", 0)
|
||||
if pd.notna(bottom) and pd.notna(top) and bottom <= price <= top:
|
||||
ob_hit = True
|
||||
break
|
||||
if ob_hit:
|
||||
met.append("Order Block")
|
||||
else:
|
||||
failed.append("Order Block")
|
||||
|
||||
# 3. FVG bearish
|
||||
fvgs = signals.active_fvg
|
||||
fvg_hit = False
|
||||
if not fvgs.empty and "FVG" in fvgs.columns:
|
||||
bearish_fvg = fvgs[fvgs["FVG"] == -1]
|
||||
for _, row in bearish_fvg.iterrows():
|
||||
bottom = row.get("Bottom", 0)
|
||||
top = row.get("Top", 0)
|
||||
if pd.notna(bottom) and pd.notna(top) and bottom <= price <= top:
|
||||
fvg_hit = True
|
||||
break
|
||||
if fvg_hit:
|
||||
met.append("FVG")
|
||||
else:
|
||||
failed.append("FVG")
|
||||
|
||||
# 4. Liquidity swept (bullish liquidity = trap)
|
||||
liq_swept = False
|
||||
try:
|
||||
if not signals.liquidity.empty:
|
||||
liq_col = signals.liquidity.get("Liquidity", pd.Series(dtype=float))
|
||||
recent = liq_col.dropna().tail(3)
|
||||
if len(recent) > 0 and (recent == 1).any():
|
||||
liq_swept = True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if liq_swept:
|
||||
met.append("Liquidity Sweep")
|
||||
else:
|
||||
failed.append("Liquidity Sweep")
|
||||
|
||||
# 5. CHOCH bearish
|
||||
if signals.latest_choch == -1:
|
||||
met.append("CHOCH bearish")
|
||||
else:
|
||||
failed.append("CHOCH bearish")
|
||||
|
||||
return EntryResult(
|
||||
is_valid=len(met) >= 3,
|
||||
direction=TradeDirection.SHORT,
|
||||
conditions_met=met,
|
||||
conditions_failed=failed,
|
||||
)
|
||||
|
||||
def calculate_stop_loss(
|
||||
self,
|
||||
direction: TradeDirection,
|
||||
signals: ICTSignals,
|
||||
entry_price: float,
|
||||
) -> float:
|
||||
"""Calculate stop-loss based on OB boundary or recent swing high/low.
|
||||
|
||||
For LONG: SL below the nearest bullish OB bottom or swing low.
|
||||
For SHORT: SL above the nearest bearish OB top or swing high.
|
||||
"""
|
||||
buffer_pct = 0.002 # 0.2% buffer
|
||||
|
||||
if direction == TradeDirection.LONG:
|
||||
# Try OB bottom first
|
||||
obs = signals.active_order_blocks
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bullish_obs = obs[obs["OB"] == 1]
|
||||
if not bullish_obs.empty:
|
||||
lowest_bottom = bullish_obs["Bottom"].dropna().min()
|
||||
if pd.notna(lowest_bottom):
|
||||
return float(lowest_bottom) * (1 - buffer_pct)
|
||||
|
||||
# Fallback: recent swing low
|
||||
swing = signals.swing_highs_lows
|
||||
if "Level" in swing.columns and "HighLow" in swing.columns:
|
||||
lows = swing[swing["HighLow"] == -1]["Level"].dropna()
|
||||
if len(lows) > 0:
|
||||
return float(lows.iloc[-1]) * (1 - buffer_pct)
|
||||
|
||||
# Last resort: fixed percentage
|
||||
return entry_price * (1 - 0.02)
|
||||
|
||||
else: # SHORT
|
||||
obs = signals.active_order_blocks
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bearish_obs = obs[obs["OB"] == -1]
|
||||
if not bearish_obs.empty:
|
||||
highest_top = bearish_obs["Top"].dropna().max()
|
||||
if pd.notna(highest_top):
|
||||
return float(highest_top) * (1 + buffer_pct)
|
||||
|
||||
swing = signals.swing_highs_lows
|
||||
if "Level" in swing.columns and "HighLow" in swing.columns:
|
||||
highs = swing[swing["HighLow"] == 1]["Level"].dropna()
|
||||
if len(highs) > 0:
|
||||
return float(highs.iloc[-1]) * (1 + buffer_pct)
|
||||
|
||||
return entry_price * (1 + 0.02)
|
||||
|
||||
def calculate_take_profit(
|
||||
self,
|
||||
direction: TradeDirection,
|
||||
signals: ICTSignals,
|
||||
entry_price: float,
|
||||
stop_loss: float,
|
||||
) -> float:
|
||||
"""Calculate take-profit targeting opposite OB/FVG or 2:1 R:R minimum.
|
||||
|
||||
For LONG: TP at the nearest bearish OB/FVG above entry, or 2x risk.
|
||||
For SHORT: TP at the nearest bullish OB/FVG below entry, or 2x risk.
|
||||
"""
|
||||
risk = abs(entry_price - stop_loss)
|
||||
min_tp_distance = risk * 2 # ensure at least 2:1 R:R
|
||||
|
||||
if direction == TradeDirection.LONG:
|
||||
# Look for bearish OB above price
|
||||
obs = signals.active_order_blocks
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bearish_obs = obs[obs["OB"] == -1]
|
||||
bottom_vals = bearish_obs["Bottom"].dropna()
|
||||
above = bottom_vals[bottom_vals > entry_price]
|
||||
if len(above) > 0:
|
||||
tp = float(above.min())
|
||||
if tp - entry_price >= min_tp_distance:
|
||||
return tp
|
||||
|
||||
return entry_price + min_tp_distance
|
||||
|
||||
else: # SHORT
|
||||
obs = signals.active_order_blocks
|
||||
if not obs.empty and "OB" in obs.columns:
|
||||
bullish_obs = obs[obs["OB"] == 1]
|
||||
top_vals = bullish_obs["Top"].dropna()
|
||||
below = top_vals[top_vals < entry_price]
|
||||
if len(below) > 0:
|
||||
tp = float(below.max())
|
||||
if entry_price - tp >= min_tp_distance:
|
||||
return tp
|
||||
|
||||
return entry_price - min_tp_distance
|
||||
144
strategy/exit_rules.py
Normal file
144
strategy/exit_rules.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""ICT exit / position management rules.
|
||||
|
||||
Evaluates whether an open position should be closed based on
|
||||
TP, SL, CHOCH reversal, time expiry, or trailing stop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from config import settings
|
||||
from indicators.ict_engine import ICTSignals
|
||||
from indicators.multi_timeframe import TradeDirection
|
||||
|
||||
|
||||
class ExitReason(str, Enum):
|
||||
TAKE_PROFIT = "TP"
|
||||
STOP_LOSS = "SL"
|
||||
CHOCH = "CHOCH"
|
||||
TRAILING_STOP = "TRAILING"
|
||||
TIME_EXIT = "TIME"
|
||||
MANUAL = "MANUAL"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExitResult:
|
||||
"""Result of exit rule evaluation."""
|
||||
|
||||
should_exit: bool
|
||||
reason: Optional[ExitReason] = None
|
||||
detail: str = ""
|
||||
|
||||
|
||||
class ExitRules:
|
||||
"""Evaluate exit conditions for an open position.
|
||||
|
||||
Conditions checked (in priority order):
|
||||
1. Stop-loss hit
|
||||
2. Take-profit hit
|
||||
3. CHOCH in opposite direction
|
||||
4. Trailing stop triggered
|
||||
5. Time-based exit (too many candles without movement)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
trailing_activation_pct: float | None = None,
|
||||
trailing_distance_pct: float | None = None,
|
||||
time_exit_candles: int = 48,
|
||||
):
|
||||
self.trailing_activation_pct = (
|
||||
trailing_activation_pct
|
||||
or getattr(settings, "TRAILING_STOP_ACTIVATION_PCT", 0.01)
|
||||
)
|
||||
self.trailing_distance_pct = (
|
||||
trailing_distance_pct
|
||||
or getattr(settings, "TRAILING_STOP_DISTANCE_PCT", 0.005)
|
||||
)
|
||||
self.time_exit_candles = time_exit_candles
|
||||
|
||||
def should_exit(
|
||||
self,
|
||||
direction: TradeDirection,
|
||||
entry_price: float,
|
||||
stop_loss: float,
|
||||
take_profit: float,
|
||||
current_price: float,
|
||||
signals: ICTSignals,
|
||||
opened_at: datetime | None = None,
|
||||
candles_since_entry: int = 0,
|
||||
trailing_stop: float | None = None,
|
||||
) -> ExitResult:
|
||||
"""Evaluate all exit conditions against current market state."""
|
||||
|
||||
# 1. Stop-Loss
|
||||
if direction == TradeDirection.LONG and current_price <= stop_loss:
|
||||
return ExitResult(True, ExitReason.STOP_LOSS, f"Price {current_price} <= SL {stop_loss}")
|
||||
if direction == TradeDirection.SHORT and current_price >= stop_loss:
|
||||
return ExitResult(True, ExitReason.STOP_LOSS, f"Price {current_price} >= SL {stop_loss}")
|
||||
|
||||
# 2. Take-Profit
|
||||
if direction == TradeDirection.LONG and current_price >= take_profit:
|
||||
return ExitResult(True, ExitReason.TAKE_PROFIT, f"Price {current_price} >= TP {take_profit}")
|
||||
if direction == TradeDirection.SHORT and current_price <= take_profit:
|
||||
return ExitResult(True, ExitReason.TAKE_PROFIT, f"Price {current_price} <= TP {take_profit}")
|
||||
|
||||
# 3. CHOCH in opposite direction
|
||||
choch = signals.latest_choch
|
||||
if choch is not None:
|
||||
if direction == TradeDirection.LONG and choch == -1:
|
||||
return ExitResult(True, ExitReason.CHOCH, "Bearish CHOCH while LONG")
|
||||
if direction == TradeDirection.SHORT and choch == 1:
|
||||
return ExitResult(True, ExitReason.CHOCH, "Bullish CHOCH while SHORT")
|
||||
|
||||
# 4. Trailing stop
|
||||
if trailing_stop is not None:
|
||||
if direction == TradeDirection.LONG and current_price <= trailing_stop:
|
||||
return ExitResult(True, ExitReason.TRAILING_STOP, f"Trailing hit {trailing_stop}")
|
||||
if direction == TradeDirection.SHORT and current_price >= trailing_stop:
|
||||
return ExitResult(True, ExitReason.TRAILING_STOP, f"Trailing hit {trailing_stop}")
|
||||
|
||||
# 5. Time-based exit
|
||||
if candles_since_entry >= self.time_exit_candles:
|
||||
return ExitResult(True, ExitReason.TIME_EXIT, f"Exceeded {self.time_exit_candles} candles")
|
||||
|
||||
return ExitResult(False)
|
||||
|
||||
def update_trailing_stop(
|
||||
self,
|
||||
direction: TradeDirection,
|
||||
entry_price: float,
|
||||
current_price: float,
|
||||
current_trailing: float | None,
|
||||
) -> Optional[float]:
|
||||
"""Update trailing stop if price has moved enough into profit.
|
||||
|
||||
Returns the new trailing stop value, or None if not yet activated.
|
||||
"""
|
||||
if direction == TradeDirection.LONG:
|
||||
pnl_pct = (current_price - entry_price) / entry_price
|
||||
if pnl_pct < self.trailing_activation_pct:
|
||||
return current_trailing # not yet in profit enough
|
||||
|
||||
new_trail = current_price * (1 - self.trailing_distance_pct)
|
||||
if current_trailing is None or new_trail > current_trailing:
|
||||
logger.debug("Trailing stop updated: {} -> {}", current_trailing, new_trail)
|
||||
return new_trail
|
||||
return current_trailing
|
||||
|
||||
else: # SHORT
|
||||
pnl_pct = (entry_price - current_price) / entry_price
|
||||
if pnl_pct < self.trailing_activation_pct:
|
||||
return current_trailing
|
||||
|
||||
new_trail = current_price * (1 + self.trailing_distance_pct)
|
||||
if current_trailing is None or new_trail < current_trailing:
|
||||
logger.debug("Trailing stop updated: {} -> {}", current_trailing, new_trail)
|
||||
return new_trail
|
||||
return current_trailing
|
||||
168
strategy/signal_generator.py
Normal file
168
strategy/signal_generator.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""Trade signal generator.
|
||||
|
||||
Orchestrates MTF analysis, confluence checking, and entry/exit
|
||||
rule evaluation to produce actionable TradeSignal objects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from config import settings
|
||||
from core.data_feed import DataFeed
|
||||
from indicators.ict_engine import ICTEngine
|
||||
from indicators.multi_timeframe import (
|
||||
MarketBias,
|
||||
MultiTimeframeAnalyzer,
|
||||
TradeDirection,
|
||||
)
|
||||
from indicators.confluence import ConfluenceChecker, ConfluenceResult
|
||||
from strategy.entry_rules import EntryRules
|
||||
from strategy.exit_rules import ExitRules
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradeSignal:
|
||||
"""Actionable trade signal produced by the strategy engine."""
|
||||
|
||||
symbol: str
|
||||
direction: TradeDirection
|
||||
entry_price: float
|
||||
stop_loss: float
|
||||
take_profit: float
|
||||
confidence: int # confluence score (3-6)
|
||||
timeframe: str
|
||||
timestamp: datetime = field(default_factory=datetime.utcnow)
|
||||
reasons: List[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def risk_reward_ratio(self) -> float:
|
||||
"""Calculate the risk/reward ratio."""
|
||||
risk = abs(self.entry_price - self.stop_loss)
|
||||
reward = abs(self.take_profit - self.entry_price)
|
||||
return reward / risk if risk > 0 else 0.0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"symbol": self.symbol,
|
||||
"direction": self.direction.value,
|
||||
"entry_price": self.entry_price,
|
||||
"stop_loss": self.stop_loss,
|
||||
"take_profit": self.take_profit,
|
||||
"confidence": self.confidence,
|
||||
"risk_reward": round(self.risk_reward_ratio, 2),
|
||||
"timeframe": self.timeframe,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"reasons": self.reasons,
|
||||
}
|
||||
|
||||
|
||||
class SignalGenerator:
|
||||
"""Generate trade signals by combining ICT analysis across timeframes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ict_engine: ICTEngine,
|
||||
mtf_analyzer: MultiTimeframeAnalyzer,
|
||||
confluence_checker: ConfluenceChecker,
|
||||
entry_rules: EntryRules | None = None,
|
||||
exit_rules: ExitRules | None = None,
|
||||
):
|
||||
self.engine = ict_engine
|
||||
self.mtf = mtf_analyzer
|
||||
self.confluence = confluence_checker
|
||||
self.entry_rules = entry_rules or EntryRules()
|
||||
self.exit_rules = exit_rules or ExitRules()
|
||||
|
||||
async def generate(
|
||||
self, symbol: str, data_feed: DataFeed
|
||||
) -> Optional[TradeSignal]:
|
||||
"""Run the full signal generation pipeline for a symbol.
|
||||
|
||||
Steps:
|
||||
1. Multi-timeframe ICT analysis
|
||||
2. Confluence check (>= MIN_CONFLUENCE)
|
||||
3. Entry rule validation
|
||||
4. Build TradeSignal
|
||||
|
||||
Returns:
|
||||
TradeSignal if conditions met, None otherwise.
|
||||
"""
|
||||
# 1. MTF analysis
|
||||
mtf_result = await self.mtf.analyze_all(data_feed, symbol)
|
||||
if mtf_result.htf_bias == MarketBias.NEUTRAL:
|
||||
logger.debug("{}: HTF bias NEUTRAL -- no signal", symbol)
|
||||
return None
|
||||
|
||||
# 2. Get per-timeframe signals for confluence detail
|
||||
tfs = self.mtf.TIMEFRAMES
|
||||
htf_df = data_feed.get_dataframe(symbol, tfs["htf"])
|
||||
mtf_df = data_feed.get_dataframe(symbol, tfs["mtf"])
|
||||
ltf_df = data_feed.get_dataframe(symbol, tfs["ltf"])
|
||||
|
||||
htf_signals = self.engine.analyze(htf_df)
|
||||
mtf_signals = self.engine.analyze(mtf_df)
|
||||
ltf_signals = self.engine.analyze(ltf_df)
|
||||
|
||||
current_price = float(ltf_df["close"].iloc[-1])
|
||||
|
||||
# 3. Confluence check
|
||||
conf = self.confluence.check(
|
||||
mtf_result,
|
||||
current_price,
|
||||
htf_signals=htf_signals,
|
||||
mtf_signals=mtf_signals,
|
||||
ltf_signals=ltf_signals,
|
||||
)
|
||||
|
||||
if not conf.is_valid:
|
||||
logger.debug(
|
||||
"{}: Confluence {} < {} -- no signal",
|
||||
symbol, conf.score, self.confluence.MIN_CONFLUENCE,
|
||||
)
|
||||
return None
|
||||
|
||||
# 4. Entry rules
|
||||
if conf.direction == TradeDirection.LONG:
|
||||
entry_result = self.entry_rules.check_bullish_entry(
|
||||
ltf_signals, current_price
|
||||
)
|
||||
else:
|
||||
entry_result = self.entry_rules.check_bearish_entry(
|
||||
ltf_signals, current_price
|
||||
)
|
||||
|
||||
sl = self.entry_rules.calculate_stop_loss(conf.direction, ltf_signals, current_price)
|
||||
tp = self.entry_rules.calculate_take_profit(
|
||||
conf.direction, ltf_signals, current_price, sl
|
||||
)
|
||||
|
||||
# Build reasons from met conditions
|
||||
reasons = [c.name for c in conf.conditions if c.met]
|
||||
|
||||
signal = TradeSignal(
|
||||
symbol=symbol,
|
||||
direction=conf.direction,
|
||||
entry_price=current_price,
|
||||
stop_loss=sl,
|
||||
take_profit=tp,
|
||||
confidence=conf.score,
|
||||
timeframe=settings.LTF_TIMEFRAME,
|
||||
reasons=reasons,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"SIGNAL: {} {} @ {} | SL={} TP={} | RR={:.2f} | conf={}",
|
||||
signal.direction.value,
|
||||
symbol,
|
||||
current_price,
|
||||
sl,
|
||||
tp,
|
||||
signal.risk_reward_ratio,
|
||||
conf.score,
|
||||
)
|
||||
return signal
|
||||
Reference in New Issue
Block a user