From ede9933c88a3a2222034aa0e4a638a52ed378244 Mon Sep 17 00:00:00 2001 From: pie Date: Fri, 1 May 2026 15:18:33 +0100 Subject: [PATCH] fix: resolve 429 rate limit errors with API backoff and correct timezone handling for 09:30 candle --- main.py | 39 ++++++++++++++++++++++++------------- scripts/verify_last_week.py | 8 ++++++++ src/strategy/touch_turn.py | 6 ++++++ test_order.py | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 scripts/verify_last_week.py create mode 100644 test_order.py diff --git a/main.py b/main.py index 14b29b8..ef06938 100644 --- a/main.py +++ b/main.py @@ -75,7 +75,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, 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 (up to 3 minutes) + # 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): @@ -90,19 +90,30 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz): params = strategy.get_trade_params() params['ticker'] = t212_ticker - # Fetch Account Balance to calculate risk - try: - account_info = client.get_account_info() - virtual_balance = float(os.getenv("VIRTUAL_STARTING_BALANCE", 0)) - - if virtual_balance > 0: - risk_amount = virtual_balance * 0.01 - else: - available_cash = account_info.get('cash', {}).get('availableToTrade', 1000) - risk_amount = available_cash * 0.01 - except Exception as e: - logger.error(f"Failed to fetch account info for risk calculation: {e}. Defaulting to £2.50 risk.") - risk_amount = 2.50 + # 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 + risk_amount = 2.50 # Fallback + for attempt in range(3): + try: + account_info = client.get_account_info() + virtual_balance = float(os.getenv("VIRTUAL_STARTING_BALANCE", 0)) + + if virtual_balance > 0: + risk_amount = virtual_balance * 0.01 + else: + available_cash = account_info.get('cash', {}).get('availableToTrade', 1000) + risk_amount = available_cash * 0.01 + break # Success + except Exception as e: + if '429' in str(e): + logger.warning(f"Rate limited on account fetch for {yf_ticker}. Retrying in {2**(attempt+1)}s...") + 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): # monitor_and_bracket is blocking, wait for fill (times out at 11:00) diff --git a/scripts/verify_last_week.py b/scripts/verify_last_week.py new file mode 100644 index 0000000..1d76ddf --- /dev/null +++ b/scripts/verify_last_week.py @@ -0,0 +1,8 @@ +import pandas as pd +import yfinance as yf +from scripts.backtest import backtest_ticker + +print("Running historical check for the last 5 trading days...") +for ticker in ["NFLX", "AMZN", "AAPL"]: + res = backtest_ticker(ticker, days=5, quiet=False) + print("="*60) diff --git a/src/strategy/touch_turn.py b/src/strategy/touch_turn.py index 4b2aad1..9e7e1f1 100644 --- a/src/strategy/touch_turn.py +++ b/src/strategy/touch_turn.py @@ -59,6 +59,12 @@ class TouchTurnStrategy: if isinstance(intraday_data.columns, pd.MultiIndex): intraday_data.columns = intraday_data.columns.droplevel(1) + # Timezone Correction: Convert the index to Eastern Time before filtering + if intraday_data.index.tz is None: + intraday_data.index = intraday_data.index.tz_localize('UTC').tz_convert(self.tz) + else: + intraday_data.index = intraday_data.index.tz_convert(self.tz) + # The first candle of the session (09:30) opening_candle = intraday_data.between_time('09:30', '09:30') if opening_candle.empty: diff --git a/test_order.py b/test_order.py new file mode 100644 index 0000000..beb25ad --- /dev/null +++ b/test_order.py @@ -0,0 +1,36 @@ +import os +import logging +from dotenv import load_dotenv +from src.api.client import Trading212Client + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def test_order(): + load_dotenv() + api_key_id = os.getenv("TRADING212_API_KEY_ID") + api_key = os.getenv("TRADING212_API_KEY") + base_url = os.getenv("TRADING212_BASE_URL", "https://demo.trading212.com/api/v0/") + + client = Trading212Client(api_key_id, api_key, base_url) + + logger.info("Attempting to place a test Limit Order (Buy 0.1 AAPL @ $1.00)...") + try: + # We place a limit order far away from the current price so it won't fill + response = client.place_limit_order("AAPL_US_EQ", 0.1, 1.00) + logger.info(f"Success! Response: {response}") + + # If successful, immediately cancel it + order_id = response.get('id') + if order_id: + logger.info(f"Cancelling test order {order_id}...") + client.cancel_order(order_id) + logger.info("Cancelled.") + except Exception as e: + logger.error(f"Failed: {e}") + # If it's a requests HTTPError, let's print the raw response text for debugging + if hasattr(e, 'response') and e.response is not None: + logger.error(f"API Response Body: {e.response.text}") + +if __name__ == "__main__": + test_order() \ No newline at end of file