""" bot.py — Main entry point for the TSLA grid trading bot. ========================================================= Run this file to start the bot. USAGE: ------ python3 bot.py --mode dryrun # default — safe, no actions python3 bot.py --mode confirm # ask before every action python3 bot.py --mode live # fully autonomous FIRST TIME SETUP: ----------------- 1. Get API key from Capital.com → Settings → API integrations 2. Set environment variables: export CAPITAL_API_KEY="your_key" export CAPITAL_IDENTIFIER="your@email.com" export CAPITAL_PASSWORD="your_password" 3. Run dryrun first and verify the output looks correct. 4. Run confirm mode for several days, approve each action manually. 5. Only switch to live mode when fully confident. TO RUN AS A SYSTEM SERVICE (auto-start on boot): ------------------------------------------------- See README.md for systemd setup instructions. PROJECT STRUCTURE: ------------------ bot.py ← YOU ARE HERE — main loop, startup, CLI config.py ← ALL settings — edit this for day-to-day changes client.py ← Capital.com API calls state.py ← Account state snapshot, field access helpers calculator.py ← Grid maths, survival check, gap detection grid.py ← Order management logic (gap-filling, TP rules) actions.py ← dry/confirm/live action handler bot.log ← All activity logged here (auto-created) ENHANCING THE BOT: ------------------ The code is intentionally split so future additions are isolated: - Telegram alerts → new file telegram.py, called from run_loop() - Profit tracking → new file tracker.py, log closed positions - Claude AI → new file ai_advisor.py, called on edge cases - Web dashboard → new file dashboard.py, reads state and logs - Auto threshold → add equity check in run_loop(), update config Each addition touches one file without breaking anything else. WHAT THE BOT DOES EACH LOOP (every 60 seconds): ------------------------------------------------ 1. Refresh API session if tokens approaching expiry 2. Fetch: positions, orders, account balance, TSLA price, GBP/USD rate 3. Check for abnormal price gap (> 20%) — pause if detected 4. Check margin health — stop placing orders if level < 100% 5. Manage bottom two positions (TP rules) 6. Manage order grid (cancel out-of-window, fill gaps) 7. Sleep 60 seconds, repeat """ import argparse import logging import os import sys import time from datetime import datetime import config from client import CapitalClient from state import BotState, pos_level, pos_size, pos_tp, ord_level, ord_size from calculator import GridCalculator from grid import GridManager from actions import ActionHandler from calibration import Calibrator, setup_calibration_log log = logging.getLogger("maxbot") # ───────────────────────────────────────────── # LOGGING SETUP # ───────────────────────────────────────────── def setup_logging(): """ Two log files in the logs/ subfolder: logs/bot.log — full detail, every loop, for debugging logs/actions.log — only actions taken (orders placed/cancelled, TP changes) logs/calibration.log — startup calibration reports, appended each run Monitoring tip: tail -f ~/maxbot/logs/actions.log ← clean, just what the bot did tail -f ~/maxbot/logs/bot.log ← full detail if something goes wrong cat ~/maxbot/logs/calibration.log ← progress reports over time """ base = os.path.dirname(os.path.abspath(__file__)) logs_dir = os.path.join(base, "logs") os.makedirs(logs_dir, exist_ok=True) fmt = "%(asctime)s [%(levelname)s] %(message)s" full_handler = logging.FileHandler(os.path.join(logs_dir, "bot.log")) full_handler.setFormatter(logging.Formatter(fmt)) console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(logging.Formatter(fmt)) root = logging.getLogger() root.setLevel(logging.INFO) root.addHandler(full_handler) root.addHandler(console_handler) actions_handler = logging.FileHandler(os.path.join(logs_dir, "actions.log")) actions_handler.setFormatter(logging.Formatter(fmt)) actions_handler.setLevel(logging.WARNING) logging.getLogger("maxbot.actions").addHandler(actions_handler) setup_calibration_log() # ───────────────────────────────────────────── # MAIN BOT CLASS # ───────────────────────────────────────────── class TeslaGridBot: def __init__(self, mode: str): self.mode = mode self.client = CapitalClient() self.state = BotState() self.calculator = GridCalculator() self.actions = ActionHandler(mode, self.client) self.grid = GridManager(self.state, self.calculator, self.actions) self.calibrator = Calibrator(self.state, self.calculator) self.loop_count = 0 self.gap_pause = False self._prev_position_count = None self._prev_order_count = None self._prev_equity = None # ───────────────────────────────────────── # STARTUP # ───────────────────────────────────────── def banner(self): labels = { "dryrun": "DRY RUN — no actions will be taken", "confirm": "CONFIRM MODE — will ask before every action", "live": "LIVE MODE — fully autonomous", } print("\n" + "=" * 60) print(" TSLA Grid Trading Bot — Capital.com") print(f" Mode: {labels.get(self.mode, self.mode)}") print(f" Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 60 + "\n") def fetch_state(self): """Refresh all state from Capital.com API.""" self.client.refresh_session_if_needed() positions = self.client.get_positions() orders = self.client.get_working_orders() account = self.client.get_account_details() price_data = self.client.get_price(config.EPIC) gbpusd = self.client.get_gbpusd() self.state.update(positions, orders, account, price_data, gbpusd) def print_state(self): """Print current account and grid status to log.""" s = self.state log.info("─" * 50) log.info(f"TSLA price: ${s.current_price:.2f}") log.info(f"Equity: £{s.equity:.2f}") log.info(f"Available: £{s.available:.2f}") log.info(f"Margin in use: £{s.total_margin_gbp:.2f}") log.info(f"Margin level: {s.margin_level_pct:.1f}%") log.info(f"Open positions: {len(s.positions)}") log.info(f"Pending orders: {len(s.orders)}") log.info(f"Highest open: ${s.highest_open_price:.2f}") log.info(f"Lowest level: ${s.lowest_level_price:.2f}") log.info(f"GBP/USD: {s.gbpusd:.4f}") calc = self.calculator spacing = calc.get_spacing_pct(s.equity) depth = calc.get_queue_depth(s.equity) full_sim = calc._simulate_grid( s.highest_open_price, spacing, s.gbpusd, s.all_levels() ) survival = calc.survival_check( s.equity, s.highest_open_price, full_sim, s.gbpusd, override_pct=60.0 ) # Find max safe % for clear display max_safe = 0.0 for pct in range(10, 81, 5): sim = calc._simulate_grid( s.highest_open_price, spacing, s.gbpusd, s.all_levels() ) r = calc.survival_check( s.equity, s.highest_open_price, sim, s.gbpusd, override_pct=float(pct) ) if r["safe"]: max_safe = float(pct) else: break log.info(f"Grid spacing: {spacing}%") log.info(f"Queue depth: {depth}") log.info( f"Survival: max safe {max_safe:.0f}% | " f"60% sim {'✓' if survival['safe'] else '✗'} " f"(margin at floor: {survival['margin_level_at_floor_pct']:.1f}%)" ) log.info("─" * 50) def startup_audit(self): """ Full account audit on first start. Reads and displays everything before taking any action. """ log.info("=" * 50) log.info("STARTUP AUDIT") log.info("=" * 50) self.fetch_state() self.print_state() s = self.state # Print all open positions sorted_pos = s.positions_sorted_asc() log.info(f"\nOpen positions ({len(sorted_pos)}) — lowest to highest:") for p in sorted_pos: tp = pos_tp(p) tp_str = f"${tp:.2f}" if tp else "NONE (manual close)" upl = float(p["position"].get("upl", 0)) log.info( f" ${pos_level(p):.2f} | " f"size {p['position']['size']} | " f"TP {tp_str} | " f"P&L £{upl / self.state.gbpusd:.2f}" ) # Print all pending orders sorted_ord = s.orders_sorted_asc() log.info(f"\nPending orders ({len(sorted_ord)}) — lowest to highest:") for o in sorted_ord: log.info(f" ${ord_level(o):.2f} | size {ord_size(o)}") # Full survival check log.info("") self._log_survival_check() log.info("\nStartup audit complete.") log.info("=" * 50) # ───────────────────────────────────────── # CHECKS # ───────────────────────────────────────── def _log_survival_check(self): """Run and log full survival check — always simulates full 60% grid.""" s = self.state calc = self.calculator # Always simulate full grid at 60% drop — honest worst case full_sim = calc._simulate_grid( s.highest_open_price, calc.get_spacing_pct(s.equity), s.gbpusd, s.all_levels() ) result = calc.survival_check( s.equity, s.highest_open_price, full_sim, s.gbpusd, override_pct=60.0 ) status = "✓ SAFE" if result["safe"] else "✗ UNSAFE" log.info(f"Survival check (full 60% grid simulation): {status}") log.info(f" Floor price: ${result['floor_price']:.2f}") log.info(f" Total margin (GBP): £{result['total_margin_gbp']:.2f}") log.info(f" Equity at floor: £{result['equity_at_floor_gbp']:.2f}") log.info(f" Margin level there: {result['margin_level_at_floor_pct']:.1f}%") log.info(f" Unrealised P&L: £{result['unrealised_loss_gbp']:.2f}") if not result["safe"]: log.warning( "SURVIVAL CHECK FAILED — " "account may be margin called at 60% drop." ) return result def check_price_gap(self) -> bool: """ Detect abnormal price gaps between loops. A gap > GAP_ALERT_PCT triggers a pause on order placement. This protects against Monday gap-downs, stock splits, news events. The bot will resume normal operation next loop after alerting. To resume immediately, restart the bot. """ s = self.state if s.last_price and s.last_price > 0: change_pct = abs(s.current_price - s.last_price) / s.last_price * 100 if change_pct >= config.GAP_ALERT_PCT: log.warning("!" * 50) log.warning( f"PRICE GAP DETECTED: " f"${s.last_price:.2f} → ${s.current_price:.2f} " f"({change_pct:.1f}%)" ) log.warning( "This may be a gap-down open, stock split, or major news event." ) log.warning( "Bot is PAUSING order placement this cycle. " "Review Capital.com manually." ) log.warning("!" * 50) self.gap_pause = True return True s.last_price = s.current_price self.gap_pause = False return False def check_margin_health(self) -> str: """ Check current margin level against Capital.com thresholds. Returns: 'ok', 'warning1', 'warning2', or 'closeout' Capital.com UK retail margin call stages: > 100% → Normal = 100% → Warning 1: cannot open new positions = 75% → Warning 2: urgent = 50% → Auto closeout begins """ ml = self.state.margin_level_pct if ml <= 50: log.warning( f"MARGIN CLOSEOUT LEVEL ({ml:.1f}% ≤ 50%). " "Capital.com may be closing positions!" ) return "closeout" if ml <= 75: log.warning( f"MARGIN WARNING 2 ({ml:.1f}% ≤ 75%). " "Cannot place new orders." ) return "warning2" if ml <= 100: log.warning( f"MARGIN WARNING 1 ({ml:.1f}% ≤ 100%). " "Cannot place new orders." ) return "warning1" return "ok" # ───────────────────────────────────────── # MAIN LOOP # ───────────────────────────────────────── def run_loop(self): """Single iteration of the main loop.""" self.loop_count += 1 self.fetch_state() s = self.state # Detect meaningful changes since last loop pos_count = len(s.positions) ord_count = len(s.orders) equity_delta = abs((s.equity - self._prev_equity) if self._prev_equity else 0) state_changed = ( pos_count != self._prev_position_count or ord_count != self._prev_order_count or equity_delta > 0.50 # equity moved more than 50p ) if state_changed: # Full state print when something meaningful changed log.info(f"\n{'=' * 50}") log.info(f"Loop #{self.loop_count} — {datetime.now().strftime('%H:%M:%S')} — STATE CHANGED") self.print_state() else: # Quiet heartbeat — one line only log.info( f"Loop #{self.loop_count} — {datetime.now().strftime('%H:%M:%S')} — " f"no changes " f"(price ${s.current_price:.2f} " f"equity £{s.equity:.2f} " f"pos {pos_count} orders {ord_count})" ) # Update previous state self._prev_position_count = pos_count self._prev_order_count = ord_count self._prev_equity = s.equity # Gap detection if self.check_price_gap(): log.warning("Gap detected — skipping order management this cycle.") return # Margin health margin_status = self.check_margin_health() if margin_status != "ok": log.warning( f"Margin [{margin_status}] — managing positions only, " "no new orders." ) self.grid.manage_bottom_two_positions() return # Normal operation self.grid.manage_bottom_two_positions() self.grid.manage_orders() def run(self): """Start the bot.""" self.banner() log.info("Connecting to Capital.com...") try: self.client.create_session() except Exception as e: log.error(f"Connection failed: {e}") log.error( "Check credentials: " "CAPITAL_API_KEY, CAPITAL_IDENTIFIER, CAPITAL_PASSWORD" ) sys.exit(1) self.startup_audit() try: self.calibrator.run() except Exception as e: log.error(f"Calibration failed: {e}", exc_info=True) if self.mode == "dryrun": log.info("\nDRY RUN complete — no actions taken.") log.info("Review output above, then run with --mode confirm") return log.info( f"\nEntering main loop " f"(every {config.LOOP_INTERVAL_SECS}s)..." ) log.info("Press Ctrl+C to stop.\n") try: while True: try: self.run_loop() except KeyboardInterrupt: raise except Exception as e: log.error(f"Loop error: {e}", exc_info=True) log.info("Waiting 30s before retry...") time.sleep(30) continue time.sleep(config.LOOP_INTERVAL_SECS) except KeyboardInterrupt: log.info(f"\nBot stopped by user.") log.info(self.actions.summary()) # ───────────────────────────────────────────── # CLI # ───────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="TSLA Grid Trading Bot — Capital.com", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Modes: dryrun Connect and read everything. Print what bot would do. No actions. confirm Ask y/n before every action. Run this for days before going live. live Fully autonomous. Only use after thorough testing. Examples: python3 bot.py # dryrun (default) python3 bot.py --mode confirm # confirm mode python3 bot.py --mode live # live mode """ ) parser.add_argument( "--mode", choices=["dryrun", "confirm", "live"], default="dryrun", help="Operation mode (default: dryrun)" ) args = parser.parse_args() setup_logging() TeslaGridBot(mode=args.mode).run() if __name__ == "__main__": main()