Files
xau-ai-trading-bot/src/session_filter.py
T
buckybonez a8d01995ab feat: apply #24B optimizations — ATR-adaptive exit, skip Tokyo-London, relaxed early cut
Backtest #24B results: 739 trades, 80.4% WR, $2,235 PnL, 3.4% DD, Sharpe 2.87, PF 1.77 (+$785 vs baseline)

Three proven improvements:
- Skip Tokyo-London overlap session (15:00-16:00 WIB) — backtest +$345
- Relax early cut momentum threshold from -30 to -50 — backtest +$125
- ATR-adaptive breakeven/trail (BE=2.0x ATR, trail_start=4.0x ATR, trail_step=3.0x ATR) — backtest +$373

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 22:34:24 +07:00

334 lines
12 KiB
Python

"""
Trading Session Filter
======================
Filter trades based on market sessions and optimal trading hours.
Timezone: WIB (Waktu Indonesia Barat) - GMT+7 for Batam/Jakarta.
Optimal Trading Hours for XAUUSD:
- London-NY Overlap: 20:00 - 00:00 WIB (BEST)
- London Session: 15:00 - 00:00 WIB
- NY Session: 20:00 - 05:00 WIB
Dangerous Zones:
- Rollover/Spread Wide: 04:00 - 06:00 WIB
- Low Liquidity: 00:00 - 04:00 WIB
- Friday Close: After 23:00 WIB Friday
"""
from datetime import datetime, time, timedelta
from typing import Tuple, Dict, Optional
from dataclasses import dataclass
from enum import Enum
from loguru import logger
import pytz
class TradingSession(Enum):
"""Market trading sessions."""
SYDNEY = "sydney"
TOKYO = "tokyo"
LONDON = "london"
NEW_YORK = "new_york"
OVERLAP_TOKYO_LONDON = "tokyo_london_overlap"
OVERLAP_LONDON_NY = "london_ny_overlap"
OFF_HOURS = "off_hours"
@dataclass
class SessionConfig:
"""Session trading configuration."""
name: str
start_hour: int # WIB
start_minute: int
end_hour: int # WIB
end_minute: int
volatility: str # "low", "medium", "high", "extreme"
allow_trading: bool
position_size_multiplier: float
class SessionFilter:
"""
Trading session filter for optimal trading hours.
Configured for XAUUSD aggressive trading during London/NY overlap.
All times in WIB (GMT+7).
"""
def __init__(
self,
timezone: str = "Asia/Jakarta", # WIB
aggressive_mode: bool = True, # Focus on high volatility
):
self.tz = pytz.timezone(timezone)
self.aggressive_mode = aggressive_mode
# Define trading windows (WIB)
self.sessions = {
# Main sessions
TradingSession.SYDNEY: SessionConfig(
name="Sydney",
start_hour=6, start_minute=0, # Start after rollover (skip 04:00-06:00)
end_hour=13, end_minute=0,
volatility="low",
allow_trading=True, # ENABLED - backtest shows $5,934 profit!
position_size_multiplier=0.5, # HALF lot size for safety
),
TradingSession.TOKYO: SessionConfig(
name="Tokyo",
start_hour=7, start_minute=0,
end_hour=16, end_minute=0,
volatility="medium",
allow_trading=True,
position_size_multiplier=0.7,
),
TradingSession.LONDON: SessionConfig(
name="London",
start_hour=15, start_minute=0,
end_hour=23, end_minute=59,
volatility="high",
allow_trading=True,
position_size_multiplier=1.0,
),
TradingSession.NEW_YORK: SessionConfig(
name="New York",
start_hour=20, start_minute=0,
end_hour=23, end_minute=59, # NY continues past midnight
volatility="extreme",
allow_trading=True,
position_size_multiplier=1.0,
),
# Overlap sessions (BEST TIMES)
TradingSession.OVERLAP_TOKYO_LONDON: SessionConfig(
name="Tokyo-London Overlap",
start_hour=15, start_minute=0,
end_hour=16, end_minute=0,
volatility="high",
allow_trading=False, # #24B: Skip Tokyo-London overlap (backtest +$345)
position_size_multiplier=0.0,
),
TradingSession.OVERLAP_LONDON_NY: SessionConfig(
name="London-NY Overlap (GOLDEN)",
start_hour=20, start_minute=0,
end_hour=23, end_minute=59,
volatility="extreme",
allow_trading=True,
position_size_multiplier=1.2, # Boost during golden hours
),
}
# Danger zones (WIB)
self.danger_zones = [
# Rollover - spread extremely wide
{"name": "Rollover", "start": (4, 0), "end": (6, 0), "reason": "Spread melebar saat rollover"},
# Low liquidity
{"name": "Dead Zone", "start": (0, 0), "end": (4, 0), "reason": "Likuiditas rendah, spread tinggi"},
]
# High impact news times to avoid (typical release times in WIB)
self.news_blackout_times = [
# NFP - First Friday of month
{"event": "NFP", "hour": 19, "minute": 30, "buffer_before": 15, "buffer_after": 30},
# Fed Interest Rate
{"event": "FOMC", "hour": 1, "minute": 0, "buffer_before": 15, "buffer_after": 45},
# US CPI
{"event": "CPI", "hour": 19, "minute": 30, "buffer_before": 15, "buffer_after": 30},
]
def get_current_time_wib(self) -> datetime:
"""Get current time in WIB."""
return datetime.now(self.tz)
def get_current_session(self) -> Tuple[TradingSession, SessionConfig]:
"""
Get the current trading session.
Returns highest priority session if multiple overlap.
Priority: Overlap > London/NY > Tokyo > Sydney > Off Hours
"""
now = self.get_current_time_wib()
hour = now.hour
minute = now.minute
current_time = hour * 60 + minute
# Check overlaps first (highest priority)
if 20 * 60 <= current_time <= 24 * 60: # 20:00 - 00:00
return TradingSession.OVERLAP_LONDON_NY, self.sessions[TradingSession.OVERLAP_LONDON_NY]
if 15 * 60 <= current_time <= 16 * 60: # 15:00 - 16:00
return TradingSession.OVERLAP_TOKYO_LONDON, self.sessions[TradingSession.OVERLAP_TOKYO_LONDON]
# Check main sessions
for session, config in self.sessions.items():
if session in [TradingSession.OVERLAP_LONDON_NY, TradingSession.OVERLAP_TOKYO_LONDON]:
continue
start = config.start_hour * 60 + config.start_minute
end = config.end_hour * 60 + config.end_minute
if start <= current_time <= end:
return session, config
# Off hours
return TradingSession.OFF_HOURS, SessionConfig(
name="Off Hours",
start_hour=0, start_minute=0,
end_hour=0, end_minute=0,
volatility="low",
allow_trading=False,
position_size_multiplier=0.0,
)
def is_danger_zone(self) -> Tuple[bool, str]:
"""Check if current time is in a danger zone."""
now = self.get_current_time_wib()
hour = now.hour
minute = now.minute
current_time = hour * 60 + minute
for zone in self.danger_zones:
start = zone["start"][0] * 60 + zone["start"][1]
end = zone["end"][0] * 60 + zone["end"][1]
if start <= current_time < end:
return True, zone["reason"]
return False, ""
def is_friday_close(self) -> bool:
"""Check if approaching Friday market close (Saturday 05:00 WIB)."""
now = self.get_current_time_wib()
# Market closes Saturday 05:00 WIB — only block 30 min before
# Saturday 04:30+ WIB
if now.weekday() == 5 and now.hour == 4 and now.minute >= 30:
return True
return False
def is_weekend(self) -> bool:
"""Check if market is closed (weekend)."""
now = self.get_current_time_wib()
weekday = now.weekday()
# Saturday full day
if weekday == 5:
return True
# Sunday until 04:00 WIB Monday
if weekday == 6:
return True
# Saturday early morning (before market close at 05:00)
if weekday == 5 and now.hour < 5:
return False # Market still open
return False
def can_trade(self) -> Tuple[bool, str, float]:
"""
Check if trading is allowed right now.
Returns:
Tuple of (can_trade, reason, position_multiplier)
"""
now = self.get_current_time_wib()
# Check weekend
if self.is_weekend():
return False, "Market tutup (weekend)", 0.0
# Check Friday close
if self.is_friday_close():
return False, "Mendekati penutupan Jumat - hindari gap weekend", 0.0
# Check danger zones
is_danger, danger_reason = self.is_danger_zone()
if is_danger:
return False, f"Zona bahaya: {danger_reason}", 0.0
# Get current session
session, config = self.get_current_session()
if not config.allow_trading:
return False, f"Trading tidak diizinkan saat {config.name}", 0.0
# In aggressive mode, allow medium+ volatility + Sydney (proven profitable)
if self.aggressive_mode:
# Sydney session is ALLOWED - backtest shows 62% WR, $5,934 profit
if session == TradingSession.SYDNEY:
return True, f"Trading OK - {config.name} (SAFE MODE: 0.5x lot)", config.position_size_multiplier
# Only block low volatility sessions
if config.volatility not in ["medium", "high", "extreme"]:
return False, f"Mode agresif: tunggu sesi {config.name} (volatilitas {config.volatility})", config.position_size_multiplier
return True, f"Trading OK - {config.name} ({config.volatility} volatility)", config.position_size_multiplier
def get_next_trading_window(self) -> Dict:
"""Get when the next optimal trading window starts."""
now = self.get_current_time_wib()
current_hour = now.hour
# Find next London-NY overlap
if current_hour < 20:
# Today at 20:00
next_window = now.replace(hour=20, minute=0, second=0, microsecond=0)
hours_until = 20 - current_hour
else:
# Tomorrow at 20:00
next_window = (now + timedelta(days=1)).replace(hour=20, minute=0, second=0, microsecond=0)
hours_until = 24 - current_hour + 20
return {
"next_window": next_window.strftime("%Y-%m-%d %H:%M WIB"),
"hours_until": hours_until,
"session": "London-NY Overlap",
"is_weekend": self.is_weekend(),
}
def get_status_report(self) -> Dict:
"""Get comprehensive trading session status."""
now = self.get_current_time_wib()
session, config = self.get_current_session()
can_trade, reason, multiplier = self.can_trade()
is_danger, danger_reason = self.is_danger_zone()
return {
"current_time": now.strftime("%Y-%m-%d %H:%M:%S WIB"),
"day_of_week": now.strftime("%A"),
"current_session": config.name,
"volatility": config.volatility,
"can_trade": can_trade,
"reason": reason,
"position_multiplier": multiplier,
"is_danger_zone": is_danger,
"danger_reason": danger_reason,
"is_friday_close": self.is_friday_close(),
"is_weekend": self.is_weekend(),
"next_window": self.get_next_trading_window(),
}
# Convenience function
def create_wib_session_filter(aggressive: bool = True) -> SessionFilter:
"""Create session filter for WIB timezone."""
return SessionFilter(
timezone="Asia/Jakarta",
aggressive_mode=aggressive,
)
if __name__ == "__main__":
# Test session filter
sf = create_wib_session_filter(aggressive=True)
print("\n" + "=" * 60)
print("TRADING SESSION STATUS")
print("=" * 60)
status = sf.get_status_report()
for key, value in status.items():
print(f"{key}: {value}")
print("\n" + "=" * 60)
can_trade, reason, multiplier = sf.can_trade()
print(f"Can Trade: {can_trade}")
print(f"Reason: {reason}")
print(f"Position Multiplier: {multiplier}")