fix: add early api connection verification to morning routine

This commit is contained in:
pie
2026-05-07 16:49:23 +01:00
parent f2180891fc
commit 9448c613ca
+33 -31
View File
@@ -4,6 +4,7 @@ import logging
import pytz import pytz
import threading import threading
import csv import csv
import random
from datetime import datetime, time as dtime from datetime import datetime, time as dtime
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -30,19 +31,20 @@ logger = logging.getLogger(__name__)
PNL_FILE = "pnl_tracking.csv" PNL_FILE = "pnl_tracking.csv"
def record_pnl(ticker, direction, entry_price, exit_price, reason, pnl_r): def record_pnl(ticker, direction, entry_price, exit_price, reason, pnl_r, trading_ticker=None):
"""Appends the result of a closed trade to the PnL CSV.""" """Appends the result of a closed trade to the PnL CSV."""
file_exists = os.path.isfile(PNL_FILE) file_exists = os.path.isfile(PNL_FILE)
with open(PNL_FILE, mode='a', newline='') as file: with open(PNL_FILE, mode='a', newline='') as file:
writer = csv.writer(file) writer = csv.writer(file)
if not file_exists: if not file_exists:
writer.writerow(["Date", "Ticker", "Direction", "Entry Price", "Exit Price", "Reason", "PnL (R)"]) writer.writerow(["Date", "Ticker", "Trading Ticker", "Direction", "Entry Price", "Exit Price", "Reason", "PnL (R)"])
today = datetime.now().strftime("%Y-%m-%d %H:%M:%S") today = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
writer.writerow([today, ticker, direction, round(entry_price, 2), round(exit_price, 2), reason, round(pnl_r, 2)]) writer.writerow([today, ticker, trading_ticker or ticker, direction, round(entry_price, 2), round(exit_price, 2), reason, round(pnl_r, 2)])
logger.info(f"Recorded trade in {PNL_FILE}: {ticker} {direction} | Result: {reason} | PnL: {pnl_r:.2f} R") label = f"{ticker} ({trading_ticker})" if trading_ticker else ticker
logger.info(f"Recorded trade in {PNL_FILE}: {label} {direction} | Result: {reason} | PnL: {pnl_r:.2f} R")
def calculate_r_multiple(direction, entry_price, exit_price, stop_loss): def calculate_r_multiple(direction, entry_price, exit_price, stop_loss):
"""Calculates the PnL in terms of Risk Multiples (R).""" """Calculates the PnL in terms of Risk Multiples (R)."""
@@ -91,7 +93,6 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
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
import random
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
@@ -99,15 +100,9 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
for attempt in range(3): for attempt in range(3):
try: try:
account_info = client.get_account_info() account_info = client.get_account_info()
# Trading212 returns totalValue for the account equity
actual_balance = float(account_info.get('totalValue', 5000.0)) actual_balance = float(account_info.get('totalValue', 5000.0))
# Simulate starting with 250 by subtracting the demo excess (4750)
virtual_balance = max(0, actual_balance - 4750.0) virtual_balance = max(0, actual_balance - 4750.0)
# Risk 1% of this adjusted virtual balance
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:
@@ -119,22 +114,26 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
break break
if execution.execute_trade(params, target_risk_amount=risk_amount): if execution.execute_trade(params, target_risk_amount=risk_amount):
# monitor_and_bracket is blocking, wait for fill (times out at 11:00)
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:
pnl_r = calculate_r_multiple(params['direction'], params['entry_price'], exit_price, params['stop_loss']) final_entry = execution.params.get('final_entry', params['entry_price'])
record_pnl(yf_ticker, params['direction'], params['entry_price'], exit_price, reason, pnl_r) final_sl = execution.params.get('final_sl', params['stop_loss'])
break # Exit the monitoring loop 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) time.sleep(15)
now = datetime.now(tz)
else: else:
logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.") logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.")
return return
# 2. Wait until 11:00 EST for Forced Exit (if we are still in a position or have pending orders) # 2. Wait until 11:00 EST for Forced Exit
now = datetime.now(tz) now = datetime.now(tz)
target_exit_time = now.replace(hour=11, minute=0, second=0, microsecond=0) target_exit_time = now.replace(hour=11, minute=0, second=0, microsecond=0)
@@ -143,18 +142,20 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
logger.info(f"Waiting {wait_seconds:.0f} seconds until 11:00 EST forced exit...") logger.info(f"Waiting {wait_seconds:.0f} seconds until 11:00 EST forced exit...")
time.sleep(wait_seconds) time.sleep(wait_seconds)
# 3. 11:00 EST - Cleanup (with jitter to prevent 429s from parallel threads) # 3. 11:00 EST - Cleanup (with jitter to prevent 429s)
import random
time.sleep(random.uniform(0.1, 5.0)) time.sleep(random.uniform(0.1, 5.0))
logger.info(f"Time exit reached for {yf_ticker}. Cleaning up.") logger.info(f"Time exit reached for {yf_ticker}. Cleaning up.")
if execution.is_in_position: if execution.is_in_position:
exit_price = execution.close_all(t212_ticker) exit_price = execution.close_all(t212_ticker)
if hasattr(execution, 'params') and exit_price > 0: if hasattr(execution, 'params') and exit_price > 0:
pnl_r = calculate_r_multiple(execution.params['direction'], execution.params['entry_price'], exit_price, execution.params['stop_loss']) final_entry = execution.params.get('final_entry', execution.params['entry_price'])
record_pnl(yf_ticker, execution.params['direction'], execution.params['entry_price'], exit_price, "11:00 Time Exit", pnl_r) 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: else:
# Cleanup any pending orders if entry wasn't filled
execution.close_all(t212_ticker) 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.")
@@ -168,12 +169,10 @@ def main():
now = datetime.now(tz) now = datetime.now(tz)
# Safety Guard: Check if it's a weekend if now.weekday() >= 5:
if now.weekday() >= 5: # 5 = Saturday, 6 = Sunday
logger.warning("Weekend detected. The market is closed. Exiting cleanly.") logger.warning("Weekend detected. The market is closed. Exiting cleanly.")
return return
# Safety Guard: Check if executed outside the expected morning window (allow 09:00 to 09:40 EST)
if now.hour < 9 or (now.hour == 9 and now.minute > 40) or now.hour >= 10: if now.hour < 9 or (now.hour == 9 and now.minute > 40) or now.hour >= 10:
logger.warning(f"Bot executed at {now.strftime('%H:%M')} EST. Expected launch window is 09:00 - 09:40 EST. Exiting cleanly.") logger.warning(f"Bot executed at {now.strftime('%H:%M')} EST. Expected launch window is 09:00 - 09:40 EST. Exiting cleanly.")
return return
@@ -184,7 +183,16 @@ def main():
client = Trading212Client(api_key_id, api_key, base_url) client = Trading212Client(api_key_id, api_key, base_url)
# 1. Morning Routine: Find Candidates # Early verification: Check connection before starting the day
try:
logger.info("Verifying API connection...")
client.get_account_info()
logger.info("API Connection verified successfully.")
except Exception as e:
logger.error(f"API Connection check failed: {e}")
logger.error("Please check your API key and permissions in .env. Exiting.")
return
logger.info("Starting Morning Routine: Finding ISA Candidates...") logger.info("Starting Morning Routine: Finding ISA Candidates...")
candidates_df = find_best_isa_tickers() candidates_df = find_best_isa_tickers()
@@ -192,11 +200,9 @@ def main():
logger.error("No candidates found. Exiting.") logger.error("No candidates found. Exiting.")
return return
# 2. Morning Routine: Backtest Candidates to find the 'Edge'
logger.info("Running Backtests on candidates to find current winners...") logger.info("Running Backtests on candidates to find current winners...")
profitable_tickers = [] profitable_tickers = []
# We'll test the top 10 candidates from the scanner
for _, row in candidates_df.head(10).iterrows(): for _, row in candidates_df.head(10).iterrows():
yf_t = row['Ticker'] yf_t = row['Ticker']
t212_t = row['T212_Ticker'] t212_t = row['T212_Ticker']
@@ -209,10 +215,7 @@ def main():
'pnl': res['Net PnL (R)'] 'pnl': res['Net PnL (R)']
}) })
# Sort by best backtest performance
profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True) profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True)
# Select Top 3
final_watchlist = profitable_tickers[:3] final_watchlist = profitable_tickers[:3]
if not final_watchlist: if not final_watchlist:
@@ -221,7 +224,6 @@ def main():
logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}") logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}")
# 3. Launch execution threads
threads = [] threads = []
for ticker_info in final_watchlist: for ticker_info in final_watchlist:
t = threading.Thread( t = threading.Thread(