Files
biggfish/src/main_auto.py
T
sami7777 c2a2109316 fix: initialize peak_equity from actual portfolio value on startup
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.
2026-03-26 08:23:48 +01:00

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()