From 7a8f41c57f1e966de66229fac1334dbdcb932c0d Mon Sep 17 00:00:00 2001 From: pie Date: Fri, 3 Jul 2026 16:35:16 +0100 Subject: [PATCH] feat: port leaderboard visualization from proposals and maintain robust execution logic --- main.py | 100 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/main.py b/main.py index 2287f33..0fa544a 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ os.environ['PYTHONUNBUFFERED'] = '1' import time import logging +import logging.handlers import pytz import threading import csv @@ -19,16 +20,29 @@ from src.execution.manager import ExecutionManager from scripts.find_isa_candidates import find_best_isa_tickers from scripts.backtest import backtest_ticker +# Aggressive flush handler to ensure logs are physically written to disk +class HardFlushHandler(logging.handlers.WatchedFileHandler): + def emit(self, record): + super().emit(record) + # Flush the internal stream buffer + self.flush() + # Force the OS to write the bits to the physical drive + if self.stream and not self.stream.closed: + try: + os.fsync(self.stream.fileno()) + except (OSError, ValueError): + pass + # Ensure logs directory exists os.makedirs("logs", exist_ok=True) log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log") -# Simple, robust logging setup +# Robust logging setup logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s', handlers=[ - logging.FileHandler(log_filename, mode='a'), + HardFlushHandler(log_filename, mode='a'), logging.StreamHandler(sys.stdout) ] ) @@ -37,6 +51,11 @@ logger = logging.getLogger(__name__) def flush_logs(): for handler in logging.getLogger().handlers: handler.flush() + if hasattr(handler, 'stream') and handler.stream and not handler.stream.closed: + try: + os.fsync(handler.stream.fileno()) + except: + pass PNL_FILE = "pnl_tracking.csv" @@ -74,17 +93,13 @@ def calculate_r_multiple(direction, entry_price, exit_price, stop_loss): risk = stop_loss - entry_price return (entry_price - exit_price) / risk if risk != 0 else 0 -def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers): - """Handles the full strategy lifecycle for a single ticker in its own thread, then exits.""" +def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz): + """Handles the full strategy lifecycle for a single ticker.""" strategy = TouchTurnStrategy(yf_ticker) execution = ExecutionManager(client) logger.info(f"Bot thread started for {yf_ticker} ({t212_ticker}).") - # Initialize variables outside the retry loop to prevent UnboundLocalError - risk_share = 12.50 / num_tickers - capital_share = 250.0 / num_tickers - try: now = datetime.now(tz) target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0) @@ -116,22 +131,41 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers): params = strategy.get_trade_params() params['ticker'] = t212_ticker - # Anti-thundering-herd: Random jitter to prevent 429s from parallel threads - # Use a larger range (1-10s) to better stagger independent threads + # Check for ISA short restriction + isa_mode = os.getenv("ISA_MODE", "False").lower() == "true" + from src.strategy.inverse_mapping import INVERSE_TICKER_MAP + + can_trade = True + if isa_mode and params['direction'] == "SELL": + base_ticker = yf_ticker.split('_')[0] + if base_ticker not in INVERSE_TICKER_MAP: + logger.warning(f"ISA Mode: Bypassing {yf_ticker} Short (No ETP). Capital will be reallocated.") + can_trade = False + + if not can_trade: + return + + # Anti-thundering-herd jitter time.sleep(random.uniform(1.0, 10.0)) - # Fetch Account Balance to calculate risk with backoff + # Fetch Account Balance and DYNAMICALLY partition for attempt in range(3): try: account_info = client.get_account_info() actual_balance = float(account_info.get('totalValue', 5000.0)) virtual_balance = max(0, actual_balance - 4750.0) - # Risk 5% of this adjusted virtual balance - risk_share = (virtual_balance * 0.05) / num_tickers - capital_share = virtual_balance / num_tickers + # Count actively trading threads + num_active = 0 + for t in threading.enumerate(): + if t.name.startswith("Bot-") and t.is_alive(): + num_active += 1 - logger.info(f"Account: {actual_balance:.2f} | Virtual: {virtual_balance:.2f} | Share: {capital_share:.2f}") + num_active = max(1, num_active) # Safety + risk_share = (virtual_balance * 0.05) / num_active + capital_share = virtual_balance / num_active + + logger.info(f"Active Trades: {num_active} | Virtual: {virtual_balance:.2f} | Share: {capital_share:.2f} | Risk: {risk_share:.2f}") break except Exception as e: if '429' in str(e): @@ -233,7 +267,8 @@ def main(): logger.error("No candidates found. Exiting.") return - logger.info("Running Backtests on candidates to find current winners...") + logger.info("Running Backtests on top 10 candidates to find the current 'Edge'...") + all_results = [] profitable_tickers = [] for _, row in candidates_df.head(10).iterrows(): @@ -241,12 +276,30 @@ def main(): t212_t = row['T212_Ticker'] res = backtest_ticker(yf_t, quiet=True) - if res and res['Net PnL (R)'] > 0: - profitable_tickers.append({ - 'yf': yf_t, - 't212': t212_t, - 'pnl': res['Net PnL (R)'] - }) + if res: + all_results.append(res) + if res['Net PnL (R)'] > 0: + profitable_tickers.append({ + 'yf': yf_t, + 't212': t212_t, + 'pnl': res['Net PnL (R)'] + }) + + # Print Leaderboard for transparency + if all_results: + from prettytable import PrettyTable + results_df = pd.DataFrame(all_results) + results_df = results_df.sort_values(by="Net PnL (R)", ascending=False).reset_index(drop=True) + + print("\n" + "="*80) + print("🚀 MORNING BACKTEST LEADERBOARD (LAST ~60 DAYS) 🚀") + print("="*80) + table = PrettyTable() + table.field_names = results_df.columns + for _, r in results_df.iterrows(): + table.add_row(r.tolist()) + print(table) + print("\n") profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True) final_watchlist = profitable_tickers[:3] @@ -258,11 +311,10 @@ def main(): logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}") threads = [] - num_active = len(final_watchlist) for ticker_info in final_watchlist: t = threading.Thread( target=run_ticker_lifecycle, - args=(client, ticker_info['yf'], ticker_info['t212'], tz, num_active), + args=(client, ticker_info['yf'], ticker_info['t212'], tz), name=f"Bot-{ticker_info['yf']}" ) t.start()