c2a2109316
Fixes false 50% drawdown circuit breaker that was blocking all trades. SafetyManager was initialized with peak_equity=initial_capital (200K) but OANDA account starts at ~100K, triggering immediate halt. Now syncs peak_equity to actual portfolio value on startup.
876 lines
34 KiB
Python
876 lines
34 KiB
Python
"""
|
|
BIGGFISH Autonomous Self-Learning Trading Bot
|
|
24/7 entry point - runs forever, learns continuously, trades autonomously.
|
|
|
|
Usage:
|
|
python -m src.main_auto
|
|
python -m src.main_auto --config config/auto_config.json
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
import threading
|
|
import signal as signal_module
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta
|
|
from loguru import logger
|
|
import numpy as np
|
|
|
|
# Setup logging
|
|
log_path = Path(__file__).parent.parent / "logs"
|
|
log_path.mkdir(exist_ok=True)
|
|
logger.add(
|
|
log_path / "biggfish_auto_{time}.log",
|
|
rotation="1 day",
|
|
retention="30 days",
|
|
level="INFO"
|
|
)
|
|
|
|
from trading.broker import AlpacaBroker
|
|
from data.store import DataStore
|
|
from data.candle_cache import CandleCache
|
|
try:
|
|
from trading.oanda_broker import OandaBroker
|
|
except ImportError:
|
|
OandaBroker = None
|
|
from data.features import FeatureEngine
|
|
from backtest.engine import BacktestEngine
|
|
from backtest.metrics import compute_metrics
|
|
from ml.rl_agent import RLAgent
|
|
from ml.rl_environment import TradingEnvironment
|
|
from ml.genetic import GeneticEvolver, StrategyGenome
|
|
from strategies.auto_strategy import genome_to_strategy, evaluate_genome_signal
|
|
from trading.executor import TradingExecutor
|
|
from core.safety import SafetyManager
|
|
from reporting.telegram_reporter import TelegramReporter
|
|
from reporting.krystie_bridge import KrystieBridge
|
|
|
|
|
|
class BiggFishAuto:
|
|
"""
|
|
24/7 Autonomous self-learning trading bot.
|
|
Coordinates trading, backtesting, RL training, and GA evolution.
|
|
"""
|
|
|
|
def __init__(self, config_path: str = "config/auto_config.json"):
|
|
logger.info("Initializing BIGGFISH Autonomous Trader...")
|
|
|
|
# Load config
|
|
with open(config_path) as f:
|
|
self.config = json.load(f)
|
|
|
|
# Initialize components
|
|
self.store = DataStore(self.config['database']['path'])
|
|
self.store.initialize()
|
|
|
|
self.broker = AlpacaBroker(self.config['alpaca'])
|
|
self.feature_engine = FeatureEngine()
|
|
|
|
# OANDA broker for forex (optional)
|
|
self.oanda_broker = None
|
|
oanda_config = self.config.get('oanda')
|
|
if oanda_config and oanda_config.get('api_token') and OandaBroker:
|
|
try:
|
|
self.oanda_broker = OandaBroker(oanda_config)
|
|
logger.info("OANDA forex broker connected")
|
|
except Exception as e:
|
|
logger.warning(f"OANDA connection failed (forex disabled): {e}")
|
|
|
|
self.candle_cache = CandleCache(self.store, oanda_broker=self.oanda_broker)
|
|
|
|
self.backtest_engine = BacktestEngine(
|
|
initial_capital=self.config['backtest']['initial_capital'],
|
|
commission_rate=self.config['trading'].get('commission_rate', 0.001)
|
|
)
|
|
|
|
self.safety = SafetyManager(self.config['safety'], self.store)
|
|
|
|
# Executors per broker
|
|
self.executor = TradingExecutor(
|
|
self.broker, self.store, self.safety, self.config['trading']
|
|
)
|
|
self.forex_executor = None
|
|
if self.oanda_broker:
|
|
self.forex_executor = TradingExecutor(
|
|
self.oanda_broker, self.store, self.safety, self.config['trading']
|
|
)
|
|
|
|
# RL Agent
|
|
env_temp = TradingEnvironment(self.feature_engine,
|
|
initial_capital=self.config['trading']['initial_capital'])
|
|
self.rl_agent = RLAgent(
|
|
state_dim=env_temp.state_dim,
|
|
action_dim=TradingEnvironment.NUM_ACTIONS,
|
|
config=self.config['rl']
|
|
)
|
|
|
|
# GA Evolver
|
|
self.ga_evolver = GeneticEvolver(
|
|
self.config['ga'], self.backtest_engine, self.store
|
|
)
|
|
|
|
# Telegram daily reporter
|
|
tg_config = self.config.get('telegram', {})
|
|
if tg_config.get('enabled') and tg_config.get('bot_token') and tg_config.get('chat_id'):
|
|
self.telegram = TelegramReporter(tg_config['bot_token'], tg_config['chat_id'])
|
|
logger.info("Telegram reporting enabled")
|
|
else:
|
|
self.telegram = None
|
|
logger.info("Telegram reporting disabled (no config)")
|
|
|
|
# Krystie bridge (writes status/events to JSON for Krystie to read)
|
|
self.krystie = KrystieBridge(self.config['database']['path'].rsplit('/', 1)[0] or 'data')
|
|
|
|
# State
|
|
self.running = False
|
|
self.start_time = None
|
|
self.cycle_count = 0
|
|
self.last_backtest = datetime.min
|
|
self.last_rl_train = datetime.min
|
|
self.last_ga_evolve = datetime.min
|
|
self.last_dashboard = datetime.min
|
|
self.last_daily_report = datetime.min
|
|
self.recent_trades = [] # Last 10 trades for dashboard
|
|
self.backtest_results = {} # symbol -> {'sharpe': x, 'win_rate': y}
|
|
|
|
logger.info("BIGGFISH Autonomous Trader initialized")
|
|
|
|
def start(self):
|
|
"""Start the autonomous trading system"""
|
|
self.running = True
|
|
self.start_time = datetime.utcnow()
|
|
|
|
# Register shutdown handler
|
|
signal_module.signal(signal_module.SIGINT, self._signal_handler)
|
|
|
|
logger.info("=" * 60)
|
|
logger.info(" BIGGFISH AUTONOMOUS TRADER STARTING")
|
|
logger.info("=" * 60)
|
|
|
|
# 1. Load saved state
|
|
self._load_state()
|
|
|
|
self.krystie.log_startup()
|
|
|
|
# 1.5. Initialize peak equity from current portfolio value
|
|
try:
|
|
portfolio = self.broker.get_portfolio()
|
|
current_equity = portfolio.get('portfolio_value', self.config['safety']['initial_capital'])
|
|
self.safety.peak_equity = max(current_equity, self.safety.peak_equity)
|
|
logger.info(f"Initialized peak equity: ${self.safety.peak_equity:,.2f}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not initialize peak equity: {e}")
|
|
|
|
# 2. Warm candle cache
|
|
self._warm_cache()
|
|
|
|
# 3. Initial GA population
|
|
self.ga_evolver.initialize_population()
|
|
if not self.ga_evolver.population:
|
|
logger.info("Running initial GA evolution...")
|
|
self._run_ga_evolution()
|
|
|
|
# 4. Initial RL training if no checkpoint
|
|
if self.rl_agent.steps == 0:
|
|
logger.info("Running initial RL training...")
|
|
self._run_rl_training()
|
|
|
|
# 5. Main loop
|
|
logger.info("Entering main loop...")
|
|
self._print_dashboard()
|
|
|
|
while self.running:
|
|
try:
|
|
cycle_start = datetime.utcnow()
|
|
|
|
# Trading cycle (run if any market is open)
|
|
any_market_open = self.broker.is_market_open()
|
|
if self.oanda_broker:
|
|
any_market_open = any_market_open or self.oanda_broker.is_market_open()
|
|
|
|
if any_market_open:
|
|
self._trading_cycle()
|
|
else:
|
|
logger.debug("All markets closed - running learning tasks")
|
|
|
|
# Periodic tasks (run regardless of market hours)
|
|
now = datetime.utcnow()
|
|
|
|
# Cache update (every 5 min)
|
|
cache_interval = self.config['cache']['update_interval_seconds']
|
|
if (now - self.last_backtest).total_seconds() > cache_interval:
|
|
self._update_cache()
|
|
|
|
# Backtest (every 30 min)
|
|
bt_interval = self.config['backtest']['interval_seconds']
|
|
if (now - self.last_backtest).total_seconds() > bt_interval:
|
|
self._run_backtest()
|
|
self.last_backtest = now
|
|
|
|
# RL training (every 2 hours)
|
|
rl_interval = self.config['rl']['train_interval_hours'] * 3600
|
|
if (now - self.last_rl_train).total_seconds() > rl_interval:
|
|
self._run_rl_training()
|
|
self.last_rl_train = now
|
|
|
|
# GA evolution (every 6 hours)
|
|
ga_interval = self.config['ga']['evolution_interval_hours'] * 3600
|
|
if (now - self.last_ga_evolve).total_seconds() > ga_interval:
|
|
self._run_ga_evolution()
|
|
self.last_ga_evolve = now
|
|
|
|
# Daily Telegram report (once per day, around market close ~21:00 UTC)
|
|
if self.telegram and self._should_send_daily_report(now):
|
|
self._send_daily_telegram_report()
|
|
self.last_daily_report = now
|
|
|
|
# Dashboard (every minute)
|
|
dash_interval = self.config['reporting']['dashboard_interval_seconds']
|
|
if (now - self.last_dashboard).total_seconds() > dash_interval:
|
|
self._print_dashboard()
|
|
self.last_dashboard = now
|
|
|
|
# Sleep until next cycle
|
|
elapsed = (datetime.utcnow() - cycle_start).total_seconds()
|
|
sleep_time = max(1, self.config['trading']['cycle_interval_seconds'] - elapsed)
|
|
time.sleep(sleep_time)
|
|
|
|
except KeyboardInterrupt:
|
|
self._shutdown()
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Main loop error: {e}", exc_info=True)
|
|
time.sleep(30)
|
|
|
|
def _is_forex(self, symbol: str) -> bool:
|
|
"""Check if symbol is a forex pair (OANDA format: XXX_YYY)"""
|
|
return '_' in symbol and len(symbol) == 7
|
|
|
|
def _get_broker(self, symbol: str):
|
|
"""Get the appropriate broker for a symbol"""
|
|
if self._is_forex(symbol) and self.oanda_broker:
|
|
return self.oanda_broker
|
|
return self.broker
|
|
|
|
def _get_executor(self, symbol: str):
|
|
"""Get the appropriate executor for a symbol"""
|
|
if self._is_forex(symbol) and self.forex_executor:
|
|
return self.forex_executor
|
|
return self.executor
|
|
|
|
def _trading_cycle(self):
|
|
"""Run one trading cycle across all markets"""
|
|
self.cycle_count += 1
|
|
all_symbols = self._get_tradeable_symbols()
|
|
|
|
# Check exits first
|
|
try:
|
|
current_prices = {}
|
|
for symbol in all_symbols:
|
|
broker = self._get_broker(symbol)
|
|
price = broker.get_latest_price(symbol)
|
|
if price:
|
|
current_prices[symbol] = price
|
|
|
|
# Check exits per executor
|
|
stock_prices = {s: p for s, p in current_prices.items() if not self._is_forex(s)}
|
|
forex_prices = {s: p for s, p in current_prices.items() if self._is_forex(s)}
|
|
|
|
if stock_prices:
|
|
closed = self.executor.check_exits(stock_prices)
|
|
for trade in closed:
|
|
logger.info(f"CLOSED {trade['symbol']}: ${trade['pnl']:+.2f} "
|
|
f"({trade['pnl_pct']:+.1f}%) [{trade['exit_reason']}]")
|
|
self.recent_trades.append(trade)
|
|
self.krystie.log_trade(trade)
|
|
|
|
if forex_prices and self.forex_executor:
|
|
closed = self.forex_executor.check_exits(forex_prices)
|
|
for trade in closed:
|
|
logger.info(f"CLOSED {trade['symbol']}: ${trade['pnl']:+.2f} "
|
|
f"({trade['pnl_pct']:+.1f}%) [{trade['exit_reason']}]")
|
|
self.recent_trades.append(trade)
|
|
self.krystie.log_trade(trade)
|
|
except Exception as e:
|
|
logger.error(f"Exit check error: {e}")
|
|
|
|
# Check safety
|
|
if not self.safety.is_trading_allowed():
|
|
logger.warning(f"Trading halted: {self.safety.halt_reason}")
|
|
return
|
|
|
|
# Evaluate each symbol
|
|
for symbol in all_symbols:
|
|
try:
|
|
self._trade_symbol(symbol)
|
|
except Exception as e:
|
|
logger.debug(f"Error trading {symbol}: {e}")
|
|
|
|
# Keep only last 20 trades
|
|
self.recent_trades = self.recent_trades[-20:]
|
|
|
|
def _get_tradeable_symbols(self) -> list:
|
|
"""Get symbols that can be traded right now"""
|
|
stock_symbols = self.config['trading'].get('symbols', [])
|
|
forex_symbols = self.config['trading'].get('forex_symbols', [])
|
|
|
|
tradeable = []
|
|
|
|
# Stocks: only if Alpaca market is open
|
|
if self.broker.is_market_open():
|
|
tradeable.extend(stock_symbols)
|
|
|
|
# Forex: if OANDA is connected and forex market is open (24/5)
|
|
if self.oanda_broker and self.oanda_broker.is_market_open():
|
|
tradeable.extend(forex_symbols)
|
|
|
|
return tradeable
|
|
|
|
def _trade_symbol(self, symbol: str):
|
|
"""Evaluate and potentially trade a single symbol (scalping mode)"""
|
|
# Filter by backtest Sharpe ratio
|
|
min_sharpe = self.config['trading'].get('min_backtest_sharpe', 0.5)
|
|
if symbol in self.backtest_results:
|
|
if self.backtest_results[symbol]['sharpe'] < min_sharpe:
|
|
logger.debug(f"Skipping {symbol}: Sharpe {self.backtest_results[symbol]['sharpe']:.2f} < {min_sharpe}")
|
|
return
|
|
|
|
broker = self._get_broker(symbol)
|
|
executor = self._get_executor(symbol)
|
|
|
|
# Use 5m candles for scalping, fall back to 1h
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '5m',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is None or len(df) < 60:
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '1h',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is None or len(df) < 60:
|
|
return
|
|
|
|
# Compute features
|
|
features_df = self.feature_engine.compute_and_normalize(df)
|
|
if features_df is None or len(features_df) < 10:
|
|
return
|
|
|
|
# Get market state
|
|
state = self.feature_engine.get_state_vector(features_df, -1)
|
|
|
|
# Get GA signal
|
|
best_genome = self.ga_evolver.get_best_genome()
|
|
ga_signal = {'signal': 'hold', 'confidence': 0.0}
|
|
strategy_params = {}
|
|
|
|
if best_genome:
|
|
raw_df = self.feature_engine.compute(df)
|
|
if raw_df is not None and len(raw_df) > 0:
|
|
ga_signal = evaluate_genome_signal(best_genome, raw_df, len(raw_df) - 1)
|
|
|
|
# Use config defaults if GA doesn't provide stop-loss/take-profit
|
|
sl_pct = self.config['trading'].get('stop_loss_pct', 2.5) / 100
|
|
tp_pct = self.config['trading'].get('take_profit_pct', 5.0) / 100
|
|
|
|
# CRITICAL FIX: Convert percentages to absolute prices
|
|
# Stop-loss BELOW current price for longs, ABOVE for shorts
|
|
# Take-profit ABOVE current price for longs, BELOW for shorts
|
|
current_price = broker.get_latest_price(symbol) or 0
|
|
|
|
sl_price = current_price * (1 - sl_pct) if current_price > 0 else None
|
|
tp_price = current_price * (1 + tp_pct) if current_price > 0 else None
|
|
short_sl_price = current_price * (1 + sl_pct) if current_price > 0 else None # ABOVE for shorts
|
|
short_tp_price = current_price * (1 - tp_pct) if current_price > 0 else None # BELOW for shorts
|
|
|
|
strategy_params = {
|
|
'stop_loss': sl_price,
|
|
'take_profit': tp_price,
|
|
'short_stop_loss': short_sl_price,
|
|
'short_take_profit': short_tp_price,
|
|
'position_pct': ga_signal.get('position_pct', 0.1),
|
|
'strategy_id': f"ga_gen{best_genome.generation}",
|
|
}
|
|
|
|
# Augment state with GA signal + portfolio
|
|
portfolio_state = executor.get_portfolio_state()
|
|
signal_dir = 1.0 if ga_signal['signal'] == 'buy' else (
|
|
-1.0 if ga_signal['signal'] == 'sell' else 0.0)
|
|
|
|
augmented_state = np.concatenate([
|
|
state,
|
|
[portfolio_state.get('position_ratio', 0),
|
|
portfolio_state.get('unrealized_pnl', 0),
|
|
portfolio_state.get('time_in_position', 0)],
|
|
[signal_dir, ga_signal.get('confidence', 0.0)],
|
|
])
|
|
|
|
# RL agent decides (now with 7 actions including shorts)
|
|
action = self.rl_agent.select_action(augmented_state, live_mode=True)
|
|
|
|
if action == 0: # Hold
|
|
return
|
|
|
|
# Get current price
|
|
current_price = broker.get_latest_price(symbol)
|
|
if not current_price:
|
|
return
|
|
|
|
# Execute
|
|
trade = executor.execute_signal(
|
|
symbol, action, current_price, strategy_params
|
|
)
|
|
|
|
if trade:
|
|
action_name = TradingEnvironment.ACTION_NAMES[action]
|
|
logger.info(f"TRADE: {action_name} {symbol} @ ${current_price:.4f}")
|
|
self.recent_trades.append(trade)
|
|
self.krystie.log_trade(trade)
|
|
|
|
def _all_symbols(self) -> list:
|
|
"""Get all configured symbols (stocks + forex)"""
|
|
symbols = list(self.config['trading'].get('symbols', []))
|
|
symbols.extend(self.config['trading'].get('forex_symbols', []))
|
|
return symbols
|
|
|
|
def _warm_cache(self):
|
|
"""Warm the candle cache"""
|
|
logger.info("Warming candle cache...")
|
|
symbols = self._all_symbols()
|
|
timeframes = self.config['cache'].get('timeframes', ['1h'])
|
|
lookback = self.config['cache'].get('warmup_lookback_days', 90)
|
|
|
|
self.candle_cache.warm_cache(symbols, timeframes, lookback)
|
|
logger.info("Cache warmup complete")
|
|
|
|
def _update_cache(self):
|
|
"""Update candle cache incrementally"""
|
|
symbols = self._all_symbols()
|
|
timeframes = self.config['cache'].get('timeframes', ['1h'])
|
|
self.candle_cache.update_cache(symbols, timeframes)
|
|
|
|
def _run_backtest(self):
|
|
"""Run background backtest of current strategy (scalping timeframe)"""
|
|
best_genome = self.ga_evolver.get_best_genome()
|
|
if not best_genome:
|
|
return
|
|
|
|
strategy_fn = genome_to_strategy(best_genome)
|
|
symbols = self._all_symbols()[:5] # Top 5 for breadth
|
|
|
|
lookback = self.config['backtest'].get('lookback_days', 14)
|
|
|
|
for symbol in symbols:
|
|
# Prefer 5m data for scalping backtest
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '5m',
|
|
start=datetime.utcnow() - timedelta(days=lookback)
|
|
)
|
|
if df is None or len(df) < 60:
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '1h',
|
|
start=datetime.utcnow() - timedelta(days=lookback)
|
|
)
|
|
if df is None or len(df) < 60:
|
|
continue
|
|
|
|
featured_df = self.feature_engine.compute(df)
|
|
if featured_df is None or len(featured_df) < 50:
|
|
continue
|
|
|
|
result = self.backtest_engine.run(
|
|
strategy_fn, featured_df,
|
|
params=best_genome.to_dict(), symbol=symbol
|
|
)
|
|
|
|
if result.metrics.get('total_trades', 0) > 0:
|
|
self.store.record_strategy_result(
|
|
strategy_id=f"ga_gen{best_genome.generation}",
|
|
params=best_genome.to_dict(),
|
|
metrics=result.metrics
|
|
)
|
|
# Store backtest result for filtering
|
|
self.backtest_results[symbol] = {
|
|
'sharpe': result.metrics['sharpe_ratio'],
|
|
'win_rate': result.metrics['win_rate'],
|
|
'total_return': result.metrics['total_return'],
|
|
}
|
|
logger.info(f"Backtest {symbol}: Sharpe={result.metrics['sharpe_ratio']:.2f} "
|
|
f"WR={result.metrics['win_rate']:.0f}% "
|
|
f"Return={result.metrics['total_return']:.1f}%")
|
|
|
|
def _run_rl_training(self):
|
|
"""Run RL batch training"""
|
|
logger.info("Starting RL training...")
|
|
env = TradingEnvironment(
|
|
self.feature_engine,
|
|
initial_capital=self.config['trading']['initial_capital']
|
|
)
|
|
|
|
total_metrics = {'avg_loss': 0, 'episodes': 0}
|
|
|
|
for symbol in self._all_symbols()[:5]:
|
|
# Prefer 5m data for RL training (more scalping episodes)
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '5m',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is None or len(df) < 100:
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '1h',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is None or len(df) < 100:
|
|
continue
|
|
|
|
# Create GA signal function for this data
|
|
best_genome = self.ga_evolver.get_best_genome()
|
|
ga_fn = None
|
|
if best_genome:
|
|
featured = self.feature_engine.compute(df)
|
|
if featured is not None and len(featured) > 0:
|
|
def make_ga_fn(genome, feat_df):
|
|
def fn(step):
|
|
if step < len(feat_df):
|
|
return evaluate_genome_signal(genome, feat_df, step)
|
|
return {'signal': 'hold', 'confidence': 0.0}
|
|
return fn
|
|
ga_fn = make_ga_fn(best_genome, featured)
|
|
|
|
metrics = self.rl_agent.train_on_episode(env, df, ga_signal_fn=ga_fn)
|
|
total_metrics['avg_loss'] += metrics.get('avg_loss', 0)
|
|
total_metrics['episodes'] += 1
|
|
|
|
# Save checkpoint
|
|
self.rl_agent.save(self.store)
|
|
|
|
if total_metrics['episodes'] > 0:
|
|
avg = total_metrics['avg_loss'] / total_metrics['episodes']
|
|
logger.info(f"RL training complete: {total_metrics['episodes']} episodes, "
|
|
f"avg_loss={avg:.6f}, epsilon={self.rl_agent.epsilon:.4f}")
|
|
|
|
def _run_ga_evolution(self):
|
|
"""Run GA evolution cycle"""
|
|
logger.info("Starting GA evolution...")
|
|
|
|
# Prepare candle data for fitness evaluation
|
|
candles_data = {}
|
|
symbols = self._all_symbols()[:5]
|
|
|
|
for symbol in symbols:
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '5m',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is None or len(df) < 60:
|
|
df = self.candle_cache.get_cached(
|
|
symbol, '1h',
|
|
start=datetime.utcnow() - timedelta(days=14)
|
|
)
|
|
if df is not None and len(df) > 60:
|
|
featured = self.feature_engine.compute(df)
|
|
if featured is not None and len(featured) > 50:
|
|
candles_data[symbol] = featured
|
|
|
|
if not candles_data:
|
|
logger.warning("No data available for GA evolution")
|
|
return
|
|
|
|
# Strategy function factory
|
|
def strategy_fn_factory(genome):
|
|
return genome_to_strategy(genome)
|
|
|
|
# Run evolution
|
|
num_gens = self.config['ga'].get('generations_per_cycle', 10)
|
|
best = self.ga_evolver.run_evolution_cycle(
|
|
strategy_fn_factory, candles_data, num_generations=num_gens
|
|
)
|
|
|
|
if best:
|
|
logger.info(f"GA evolution complete: Gen {self.ga_evolver.generation}, "
|
|
f"best fitness={best.fitness:.4f}")
|
|
self.krystie.log_ga_milestone(self.ga_evolver.generation, best.fitness)
|
|
|
|
def _should_send_daily_report(self, now: datetime) -> bool:
|
|
"""Check if it's time to send the daily report (once per day after 21:00 UTC)"""
|
|
report_hour = self.config.get('telegram', {}).get('daily_report_hour', 21)
|
|
if now.hour >= report_hour and (now - self.last_daily_report).total_seconds() > 20 * 3600:
|
|
return True
|
|
return False
|
|
|
|
def _send_daily_telegram_report(self):
|
|
"""Gather data and send the daily Telegram report"""
|
|
try:
|
|
portfolio = self.broker.get_portfolio()
|
|
positions = self.broker.get_positions()
|
|
|
|
# Add OANDA positions if available
|
|
if self.oanda_broker:
|
|
try:
|
|
oanda_positions = self.oanda_broker.get_positions()
|
|
positions.extend(oanda_positions)
|
|
except Exception:
|
|
pass
|
|
|
|
# Get today's trades from DB
|
|
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
today_trades = self.store.get_trades(start=today_start, limit=50)
|
|
|
|
# Learning stats
|
|
rl_stats = self.rl_agent.get_stats()
|
|
best_genome = self.ga_evolver.get_best_genome()
|
|
learning_stats = {
|
|
'rl': rl_stats,
|
|
'ga': {
|
|
'generation': self.ga_evolver.generation,
|
|
'best_fitness': best_genome.fitness if best_genome else 0,
|
|
},
|
|
}
|
|
|
|
self.telegram.send_daily_report(
|
|
portfolio, positions, today_trades, learning_stats,
|
|
self.config['trading']
|
|
)
|
|
self.krystie.log_daily_report(
|
|
equity=portfolio.get('equity', 0),
|
|
day_pnl=portfolio.get('day_pnl', 0),
|
|
trades_count=len(today_trades),
|
|
)
|
|
logger.info("Daily Telegram report sent")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send daily Telegram report: {e}")
|
|
|
|
def _print_dashboard(self):
|
|
"""Print live console dashboard"""
|
|
try:
|
|
# Get Alpaca portfolio
|
|
alpaca_portfolio = self.broker.get_portfolio()
|
|
alpaca_positions = self.broker.get_positions()
|
|
|
|
# Get OANDA portfolio if available
|
|
if self.oanda_broker:
|
|
oanda_portfolio = self.oanda_broker.get_portfolio()
|
|
oanda_positions = self.oanda_broker.get_positions()
|
|
|
|
# Combine portfolios
|
|
combined_equity = alpaca_portfolio['equity'] + oanda_portfolio['equity']
|
|
combined_day_pnl = alpaca_portfolio['day_pnl'] + oanda_portfolio['day_pnl']
|
|
prev_equity = combined_equity - combined_day_pnl
|
|
|
|
portfolio = {
|
|
'equity': combined_equity,
|
|
'cash': alpaca_portfolio['cash'] + oanda_portfolio['cash'],
|
|
'buying_power': alpaca_portfolio['buying_power'] + oanda_portfolio['buying_power'],
|
|
'portfolio_value': alpaca_portfolio['portfolio_value'] + oanda_portfolio['portfolio_value'],
|
|
'long_market_value': alpaca_portfolio['long_market_value'] + oanda_portfolio['long_market_value'],
|
|
'day_pnl': combined_day_pnl,
|
|
'day_pnl_pct': (combined_day_pnl / prev_equity * 100) if prev_equity > 0 else 0,
|
|
}
|
|
# Combine positions
|
|
positions = alpaca_positions + oanda_positions
|
|
else:
|
|
portfolio = alpaca_portfolio
|
|
positions = alpaca_positions
|
|
|
|
except Exception as e:
|
|
logger.error(f"Dashboard error: {e}")
|
|
return
|
|
|
|
equity = portfolio['equity']
|
|
target = self.config['trading']['target_capital']
|
|
|
|
# Calculate actual initial capital (both brokers start with $100k each in paper trading)
|
|
if self.oanda_broker:
|
|
initial = 200000 # $100k Alpaca + $100k OANDA
|
|
else:
|
|
initial = 100000 # $100k Alpaca only
|
|
|
|
progress = (equity / target) * 100 if target > 0 else 0
|
|
total_pnl = equity - initial
|
|
total_pnl_pct = (total_pnl / initial) * 100 if initial > 0 else 0
|
|
|
|
uptime = datetime.utcnow() - self.start_time if self.start_time else timedelta()
|
|
hours = int(uptime.total_seconds() // 3600)
|
|
minutes = int((uptime.total_seconds() % 3600) // 60)
|
|
|
|
best_genome = self.ga_evolver.get_best_genome()
|
|
gen = self.ga_evolver.generation
|
|
best_fit = best_genome.fitness if best_genome else 0
|
|
|
|
rl_stats = self.rl_agent.get_stats()
|
|
safety_status = self.safety.get_status()
|
|
|
|
stock_status = "OPEN" if self.broker.is_market_open() else "CLOSED"
|
|
forex_status = ""
|
|
if self.oanda_broker:
|
|
forex_status = " | FX: " + ("OPEN" if self.oanda_broker.is_market_open() else "CLOSED")
|
|
|
|
# Build dashboard
|
|
lines = []
|
|
lines.append("")
|
|
lines.append("=" * 64)
|
|
lines.append(f" BIGGFISH AUTONOMOUS TRADER | {hours}h {minutes}m | "
|
|
f"Gen {gen} | Stocks: {stock_status}{forex_status}")
|
|
lines.append("=" * 64)
|
|
|
|
# Progress bar
|
|
bar_width = 30
|
|
filled = int(bar_width * min(progress, 100) / 100)
|
|
bar = "#" * filled + "-" * (bar_width - filled)
|
|
lines.append(f" Portfolio: ${equity:.2f} / ${target} [{bar}] {progress:.1f}%")
|
|
lines.append(f" Day P&L: ${portfolio['day_pnl']:+.2f} ({portfolio['day_pnl_pct']:+.1f}%)")
|
|
lines.append(f" Total P&L: ${total_pnl:+.2f} ({total_pnl_pct:+.1f}%)")
|
|
|
|
# Positions
|
|
lines.append("-" * 64)
|
|
if positions:
|
|
lines.append(f" Active Positions ({len(positions)}):")
|
|
for p in positions[:5]:
|
|
pl_str = f"${p['unrealized_pl']:+.2f} ({p['unrealized_plpc']:+.1f}%)"
|
|
lines.append(f" {p['symbol']:6s} {p['qty']:.0f} @ ${p['avg_entry_price']:.2f}"
|
|
f" -> ${p['current_price']:.2f} {pl_str}")
|
|
else:
|
|
lines.append(" No active positions")
|
|
|
|
# Learning status
|
|
lines.append("-" * 64)
|
|
lines.append(" Learning Status:")
|
|
lines.append(f" RL Agent: epsilon={rl_stats['epsilon']:.3f} | "
|
|
f"loss={rl_stats['avg_loss']:.6f} | "
|
|
f"{rl_stats['memory_size']:,} experiences")
|
|
lines.append(f" GA: gen {gen} | best fitness={best_fit:.4f}")
|
|
|
|
# Recent trades
|
|
if self.recent_trades:
|
|
lines.append("-" * 64)
|
|
lines.append(" Recent Trades:")
|
|
for t in self.recent_trades[-5:]:
|
|
side = t.get('side', '?').upper()
|
|
symbol = t.get('symbol', '?')
|
|
pnl = t.get('pnl')
|
|
if pnl is not None:
|
|
pnl_str = f" P&L: ${pnl:+.2f}"
|
|
else:
|
|
pnl_str = ""
|
|
price = t.get('entry_price') or t.get('exit_price', 0)
|
|
lines.append(f" {side:4s} {symbol:6s} "
|
|
f"{t.get('amount', 0):.1f} @ ${price:.2f}{pnl_str}")
|
|
|
|
# Safety
|
|
if not safety_status['trading_allowed']:
|
|
lines.append("-" * 64)
|
|
lines.append(f" !! TRADING HALTED: {safety_status['halt_reason']}")
|
|
|
|
# Next events
|
|
lines.append("-" * 64)
|
|
now = datetime.utcnow()
|
|
bt_next = max(0, self.config['backtest']['interval_seconds'] -
|
|
(now - self.last_backtest).total_seconds())
|
|
rl_next = max(0, self.config['rl']['train_interval_hours'] * 3600 -
|
|
(now - self.last_rl_train).total_seconds())
|
|
ga_next = max(0, self.config['ga']['evolution_interval_hours'] * 3600 -
|
|
(now - self.last_ga_evolve).total_seconds())
|
|
|
|
lines.append(f" Next: backtest {bt_next/60:.0f}m | "
|
|
f"RL train {rl_next/60:.0f}m | "
|
|
f"GA evolve {ga_next/3600:.1f}h")
|
|
lines.append("=" * 64)
|
|
|
|
print("\n".join(lines))
|
|
|
|
# Update Krystie status file
|
|
try:
|
|
markets = {"stocks": stock_status}
|
|
if self.oanda_broker:
|
|
markets["forex"] = "OPEN" if self.oanda_broker.is_market_open() else "CLOSED"
|
|
|
|
learning_stats = {
|
|
'rl': rl_stats,
|
|
'ga': {'generation': gen, 'best_fitness': best_fit},
|
|
}
|
|
|
|
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
today_trades = self.store.get_trades(start=today_start, limit=50)
|
|
|
|
self.krystie.update_status(
|
|
portfolio=portfolio,
|
|
positions=positions,
|
|
learning_stats=learning_stats,
|
|
markets=markets,
|
|
config=self.config['trading'],
|
|
uptime_seconds=uptime.total_seconds(),
|
|
today_trades=today_trades,
|
|
)
|
|
except Exception as e:
|
|
logger.debug(f"Krystie status update error: {e}")
|
|
|
|
def _load_state(self):
|
|
"""Load saved state from database"""
|
|
# Load RL model
|
|
if self.rl_agent.load(self.store):
|
|
logger.info("Loaded RL model from checkpoint")
|
|
|
|
# Load timing state
|
|
last_bt = self.store.load_state('last_backtest')
|
|
if last_bt:
|
|
self.last_backtest = datetime.fromisoformat(last_bt)
|
|
|
|
last_rl = self.store.load_state('last_rl_train')
|
|
if last_rl:
|
|
self.last_rl_train = datetime.fromisoformat(last_rl)
|
|
|
|
last_ga = self.store.load_state('last_ga_evolve')
|
|
if last_ga:
|
|
self.last_ga_evolve = datetime.fromisoformat(last_ga)
|
|
|
|
last_report = self.store.load_state('last_daily_report')
|
|
if last_report:
|
|
self.last_daily_report = datetime.fromisoformat(last_report)
|
|
|
|
logger.info("State loaded from database")
|
|
|
|
def _save_state(self):
|
|
"""Save state to database for recovery"""
|
|
self.rl_agent.save(self.store)
|
|
self.store.save_state('last_backtest', self.last_backtest.isoformat())
|
|
self.store.save_state('last_rl_train', self.last_rl_train.isoformat())
|
|
self.store.save_state('last_ga_evolve', self.last_ga_evolve.isoformat())
|
|
self.store.save_state('last_daily_report', self.last_daily_report.isoformat())
|
|
self.store.save_state('last_shutdown', datetime.utcnow().isoformat())
|
|
logger.info("State saved to database")
|
|
|
|
def _signal_handler(self, signum, frame):
|
|
"""Handle SIGINT for graceful shutdown"""
|
|
self._shutdown()
|
|
|
|
def _shutdown(self):
|
|
"""Graceful shutdown"""
|
|
logger.info("Shutting down BIGGFISH...")
|
|
self.running = False
|
|
self.krystie.log_shutdown()
|
|
self._save_state()
|
|
self.store.close()
|
|
logger.info("Shutdown complete. State saved. Resume anytime.")
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="BIGGFISH Autonomous Trader")
|
|
parser.add_argument('--config', default='config/auto_config.json',
|
|
help='Path to configuration file')
|
|
args = parser.parse_args()
|
|
|
|
config_path = Path(args.config)
|
|
if not config_path.exists():
|
|
print(f"Config file not found: {config_path}")
|
|
print("Copy config/auto_config.json and fill in your Alpaca API keys")
|
|
sys.exit(1)
|
|
|
|
bot = BiggFishAuto(str(config_path))
|
|
bot.start()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|