feat: port leaderboard visualization from proposals and maintain robust execution logic

This commit is contained in:
pie
2026-07-03 16:35:16 +01:00
parent 143cdcd976
commit 7a8f41c57f
+76 -24
View File
@@ -6,6 +6,7 @@ os.environ['PYTHONUNBUFFERED'] = '1'
import time import time
import logging import logging
import logging.handlers
import pytz import pytz
import threading import threading
import csv import csv
@@ -19,16 +20,29 @@ from src.execution.manager import ExecutionManager
from scripts.find_isa_candidates import find_best_isa_tickers from scripts.find_isa_candidates import find_best_isa_tickers
from scripts.backtest import backtest_ticker 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 # Ensure logs directory exists
os.makedirs("logs", exist_ok=True) os.makedirs("logs", exist_ok=True)
log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log") log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log")
# Simple, robust logging setup # Robust logging setup
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s', format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s',
handlers=[ handlers=[
logging.FileHandler(log_filename, mode='a'), HardFlushHandler(log_filename, mode='a'),
logging.StreamHandler(sys.stdout) logging.StreamHandler(sys.stdout)
] ]
) )
@@ -37,6 +51,11 @@ logger = logging.getLogger(__name__)
def flush_logs(): def flush_logs():
for handler in logging.getLogger().handlers: for handler in logging.getLogger().handlers:
handler.flush() 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" 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 risk = stop_loss - entry_price
return (entry_price - exit_price) / risk if risk != 0 else 0 return (entry_price - exit_price) / risk if risk != 0 else 0
def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz, num_tickers): def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
"""Handles the full strategy lifecycle for a single ticker in its own thread, then exits.""" """Handles the full strategy lifecycle for a single ticker."""
strategy = TouchTurnStrategy(yf_ticker) strategy = TouchTurnStrategy(yf_ticker)
execution = ExecutionManager(client) execution = ExecutionManager(client)
logger.info(f"Bot thread started for {yf_ticker} ({t212_ticker}).") 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: try:
now = datetime.now(tz) now = datetime.now(tz)
target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0) 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 = strategy.get_trade_params()
params['ticker'] = t212_ticker params['ticker'] = t212_ticker
# Anti-thundering-herd: Random jitter to prevent 429s from parallel threads # Check for ISA short restriction
# Use a larger range (1-10s) to better stagger independent threads 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)) 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): for attempt in range(3):
try: try:
account_info = client.get_account_info() account_info = client.get_account_info()
actual_balance = float(account_info.get('totalValue', 5000.0)) actual_balance = float(account_info.get('totalValue', 5000.0))
virtual_balance = max(0, actual_balance - 4750.0) virtual_balance = max(0, actual_balance - 4750.0)
# Risk 5% of this adjusted virtual balance # Count actively trading threads
risk_share = (virtual_balance * 0.05) / num_tickers num_active = 0
capital_share = virtual_balance / num_tickers 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 break
except Exception as e: except Exception as e:
if '429' in str(e): if '429' in str(e):
@@ -233,7 +267,8 @@ def main():
logger.error("No candidates found. Exiting.") logger.error("No candidates found. Exiting.")
return 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 = [] profitable_tickers = []
for _, row in candidates_df.head(10).iterrows(): for _, row in candidates_df.head(10).iterrows():
@@ -241,12 +276,30 @@ def main():
t212_t = row['T212_Ticker'] t212_t = row['T212_Ticker']
res = backtest_ticker(yf_t, quiet=True) res = backtest_ticker(yf_t, quiet=True)
if res and res['Net PnL (R)'] > 0: if res:
profitable_tickers.append({ all_results.append(res)
'yf': yf_t, if res['Net PnL (R)'] > 0:
't212': t212_t, profitable_tickers.append({
'pnl': res['Net PnL (R)'] '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) profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True)
final_watchlist = profitable_tickers[:3] 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]}") logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}")
threads = [] threads = []
num_active = len(final_watchlist)
for ticker_info in final_watchlist: for ticker_info in final_watchlist:
t = threading.Thread( t = threading.Thread(
target=run_ticker_lifecycle, 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']}" name=f"Bot-{ticker_info['yf']}"
) )
t.start() t.start()