From 9448c613ca19fdb64bafcb3cd52e039b0cdf5525 Mon Sep 17 00:00:00 2001 From: pie Date: Thu, 7 May 2026 16:49:23 +0100 Subject: [PATCH] fix: add early api connection verification to morning routine --- main.py | 64 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/main.py b/main.py index c8d1e38..5488b35 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import logging import pytz import threading import csv +import random from datetime import datetime, time as dtime from dotenv import load_dotenv @@ -30,19 +31,20 @@ logger = logging.getLogger(__name__) 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.""" file_exists = os.path.isfile(PNL_FILE) with open(PNL_FILE, mode='a', newline='') as file: writer = csv.writer(file) 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") - 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): """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 # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads - import random time.sleep(random.uniform(0.1, 3.0)) # 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): try: account_info = client.get_account_info() - # Trading212 returns totalValue for the account equity 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) - - # Risk 1% of this adjusted virtual balance 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: @@ -119,22 +114,26 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz): break 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): # 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: - pnl_r = calculate_r_multiple(params['direction'], params['entry_price'], exit_price, params['stop_loss']) - record_pnl(yf_ticker, params['direction'], params['entry_price'], exit_price, reason, pnl_r) - break # Exit the monitoring loop + 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 (if we are still in a position or have pending orders) + # 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) @@ -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...") time.sleep(wait_seconds) - # 3. 11:00 EST - Cleanup (with jitter to prevent 429s from parallel threads) - import random + # 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: - pnl_r = calculate_r_multiple(execution.params['direction'], execution.params['entry_price'], exit_price, execution.params['stop_loss']) - record_pnl(yf_ticker, execution.params['direction'], execution.params['entry_price'], exit_price, "11:00 Time Exit", pnl_r) + 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: - # Cleanup any pending orders if entry wasn't filled execution.close_all(t212_ticker) logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.") @@ -168,12 +169,10 @@ def main(): now = datetime.now(tz) - # Safety Guard: Check if it's a weekend - if now.weekday() >= 5: # 5 = Saturday, 6 = Sunday + if now.weekday() >= 5: logger.warning("Weekend detected. The market is closed. Exiting cleanly.") 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: logger.warning(f"Bot executed at {now.strftime('%H:%M')} EST. Expected launch window is 09:00 - 09:40 EST. Exiting cleanly.") return @@ -184,7 +183,16 @@ def main(): 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...") candidates_df = find_best_isa_tickers() @@ -192,11 +200,9 @@ def main(): logger.error("No candidates found. Exiting.") return - # 2. Morning Routine: Backtest Candidates to find the 'Edge' logger.info("Running Backtests on candidates to find current winners...") profitable_tickers = [] - # We'll test the top 10 candidates from the scanner for _, row in candidates_df.head(10).iterrows(): yf_t = row['Ticker'] t212_t = row['T212_Ticker'] @@ -209,10 +215,7 @@ def main(): 'pnl': res['Net PnL (R)'] }) - # Sort by best backtest performance profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True) - - # Select Top 3 final_watchlist = profitable_tickers[:3] if not final_watchlist: @@ -221,7 +224,6 @@ def main(): logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}") - # 3. Launch execution threads threads = [] for ticker_info in final_watchlist: t = threading.Thread(