fix: add early api connection verification to morning routine
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user