feat: implement Split-Account Mode for ISA/CFD hybrid execution
This commit is contained in:
@@ -93,10 +93,9 @@ 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):
|
||||
def run_ticker_lifecycle(isa_client, cfd_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}).")
|
||||
|
||||
@@ -131,19 +130,27 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
params = strategy.get_trade_params()
|
||||
params['ticker'] = t212_ticker
|
||||
|
||||
# Check for ISA short restriction
|
||||
# Split-Account Routing Logic
|
||||
split_mode = os.getenv("SPLIT_ACCOUNT_MODE", "False").lower() == "true"
|
||||
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
|
||||
client = isa_client
|
||||
use_isa_rules = True
|
||||
|
||||
if not can_trade:
|
||||
return
|
||||
if params['direction'] == "SELL":
|
||||
if split_mode and cfd_client:
|
||||
logger.info(f"Split-Account Mode: Routing SHORT trade for {yf_ticker} to CFD account.")
|
||||
client = cfd_client
|
||||
use_isa_rules = False # Disable Inverse ETP mapping for CFD
|
||||
elif isa_mode:
|
||||
# Standard ISA mode check
|
||||
from src.strategy.inverse_mapping import INVERSE_TICKER_MAP
|
||||
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).")
|
||||
return
|
||||
|
||||
execution = ExecutionManager(client)
|
||||
|
||||
# Anti-thundering-herd jitter
|
||||
time.sleep(random.uniform(1.0, 10.0))
|
||||
@@ -155,7 +162,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
actual_balance = float(account_info.get('totalValue', 5000.0))
|
||||
virtual_balance = max(0, actual_balance - 4750.0)
|
||||
|
||||
# Count actively trading threads
|
||||
# Count actively trading threads on THIS account
|
||||
num_active = 0
|
||||
for t in threading.enumerate():
|
||||
if t.name.startswith("Bot-") and t.is_alive():
|
||||
@@ -165,7 +172,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
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}")
|
||||
logger.info(f"Account ({'ISA' if use_isa_rules else 'CFD'}): Active Trades: {num_active} | Virtual: {virtual_balance:.2f} | Risk: {risk_share:.2f}")
|
||||
break
|
||||
except Exception as e:
|
||||
if '429' in str(e):
|
||||
@@ -176,7 +183,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
logger.error(f"Failed to fetch account info: {e}")
|
||||
break
|
||||
|
||||
if execution.execute_trade(params, target_risk_amount=risk_share, max_capital=capital_share):
|
||||
if execution.execute_trade(params, target_risk_amount=risk_share, max_capital=capital_share, isa_rules=use_isa_rules):
|
||||
if execution.monitor_and_bracket(params):
|
||||
# Position is open, monitor for exit via SL/TP
|
||||
while datetime.now(tz).hour < 11:
|
||||
@@ -212,7 +219,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
time.sleep(random.uniform(0.1, 5.0))
|
||||
|
||||
logger.info(f"Cleanup phase reached for {yf_ticker}.")
|
||||
if execution.is_in_position:
|
||||
if execution and execution.is_in_position:
|
||||
exit_price = execution.close_all(t212_ticker)
|
||||
if hasattr(execution, 'params') and exit_price > 0:
|
||||
final_entry = execution.params.get('final_entry', execution.params['entry_price'])
|
||||
@@ -221,7 +228,7 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
|
||||
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, "Forced Exit (Final)", pnl_r, trading_ticker=trading_ticker)
|
||||
else:
|
||||
elif execution:
|
||||
execution.close_all(t212_ticker)
|
||||
|
||||
logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.")
|
||||
@@ -229,11 +236,18 @@ def run_ticker_lifecycle(client, yf_ticker, t212_ticker, tz):
|
||||
|
||||
def main():
|
||||
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/")
|
||||
tz = pytz.timezone('US/Eastern')
|
||||
|
||||
# Primary Account (ISA)
|
||||
isa_key_id = os.getenv("TRADING212_API_KEY_ID")
|
||||
isa_key = os.getenv("TRADING212_API_KEY")
|
||||
isa_url = os.getenv("TRADING212_BASE_URL", "https://demo.trading212.com/api/v0/")
|
||||
|
||||
# Secondary Account (CFD)
|
||||
cfd_key_id = os.getenv("CFD_API_KEY_ID")
|
||||
cfd_key = os.getenv("CFD_API_KEY")
|
||||
cfd_url = os.getenv("CFD_BASE_URL", "https://demo.trading212.com/api/v0/")
|
||||
|
||||
tz = pytz.timezone('US/Eastern')
|
||||
now = datetime.now(tz)
|
||||
|
||||
if now.weekday() >= 5:
|
||||
@@ -244,20 +258,25 @@ def main():
|
||||
logger.warning(f"Bot executed at {now.strftime('%H:%M')} EST. Expected launch window is 09:00 - 09:40 EST. Exiting cleanly.")
|
||||
return
|
||||
|
||||
if not api_key_id or not api_key:
|
||||
logger.error("API credentials not found in .env")
|
||||
if not isa_key_id or not isa_key:
|
||||
logger.error("Primary API credentials not found in .env")
|
||||
return
|
||||
|
||||
client = Trading212Client(api_key_id, api_key, base_url)
|
||||
isa_client = Trading212Client(isa_key_id, isa_key, isa_url)
|
||||
cfd_client = None
|
||||
if cfd_key_id and cfd_key:
|
||||
cfd_client = Trading212Client(cfd_key_id, cfd_key, cfd_url)
|
||||
|
||||
# Early verification: Check connection before starting the day
|
||||
# Early verification
|
||||
try:
|
||||
logger.info("Verifying API connection...")
|
||||
client.get_account_info()
|
||||
logger.info("API Connection verified successfully.")
|
||||
logger.info("Verifying Primary API connection...")
|
||||
isa_client.get_account_info()
|
||||
if cfd_client:
|
||||
logger.info("Verifying Secondary API connection...")
|
||||
cfd_client.get_account_info()
|
||||
logger.info("API Connections 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...")
|
||||
@@ -285,7 +304,6 @@ def main():
|
||||
'pnl': res['Net PnL (R)']
|
||||
})
|
||||
|
||||
# Print Leaderboard for transparency
|
||||
if all_results:
|
||||
from prettytable import PrettyTable
|
||||
results_df = pd.DataFrame(all_results)
|
||||
@@ -314,7 +332,7 @@ def main():
|
||||
for ticker_info in final_watchlist:
|
||||
t = threading.Thread(
|
||||
target=run_ticker_lifecycle,
|
||||
args=(client, ticker_info['yf'], ticker_info['t212'], tz),
|
||||
args=(isa_client, cfd_client, ticker_info['yf'], ticker_info['t212'], tz),
|
||||
name=f"Bot-{ticker_info['yf']}"
|
||||
)
|
||||
t.start()
|
||||
|
||||
Reference in New Issue
Block a user