feat: port leaderboard visualization from proposals and maintain robust execution logic
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user