Files
ForexBot2/main.py
T
Harwinder 636f0304ae Refocus on USD/JPY and add walk-forward backtester with regime filter
- Strip multi-pair plumbing; drop synthetic-data fallback
- Add BacktestConfig + walk-forward replay through the live signal pipeline
- Add metrics module (PF, Sharpe, Sortino, max DD, trade journal)
- Add parallel grid optimizer with disk parquet cache
- Add H4 ADX regime gate (--regime-filter, --regime-min-adx) — cuts chop
  losses ~84% in 2023 H1 while preserving trend edge

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:15:41 -04:00

458 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
ForexBot2 — USD/JPY Signal Intelligence (single-pair)
=======================================================
Entry point for both live signal analysis and walk-forward backtesting.
Usage:
python main.py # Live signal on H1
python main.py --timeframe H4 --detail # Detailed H4 signal
python main.py --json # JSON output
python main.py --backtest # Walk-forward backtest
python main.py --backtest --optimize # Parallel grid search
"""
import argparse
import json
import logging
import sys
from datetime import datetime, timezone
from typing import Dict, List
# Force UTF-8 output on Windows so box-drawing chars render
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8")
from config import PlatformConfig, Timeframe, SignalStrength, USDJPY, DISPLAY
from signal_aggregator import SignalAggregator, SignalReport
# ══════════════════════════════════════════════════════════════════
# TERMINAL FORMATTING
# ══════════════════════════════════════════════════════════════════
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
UNDERLINE = "\033[4m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
CYAN = "\033[96m"
WHITE = "\033[97m"
GRAY = "\033[90m"
def signal_color(signal: SignalStrength) -> str:
return {
SignalStrength.STRONG_BUY: Colors.GREEN + Colors.BOLD,
SignalStrength.BUY: Colors.GREEN,
SignalStrength.NEUTRAL: Colors.GRAY,
SignalStrength.SELL: Colors.RED,
SignalStrength.STRONG_SELL: Colors.RED + Colors.BOLD,
}.get(signal, Colors.RESET)
def quality_color(q: str) -> str:
return {
"A+": Colors.GREEN + Colors.BOLD,
"A": Colors.GREEN,
"B": Colors.CYAN,
"C": Colors.YELLOW,
"D": Colors.GRAY,
"F": Colors.DIM,
}.get(q, Colors.RESET)
def print_header(title: str = "USD/JPY SIGNAL INTELLIGENCE"):
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
print(f"\n{Colors.CYAN}{'═' * 80}{Colors.RESET}")
print(f"{Colors.BOLD}{Colors.CYAN} {title}{Colors.RESET}")
print(f"{Colors.GRAY} Time: {now}{Colors.RESET}")
print(f"{Colors.CYAN}{'═' * 80}{Colors.RESET}\n")
# ── Live signal renderer ───────────────────────────────────────────
def print_signal_report(report: SignalReport, detail: bool = True):
sc = signal_color(report.final_signal)
qc = quality_color(report.signal_quality)
sig_name = report.final_signal.name.replace("_", " ")
print(f" {Colors.BOLD}{Colors.WHITE}{report.display_name}{Colors.RESET}"
f" │ Price: {Colors.BOLD}{report.current_price:.3f}{Colors.RESET}"
f" │ Regime: {Colors.CYAN}{report.market_regime}{Colors.RESET}"
f" │ Vol: {report.volatility_regime}")
print(f"\n {Colors.BOLD}SIGNAL:{Colors.RESET} {sc}{sig_name}{Colors.RESET}"
f" │ Score: {sc}{report.final_score:+.1f}{Colors.RESET}"
f" │ Grade: {qc}{report.signal_quality}{Colors.RESET}"
f" │ Confidence: {report.final_confidence:.0f}%")
print(f" {Colors.BOLD}Action:{Colors.RESET} {report.action_recommendation}")
if report.mtf_signals:
mtf_status = (f" {Colors.GREEN}✓ ALIGNED{Colors.RESET}"
if report.mtf_agreement
else f" {Colors.YELLOW}✗ DIVERGENT{Colors.RESET}")
print(f"\n {Colors.BOLD}Multi-Timeframe:{Colors.RESET}{mtf_status}")
for tf, sig in report.mtf_signals.items():
tf_sc = signal_color(sig)
tf_score = report.mtf_scores.get(tf, 0)
print(f" {tf:>5}: {tf_sc}{sig.name.replace('_',' '):<12}{Colors.RESET}"
f" ({tf_score:+.1f})")
if detail and report.primary_analysis:
print(f"\n {Colors.BOLD}Indicators ({report.primary_analysis.timeframe}):{Colors.RESET}")
for name, ind in report.primary_analysis.indicators.items():
if name.startswith("_") or ind.weight == 0:
continue
isc = signal_color(ind.signal)
print(f" {ind.name:<18} {isc}{ind.signal.name.replace('_',' '):<12}{Colors.RESET}"
f" ({ind.confidence:.0f}%) │ {Colors.GRAY}{ind.description}{Colors.RESET}")
if report.bullish_reasons:
print(f"\n {Colors.GREEN}▲ Bullish Factors:{Colors.RESET}")
for r in report.bullish_reasons[:5]:
print(f" {Colors.GREEN}+{Colors.RESET} {r}")
if report.bearish_reasons:
print(f"\n {Colors.RED}▼ Bearish Factors:{Colors.RESET}")
for r in report.bearish_reasons[:5]:
print(f" {Colors.RED}-{Colors.RESET} {r}")
if report.warnings:
print(f"\n {Colors.YELLOW}⚠ Warnings:{Colors.RESET}")
for w in report.warnings:
print(f" {Colors.YELLOW}!{Colors.RESET} {w}")
if report.news_description and report.news_description != "Neutral news sentiment":
print(f"\n {Colors.MAGENTA}📰 News:{Colors.RESET} {report.news_description}")
if report.event_description and report.event_risk != "clear":
print(f" {Colors.YELLOW}📅 Event:{Colors.RESET} {report.event_description}")
if report.trade_setup:
ts = report.trade_setup
dir_color = Colors.GREEN if ts.direction == "BUY" else Colors.RED
print(f"\n {Colors.BOLD}Trade Setup:{Colors.RESET}")
print(f" Direction: {dir_color}{ts.direction}{Colors.RESET}")
print(f" Entry: {ts.entry_price:.3f}")
print(f" Stop Loss: {Colors.RED}{ts.stop_loss:.3f}{Colors.RESET} ({ts.risk_pips:.1f} pips)")
print(f" Take Profit 1: {Colors.GREEN}{ts.take_profit_1:.3f}{Colors.RESET} ({ts.reward_pips:.1f} pips)")
print(f" Take Profit 2: {Colors.GREEN}{ts.take_profit_2:.3f}{Colors.RESET}")
print(f" Risk:Reward: 1:{ts.risk_reward:.1f}")
if report.primary_analysis and report.primary_analysis.key_levels:
levels = report.primary_analysis.key_levels
print(f"\n {Colors.BOLD}Key Levels:{Colors.RESET}")
for lvl in ["r3", "r2", "r1", "pivot", "s1", "s2", "s3"]:
if lvl in levels:
val = levels[lvl]
color = Colors.RED if val > report.current_price else Colors.GREEN
print(f" {lvl.upper():>6}: {color}{val:.3f}{Colors.RESET}")
def report_to_dict(report: SignalReport) -> dict:
d = {
"symbol": report.symbol,
"display_name": report.display_name,
"timestamp": report.timestamp,
"current_price": report.current_price,
"signal": report.final_signal.name,
"score": round(report.final_score, 2),
"confidence": round(report.final_confidence, 2),
"quality": report.signal_quality,
"action": report.action_recommendation,
"market_regime": report.market_regime,
"volatility_regime": report.volatility_regime,
"mtf_agreement": report.mtf_agreement,
"mtf_signals": {k: v.name for k, v in report.mtf_signals.items()},
"event_risk": report.event_risk,
"event_description": report.event_description,
"news_sentiment": round(report.news_sentiment, 3),
"bullish_reasons": report.bullish_reasons,
"bearish_reasons": report.bearish_reasons,
"warnings": report.warnings,
}
if report.trade_setup:
ts = report.trade_setup
d["trade_setup"] = {
"direction": ts.direction,
"entry": ts.entry_price,
"stop_loss": ts.stop_loss,
"take_profit_1": ts.take_profit_1,
"take_profit_2": ts.take_profit_2,
"risk_reward": ts.risk_reward,
"risk_pips": round(ts.risk_pips, 1),
"reward_pips": round(ts.reward_pips, 1),
}
if report.primary_analysis:
d["indicators"] = {
name: {
"signal": ind.signal.name,
"confidence": round(ind.confidence, 1),
"description": ind.description,
}
for name, ind in report.primary_analysis.indicators.items()
if not name.startswith("_")
}
d["key_levels"] = {
k: round(v, 5) for k, v in report.primary_analysis.key_levels.items()
}
return d
# ── Backtest renderer ──────────────────────────────────────────────
def print_backtest_report(result, params: dict = None, verbose_journal: bool = False):
m = result.metrics
print(f" {Colors.BOLD}{Colors.WHITE}Backtest Summary — USD/JPY{Colors.RESET}")
if params:
params_str = ", ".join(f"{k}={v}" for k, v in params.items())
print(f" {Colors.GRAY}Params: {params_str}{Colors.RESET}")
print(f" {Colors.GRAY}{'─' * 76}{Colors.RESET}")
initial = result.initial_balance
final = result.final_balance
ret_pct = m.get("total_return_pct", 0)
ret_color = Colors.GREEN if ret_pct >= 0 else Colors.RED
print(f" Trades: {Colors.BOLD}{m.get('total_trades', 0)}{Colors.RESET}"
f" ({m.get('wins', 0)}W / {m.get('losses', 0)}L)")
print(f" Win Rate: {m.get('win_rate', 0):.1f}%")
print(f" Profit Factor: {m.get('profit_factor', 0):.2f}")
pf_inf = m.get('profit_factor', 0) == float('inf')
if pf_inf:
print(f" (no losing trades — infinite PF)")
print(f" Expectancy: {m.get('expectancy_pips', 0):+.2f} pips/trade"
f" ({m.get('expectancy_per_trade', 0):+.2f} per trade)")
print(f" Initial Balance: ${initial:,.2f}")
print(f" Final Balance: {ret_color}${final:,.2f}{Colors.RESET}")
print(f" Total Return: {ret_color}{ret_pct:+.2f}%{Colors.RESET}")
print(f" CAGR: {m.get('cagr', 0):+.2f}%")
print(f" Max Drawdown: {Colors.RED}{m.get('max_drawdown_pct', 0):.2f}%{Colors.RESET}")
print(f" Sharpe Ratio: {m.get('sharpe', 0):.2f}")
print(f" Sortino Ratio: {m.get('sortino', 0):.2f}")
print(f" Avg Duration: {m.get('avg_trade_duration_bars', 0):.1f} bars")
print(f" Win Streak: {m.get('longest_winning_streak', 0)} | "
f"Loss Streak: {m.get('longest_losing_streak', 0)}")
print(f" {Colors.GRAY}{'─' * 76}{Colors.RESET}")
if verbose_journal and result.trades:
print(f"\n {Colors.BOLD}Trade Journal (last 10):{Colors.RESET}")
print(f" {'#':<4} {'Entry Time':<22} {'Dir':<5} {'Entry':<9} "
f"{'Exit':<9} {'Reason':<6} {'Pips':>8} {'P&L%':>7}")
for i, t in enumerate(result.trades[-10:], start=max(1, len(result.trades) - 9)):
color = Colors.GREEN if t.pnl_currency > 0 else Colors.RED
entry_str = str(t.entry_time)[:19]
print(f" {i:<4} {entry_str:<22} {t.direction:<5} {t.entry_price:<9.3f} "
f"{t.exit_price:<9.3f} {t.exit_reason:<6} "
f"{color}{t.pnl_pips:>+8.1f}{Colors.RESET} "
f"{color}{t.pnl_pct:>+6.2f}%{Colors.RESET}")
# ══════════════════════════════════════════════════════════════════
# CLI
# ══════════════════════════════════════════════════════════════════
def parse_args():
parser = argparse.ArgumentParser(
description="ForexBot2 — USD/JPY Signal Intelligence + Backtester",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python main.py Live signal (H1, summary)
python main.py --detail Live signal with full breakdown
python main.py --timeframe H4 --detail H4 timeframe
python main.py --json JSON output
python main.py --backtest Walk-forward backtest (full history)
python main.py --backtest --bars 2000 Backtest on last 2000 bars
python main.py --backtest --optimize Parallel grid search across CPU cores
""",
)
parser.add_argument("--timeframe", "-tf", default="H1",
choices=["M5", "M15", "M30", "H1", "H4", "D1"],
help="Primary analysis timeframe (default: H1)")
parser.add_argument("--detail", action="store_true",
help="Show full indicator breakdown (live mode)")
parser.add_argument("--json", action="store_true",
help="Output as JSON")
parser.add_argument("--verbose", "-v", action="store_true",
help="Debug logging")
# Backtest options
parser.add_argument("--backtest", action="store_true",
help="Run walk-forward backtest instead of live signal")
parser.add_argument("--optimize", action="store_true",
help="Run parallel parameter grid search (implies --backtest)")
parser.add_argument("--bars", type=int, default=5000,
help="Max bars of history (default 5000 — TD ceiling)")
parser.add_argument("--start", default=None,
help="Backtest window start (YYYY-MM-DD)")
parser.add_argument("--end", default=None,
help="Backtest window end (YYYY-MM-DD)")
parser.add_argument("--min-grade", default="B",
choices=["A+", "A", "B", "C", "D", "F"],
help="Minimum grade to open backtest trade (default B)")
parser.add_argument("--journal", action="store_true",
help="Print last 10 trades from backtest journal")
parser.add_argument("--journal-csv", default=None,
help="Write full trade journal to CSV path")
parser.add_argument("--regime-filter", action="store_true",
help="Gate signals on higher-TF ADX trend confirmation")
parser.add_argument("--regime-min-adx", type=float, default=30.0,
help="Min ADX for regime filter (default 30, requires --regime-filter)")
return parser.parse_args()
def build_config(args) -> PlatformConfig:
config = PlatformConfig()
tf_map = {
"M5": Timeframe.M5, "M15": Timeframe.M15, "M30": Timeframe.M30,
"H1": Timeframe.H1, "H4": Timeframe.H4, "D1": Timeframe.D1,
}
config.primary_timeframe = tf_map[args.timeframe]
tf_confirmations = {
"M5": [Timeframe.M1, Timeframe.M15],
"M15": [Timeframe.M5, Timeframe.H1],
"M30": [Timeframe.M15, Timeframe.H1],
"H1": [Timeframe.M15, Timeframe.H4],
"H4": [Timeframe.H1, Timeframe.D1],
"D1": [Timeframe.H4, Timeframe.W1],
}
config.confirmation_timeframes = tf_confirmations.get(
args.timeframe, [Timeframe.M15, Timeframe.H4]
)
# Backtest config
config.backtest.start_date = args.start
config.backtest.end_date = args.end
config.backtest.max_bars = args.bars
config.backtest.min_trade_grade = args.min_grade
config.backtest.optimize = args.optimize
config.backtest.regime_filter = args.regime_filter
config.backtest.regime_filter_min_adx = args.regime_min_adx
return config
def run_live(args, config: PlatformConfig):
print(f"{Colors.GRAY} Fetching market data and running analysis...{Colors.RESET}\n")
aggregator = SignalAggregator(config)
report = aggregator.analyze_pair()
if args.json:
print(json.dumps(report_to_dict(report), indent=2))
return
print_header("USD/JPY SIGNAL INTELLIGENCE")
print_signal_report(report, detail=args.detail)
print(f"\n{Colors.CYAN}{'═' * 80}{Colors.RESET}")
print(f"{Colors.GRAY} Disclaimer: For educational/research purposes only.{Colors.RESET}")
print(f"{Colors.CYAN}{'═' * 80}{Colors.RESET}\n")
def run_backtest(args, config: PlatformConfig):
from backtest import WalkForwardBacktester
from metrics import trades_to_dataframe
print_header("USD/JPY WALK-FORWARD BACKTEST")
print(f"{Colors.GRAY} Loading data and replaying bars...{Colors.RESET}\n")
backtester = WalkForwardBacktester(config)
result = backtester.run()
if args.json:
out = {
"metrics": result.metrics,
"initial_balance": result.initial_balance,
"final_balance": result.final_balance,
"trades": [
{
"entry_time": str(t.entry_time),
"exit_time": str(t.exit_time),
"direction": t.direction,
"entry_price": t.entry_price,
"exit_price": t.exit_price,
"exit_reason": t.exit_reason,
"pnl_pips": t.pnl_pips,
"pnl_pct": t.pnl_pct,
"grade": t.grade,
}
for t in result.trades
],
}
print(json.dumps(out, indent=2))
else:
print_backtest_report(result, verbose_journal=args.journal)
if args.journal_csv and result.trades:
df = trades_to_dataframe(result.trades)
df.to_csv(args.journal_csv, index=False)
print(f"\n {Colors.GREEN}✓ Trade journal written to {args.journal_csv}{Colors.RESET}")
def run_optimize(args, config: PlatformConfig):
from backtest import Optimizer
print_header("USD/JPY BACKTEST PARAMETER OPTIMIZER")
grid = config.backtest.param_grid
n_combos = 1
for v in grid.values():
n_combos *= len(v)
print(f"{Colors.GRAY} Grid: {n_combos} combos × {config.backtest.parallel_workers} workers{Colors.RESET}\n")
optimizer = Optimizer(config)
results = optimizer.run_grid(grid)
print(f"\n {Colors.BOLD}Top 10 Parameter Combinations (by total return):{Colors.RESET}\n")
print(f" {'#':<3} {'Return%':>8} {'PF':>6} {'WinR':>6} {'Trades':>7} "
f"{'MaxDD%':>7} {'Sharpe':>7} Params")
print(f" {Colors.GRAY}{'─' * 90}{Colors.RESET}")
for i, r in enumerate(results[:10], start=1):
m = r.metrics
ret = m.get("total_return_pct", 0)
ret_color = Colors.GREEN if ret >= 0 else Colors.RED
params_str = ", ".join(f"{k}={v}" for k, v in r.params.items())
pf = m.get("profit_factor", 0)
pf_str = "inf" if pf == float("inf") else f"{pf:.2f}"
print(f" {i:<3} {ret_color}{ret:>+7.2f}%{Colors.RESET} {pf_str:>6} "
f"{m.get('win_rate', 0):>5.1f}% {m.get('total_trades', 0):>7} "
f"{m.get('max_drawdown_pct', 0):>+6.2f}% {m.get('sharpe', 0):>7.2f}"
f" {Colors.GRAY}{params_str}{Colors.RESET}")
print(f"\n {Colors.BOLD}Best combination details:{Colors.RESET}\n")
print_backtest_report(results[0], params=results[0].params, verbose_journal=False)
def main():
args = parse_args()
log_level = logging.DEBUG if args.verbose else logging.WARNING
logging.basicConfig(
level=log_level,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
config = build_config(args)
if args.optimize:
run_optimize(args, config)
elif args.backtest:
run_backtest(args, config)
else:
run_live(args, config)
if __name__ == "__main__":
main()