fix: add missing random import and ensure cleanup on thread crash

This commit is contained in:
pie
2026-05-11 16:18:25 +01:00
parent 0f5d00e292
commit 5a4c99d0d1
2 changed files with 98 additions and 92 deletions
+97 -92
View File
@@ -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}).") logger.info(f"Bot thread started for {yf_ticker} ({t212_ticker}).")
now = datetime.now(tz) try:
target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0) 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}...")
# Retry loop: wait for yfinance to publish the 09:30-09:45 candle # 1. Wait until 09:45 EST
setup_found = False if now < target_entry_time:
max_retries = 12 wait_seconds = (target_entry_time - now).total_seconds()
for attempt in range(max_retries): logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...")
if strategy.check_setup(): time.sleep(wait_seconds)
setup_found = True
break # Re-evaluate current time
elif attempt < max_retries - 1: now = datetime.now(tz)
logger.debug(f"Data not ready for {yf_ticker} yet, waiting 15s...")
time.sleep(15) 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: # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads
params = strategy.get_trade_params() time.sleep(random.uniform(0.1, 3.0))
params['ticker'] = t212_ticker
# Fetch Account Balance to calculate risk with backoff
# Anti-thundering-herd: Random jitter to prevent 429s from parallel threads risk_amount = 2.50 # Fallback
time.sleep(random.uniform(0.1, 3.0)) for attempt in range(3):
try:
# Fetch Account Balance to calculate risk with backoff account_info = client.get_account_info()
risk_amount = 2.50 # Fallback actual_balance = float(account_info.get('totalValue', 5000.0))
for attempt in range(3): virtual_balance = max(0, actual_balance - 4750.0)
try: risk_amount = virtual_balance * 0.01
account_info = client.get_account_info() logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}")
actual_balance = float(account_info.get('totalValue', 5000.0)) break # Success
virtual_balance = max(0, actual_balance - 4750.0) except Exception as e:
risk_amount = virtual_balance * 0.01 if '429' in str(e):
logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}") logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...")
break # Success time.sleep(2**(attempt+1))
except Exception as e: else:
if '429' in str(e): logger.error(f"Failed to fetch account info: {e}. Defaulting to £2.50 risk.")
logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...") break
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.execute_trade(params, target_risk_amount=risk_amount):
if execution.monitor_and_bracket(params): if execution.monitor_and_bracket(params):
# Position is open, monitor for exit via SL/TP # Position is open, monitor for exit via SL/TP
while datetime.now(tz).hour < 11: while datetime.now(tz).hour < 11:
is_closed, reason, exit_price = execution.check_exit_status() is_closed, reason, exit_price = execution.check_exit_status()
if is_closed: if is_closed:
final_entry = execution.params.get('final_entry', params['entry_price']) final_entry = execution.params.get('final_entry', params['entry_price'])
final_sl = execution.params.get('final_sl', params['stop_loss']) final_sl = execution.params.get('final_sl', params['stop_loss'])
trading_ticker = execution.params.get('trading_ticker', yf_ticker) 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) 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) record_pnl(yf_ticker, params['direction'], final_entry, exit_price, reason, pnl_r, trading_ticker=trading_ticker)
break break
time.sleep(15) time.sleep(15)
now = datetime.now(tz) 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: else:
logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.") execution.close_all(t212_ticker)
return
# 2. Wait until 11:00 EST for Forced Exit logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.")
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.")
def main(): def main():
load_dotenv() load_dotenv()
+1
View File
@@ -1,6 +1,7 @@
import time import time
import logging import logging
import os import os
import random
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from src.api.client import Trading212Client from src.api.client import Trading212Client
from src.strategy.inverse_mapping import INVERSE_TICKER_MAP, LEVERAGE_MAP from src.strategy.inverse_mapping import INVERSE_TICKER_MAP, LEVERAGE_MAP