feat: apply #33B impulse trail + #34A skip hours 9,21 WIB

#33B Impulse Trail (position_manager.py):
- Tighten trailing SL to 1.5x ATR when candle range > 1.5x ATR
- Locks profit faster during volatile spikes (+$59, Sharpe 4.03)

#34A Time-of-Hour Filter (main_live.py):
- Skip entries at WIB hours 9 (02:00 UTC) and 21 (14:00 UTC)
- Hour 9 = end NY session (low liquidity), Hour 21 = London-NY transition (whipsaw)
- +$356 vs #31B, WR 82.6%, Sharpe 4.41, PF 2.43, DD 2.4%

Cumulative live: $3,163 net, 614 trades, 82.6% WR, Sharpe 4.41

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-08 12:55:34 +07:00
parent 214b64945d
commit cb41bfe5ba
4 changed files with 2136 additions and 2 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9
View File
@@ -932,6 +932,15 @@ class TradingBot:
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1=NEUTRAL)")
return
# 10.2 Time-of-Hour Filter (#34A: skip WIB hours 9 and 21 — backtest +$356)
# Hour 9 WIB (02:00 UTC) = end of NY session, low liquidity
# Hour 21 WIB (14:00 UTC) = London-NY transition, whipsaw prone
from zoneinfo import ZoneInfo
wib_hour = datetime.now(ZoneInfo("Asia/Jakarta")).hour
if wib_hour in (9, 21):
logger.info(f"Time Filter: {final_signal.signal_type} blocked (WIB hour {wib_hour} is skip hour)")
return
# 10.5 Check trade cooldown
if self._last_trade_time:
time_since_last = (datetime.now() - self._last_trade_time).total_seconds()
+21 -2
View File
@@ -340,9 +340,18 @@ class SmartPositionManager:
if atr_val is not None and atr_val > 0:
current_atr = atr_val
# #33B: Get last candle range for impulse detection
last_candle_range = None
if len(df_market) >= 1:
last_row = df_market.tail(1)
last_high = last_row["high"].item()
last_low = last_row["low"].item()
if last_high is not None and last_low is not None:
last_candle_range = last_high - last_low
for row in positions.iter_rows(named=True):
action = self._analyze_single_position(
row, market_analysis, current_price, current_atr
row, market_analysis, current_price, current_atr, last_candle_range
)
if action:
actions.append(action)
@@ -437,6 +446,7 @@ class SmartPositionManager:
market: Dict,
current_price: float,
current_atr: float = None,
last_candle_range: float = None,
) -> Optional[PositionAction]:
"""Analyze a single position and decide action."""
ticket = pos["ticket"]
@@ -575,8 +585,17 @@ class SmartPositionManager:
)
# 6. Trailing stop (after trail_start pips)
# #33B: Impulse detection — tighten trail when candle range > 1.5x ATR
if pip_profit >= trail_start:
trail_distance = trail_step * 0.1 # Convert to price
is_impulse = False
if last_candle_range is not None and current_atr is not None and current_atr > 0:
if last_candle_range > current_atr * 1.5:
is_impulse = True
active_trail_step = trail_step
if is_impulse:
active_trail_step = (current_atr * 1.5) / 0.1 # 1.5x ATR in pips
trail_distance = active_trail_step * 0.1 # Convert to price
if is_buy:
new_trail_sl = current_price - trail_distance