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
+91 -86
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 # 1. Wait until 09:45 EST
if now < target_entry_time: if now < target_entry_time:
wait_seconds = (target_entry_time - now).total_seconds() wait_seconds = (target_entry_time - now).total_seconds()
logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...") logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...")
time.sleep(wait_seconds) time.sleep(wait_seconds)
# Re-evaluate current time # Re-evaluate current time
now = datetime.now(tz) now = datetime.now(tz)
if now.hour == 9 and now.minute >= 45: if now.hour == 9 and now.minute >= 45:
logger.info(f"Evaluating opening candle for {yf_ticker}...") logger.info(f"Evaluating opening candle for {yf_ticker}...")
# Retry loop: wait for yfinance to publish the 09:30-09:45 candle # Retry loop: wait for yfinance to publish the 09:30-09:45 candle
setup_found = False setup_found = False
max_retries = 12 max_retries = 12
for attempt in range(max_retries): for attempt in range(max_retries):
if strategy.check_setup(): if strategy.check_setup():
setup_found = True setup_found = True
break break
elif attempt < max_retries - 1: elif attempt < max_retries - 1:
logger.debug(f"Data not ready for {yf_ticker} yet, waiting 15s...") logger.debug(f"Data not ready for {yf_ticker} yet, waiting 15s...")
time.sleep(15) time.sleep(15)
if setup_found: if setup_found:
params = strategy.get_trade_params() params = strategy.get_trade_params()
params['ticker'] = t212_ticker params['ticker'] = t212_ticker
# Anti-thundering-herd: Random jitter to prevent 429s from parallel threads # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads
time.sleep(random.uniform(0.1, 3.0)) time.sleep(random.uniform(0.1, 3.0))
# Fetch Account Balance to calculate risk with backoff # Fetch Account Balance to calculate risk with backoff
risk_amount = 2.50 # Fallback risk_amount = 2.50 # Fallback
for attempt in range(3): for attempt in range(3):
try: try:
account_info = client.get_account_info() account_info = client.get_account_info()
actual_balance = float(account_info.get('totalValue', 5000.0)) actual_balance = float(account_info.get('totalValue', 5000.0))
virtual_balance = max(0, actual_balance - 4750.0) virtual_balance = max(0, actual_balance - 4750.0)
risk_amount = virtual_balance * 0.01 risk_amount = virtual_balance * 0.01
logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}") logger.info(f"Account: {actual_balance:.2f} | Virtual Balance: {virtual_balance:.2f} | Risk (1%): {risk_amount:.2f}")
break # Success break # Success
except Exception as e: except Exception as e:
if '429' in str(e): if '429' in str(e):
logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...") logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...")
time.sleep(2**(attempt+1)) time.sleep(2**(attempt+1))
else: else:
logger.error(f"Failed to fetch account info: {e}. Defaulting to £2.50 risk.") 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 break
time.sleep(15) if execution.execute_trade(params, target_risk_amount=risk_amount):
now = datetime.now(tz) 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: 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