diff --git a/main.py b/main.py index 5488b35..37af8a3 100644 --- a/main.py +++ b/main.py @@ -62,103 +62,108 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz): logger.info(f"Bot thread started for {yf_ticker} ({t212_ticker}).") - now = datetime.now(tz) - target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0) - - # 1. Wait until 09:45 EST - if now < target_entry_time: - wait_seconds = (target_entry_time - now).total_seconds() - logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...") - time.sleep(wait_seconds) - - # Re-evaluate current time - now = datetime.now(tz) - - if now.hour == 9 and now.minute >= 45: - logger.info(f"Evaluating opening candle for {yf_ticker}...") + try: + now = datetime.now(tz) + target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0) - # Retry loop: wait for yfinance to publish the 09:30-09:45 candle - setup_found = False - max_retries = 12 - for attempt in range(max_retries): - if strategy.check_setup(): - setup_found = True - break - elif attempt < max_retries - 1: - logger.debug(f"Data not ready for {yf_ticker} yet, waiting 15s...") - time.sleep(15) + # 1. Wait until 09:45 EST + if now < target_entry_time: + wait_seconds = (target_entry_time - now).total_seconds() + logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...") + time.sleep(wait_seconds) + + # Re-evaluate current time + now = datetime.now(tz) + + if now.hour == 9 and now.minute >= 45: + logger.info(f"Evaluating opening candle for {yf_ticker}...") + + # Retry loop: wait for yfinance to publish the 09:30-09:45 candle + setup_found = False + max_retries = 12 + for attempt in range(max_retries): + if strategy.check_setup(): + setup_found = True + break + elif attempt < max_retries - 1: + logger.debug(f"Data not ready for {yf_ticker} yet, waiting 15s...") + time.sleep(15) + + if setup_found: + params = strategy.get_trade_params() + params['ticker'] = t212_ticker - if setup_found: - params = strategy.get_trade_params() - params['ticker'] = t212_ticker - - # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads - time.sleep(random.uniform(0.1, 3.0)) - - # Fetch Account Balance to calculate risk with backoff - risk_amount = 2.50 # Fallback - for attempt in range(3): - try: - account_info = client.get_account_info() - actual_balance = float(account_info.get('totalValue', 5000.0)) - virtual_balance = max(0, actual_balance - 4750.0) - risk_amount = virtual_balance * 0.01 - logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}") - break # Success - except Exception as e: - if '429' in str(e): - logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...") - time.sleep(2**(attempt+1)) - else: - logger.error(f"Failed to fetch account info: {e}. Defaulting to £2.50 risk.") - break + # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads + time.sleep(random.uniform(0.1, 3.0)) + + # Fetch Account Balance to calculate risk with backoff + risk_amount = 2.50 # Fallback + for attempt in range(3): + try: + account_info = client.get_account_info() + actual_balance = float(account_info.get('totalValue', 5000.0)) + virtual_balance = max(0, actual_balance - 4750.0) + risk_amount = virtual_balance * 0.01 + logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}") + break # Success + except Exception as e: + if '429' in str(e): + logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...") + time.sleep(2**(attempt+1)) + else: + logger.error(f"Failed to fetch account info: {e}. Defaulting to £2.50 risk.") + break - if execution.execute_trade(params, target_risk_amount=risk_amount): - if execution.monitor_and_bracket(params): - # Position is open, monitor for exit via SL/TP - while datetime.now(tz).hour < 11: - is_closed, reason, exit_price = execution.check_exit_status() - if is_closed: - final_entry = execution.params.get('final_entry', params['entry_price']) - final_sl = execution.params.get('final_sl', params['stop_loss']) - trading_ticker = execution.params.get('trading_ticker', yf_ticker) - - pnl_r = calculate_r_multiple("BUY" if execution.is_etp else params['direction'], final_entry, exit_price, final_sl) - record_pnl(yf_ticker, params['direction'], final_entry, exit_price, reason, pnl_r, trading_ticker=trading_ticker) - break - - time.sleep(15) - now = datetime.now(tz) + if execution.execute_trade(params, target_risk_amount=risk_amount): + if execution.monitor_and_bracket(params): + # Position is open, monitor for exit via SL/TP + while datetime.now(tz).hour < 11: + is_closed, reason, exit_price = execution.check_exit_status() + if is_closed: + final_entry = execution.params.get('final_entry', params['entry_price']) + final_sl = execution.params.get('final_sl', params['stop_loss']) + trading_ticker = execution.params.get('trading_ticker', yf_ticker) + + pnl_r = calculate_r_multiple("BUY" if execution.is_etp else params['direction'], final_entry, exit_price, final_sl) + record_pnl(yf_ticker, params['direction'], final_entry, exit_price, reason, pnl_r, trading_ticker=trading_ticker) + break + + time.sleep(15) + now = datetime.now(tz) + else: + logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.") + return + + # 2. Wait until 11:00 EST for Forced Exit + now = datetime.now(tz) + target_exit_time = now.replace(hour=11, minute=0, second=0, microsecond=0) + + if now < target_exit_time and execution.is_in_position: + wait_seconds = (target_exit_time - now).total_seconds() + logger.info(f"Waiting {wait_seconds:.0f} seconds until 11:00 EST forced exit...") + time.sleep(wait_seconds) + + except Exception as e: + logger.error(f"Unexpected error in {yf_ticker} lifecycle: {e}", exc_info=True) + finally: + # 3. 11:00 EST - Cleanup (with jitter to prevent 429s) + # We put this in finally to ensure it runs even on crash + time.sleep(random.uniform(0.1, 5.0)) + + logger.info(f"Cleanup phase reached for {yf_ticker}.") + if execution.is_in_position: + exit_price = execution.close_all(t212_ticker) + if hasattr(execution, 'params') and exit_price > 0: + final_entry = execution.params.get('final_entry', execution.params['entry_price']) + final_sl = execution.params.get('final_sl', execution.params['stop_loss']) + trading_ticker = execution.params.get('trading_ticker', yf_ticker) + + pnl_r = calculate_r_multiple("BUY" if execution.is_etp else execution.params['direction'], final_entry, exit_price, final_sl) + record_pnl(yf_ticker, execution.params['direction'], final_entry, exit_price, "Forced Exit (Final)", pnl_r, trading_ticker=trading_ticker) else: - logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.") - return + execution.close_all(t212_ticker) - # 2. Wait until 11:00 EST for Forced Exit - now = datetime.now(tz) - target_exit_time = now.replace(hour=11, minute=0, second=0, microsecond=0) - - if now < target_exit_time and execution.is_in_position: - wait_seconds = (target_exit_time - now).total_seconds() - logger.info(f"Waiting {wait_seconds:.0f} seconds until 11:00 EST forced exit...") - time.sleep(wait_seconds) - - # 3. 11:00 EST - Cleanup (with jitter to prevent 429s) - time.sleep(random.uniform(0.1, 5.0)) - - logger.info(f"Time exit reached for {yf_ticker}. Cleaning up.") - if execution.is_in_position: - exit_price = execution.close_all(t212_ticker) - if hasattr(execution, 'params') and exit_price > 0: - final_entry = execution.params.get('final_entry', execution.params['entry_price']) - final_sl = execution.params.get('final_sl', execution.params['stop_loss']) - trading_ticker = execution.params.get('trading_ticker', yf_ticker) - - pnl_r = calculate_r_multiple("BUY" if execution.is_etp else execution.params['direction'], final_entry, exit_price, final_sl) - record_pnl(yf_ticker, execution.params['direction'], final_entry, exit_price, "11:00 Time Exit", pnl_r, trading_ticker=trading_ticker) - else: - execution.close_all(t212_ticker) - - logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.") + logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.") def main(): load_dotenv() diff --git a/src/execution/manager.py b/src/execution/manager.py index 6fb6851..7f3b325 100644 --- a/src/execution/manager.py +++ b/src/execution/manager.py @@ -1,6 +1,7 @@ import time import logging import os +import random from typing import Dict, Any, Optional from src.api.client import Trading212Client from src.strategy.inverse_mapping import INVERSE_TICKER_MAP, LEVERAGE_MAP