fix: add missing random import and ensure cleanup on thread crash
This commit is contained in:
@@ -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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user