Initial commit: Touch & Turn Scalping Bot with fully automated execution, backtesting, and ISA screening
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from src.api.client import Trading212Client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ExecutionManager:
|
||||
"""
|
||||
Manages the lifecycle of a trade: Entry, SL/TP placement, and Exit.
|
||||
"""
|
||||
def __init__(self, client: Trading212Client):
|
||||
self.client = client
|
||||
self.current_order_id = None
|
||||
self.sl_order_id = None
|
||||
self.tp_order_id = None
|
||||
self.is_in_position = False
|
||||
|
||||
def execute_trade(self, params: Dict[str, Any], target_risk_amount: float = 0.0):
|
||||
"""Starts the trade process by placing a limit entry order."""
|
||||
self.params = params
|
||||
ticker = params['ticker']
|
||||
direction = params['direction']
|
||||
entry_price = params['entry_price']
|
||||
stop_loss = params['stop_loss']
|
||||
|
||||
# Calculate Risk per share
|
||||
risk_per_share = abs(entry_price - stop_loss)
|
||||
|
||||
# Position Sizing
|
||||
if target_risk_amount > 0 and risk_per_share > 0:
|
||||
quantity = round(target_risk_amount / risk_per_share, 4) # T212 allows fractional shares
|
||||
# Enforce a minimum of 0.01 or whatever the broker allows, but we'll trust the math here
|
||||
if quantity < 0.01:
|
||||
quantity = 0.01
|
||||
else:
|
||||
quantity = 1.0 # Fallback
|
||||
|
||||
self.current_quantity = quantity # Save it so monitor_and_bracket can use it
|
||||
|
||||
# Quantity must be negative for Sell (Short)
|
||||
trade_quantity = -quantity if direction == "SELL" else quantity
|
||||
|
||||
logger.info(f"Calculated Risk/Share: {risk_per_share:.2f}. Sizing position to {quantity} shares to risk ~{target_risk_amount:.2f}")
|
||||
logger.info(f"Placing entry {direction} limit order for {ticker} at {entry_price:.2f}")
|
||||
|
||||
try:
|
||||
order = self.client.place_limit_order(ticker, trade_quantity, entry_price)
|
||||
self.current_order_id = order.get('id')
|
||||
logger.info(f"Order placed successfully. ID: {self.current_order_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place entry order: {e}")
|
||||
return False
|
||||
|
||||
def monitor_and_bracket(self, params: Dict[str, Any]):
|
||||
"""Polls the entry order and places SL/TP once filled."""
|
||||
if not self.current_order_id:
|
||||
return False
|
||||
|
||||
ticker = params['ticker']
|
||||
tp_price = params['target_price']
|
||||
sl_price = params['stop_loss']
|
||||
quantity = getattr(self, 'current_quantity', 1.0)
|
||||
|
||||
|
||||
# Wait for entry fill
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
tz = pytz.timezone('US/Eastern')
|
||||
|
||||
while not self.is_in_position:
|
||||
if datetime.now(tz).hour >= 11:
|
||||
logger.warning(f"11:00 EST reached without entry fill for {ticker}. Aborting.")
|
||||
return False
|
||||
|
||||
try:
|
||||
status_info = self.client.get_order_status(self.current_order_id)
|
||||
status = status_info.get('status')
|
||||
logger.info(f"Entry order {self.current_order_id} status: {status}")
|
||||
|
||||
if status == "FILLED":
|
||||
self.is_in_position = True
|
||||
logger.info(f"Entry order filled! Placing SL/TP.")
|
||||
break
|
||||
elif status in ["CANCELLED", "REJECTED"]:
|
||||
logger.warning(f"Entry order was {status}. Aborting.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking order status: {e}")
|
||||
|
||||
time.sleep(10) # Poll every 10 seconds
|
||||
|
||||
# Place SL and TP
|
||||
# SL is a Stop order in the opposite direction
|
||||
# TP is a Limit order in the opposite direction
|
||||
sl_qty = -quantity if params['direction'] == "BUY" else quantity
|
||||
tp_qty = -quantity if params['direction'] == "BUY" else quantity
|
||||
|
||||
try:
|
||||
# Place TP (Limit)
|
||||
tp_order = self.client.place_limit_order(ticker, tp_qty, tp_price, time_validity="GOOD_TILL_CANCEL")
|
||||
self.tp_order_id = tp_order.get('id')
|
||||
logger.info(f"TP order placed: {self.tp_order_id}")
|
||||
|
||||
# Place SL (Stop)
|
||||
sl_order = self.client.place_stop_order(ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL")
|
||||
self.sl_order_id = sl_order.get('id')
|
||||
logger.info(f"SL order placed: {self.sl_order_id}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place SL/TP orders: {e}")
|
||||
return False
|
||||
|
||||
def check_exit_status(self) -> tuple[bool, str, float]:
|
||||
"""Checks if the SL or TP orders have been filled by the broker."""
|
||||
if not self.is_in_position:
|
||||
return False, "", 0.0
|
||||
|
||||
try:
|
||||
if self.tp_order_id:
|
||||
tp_info = self.client.get_order_status(self.tp_order_id)
|
||||
if tp_info.get('status') == "FILLED":
|
||||
fill_price = tp_info.get('filledPrice', tp_info.get('limitPrice', self.params.get('target_price')))
|
||||
self.is_in_position = False
|
||||
return True, "TP Hit", float(fill_price)
|
||||
|
||||
if self.sl_order_id:
|
||||
sl_info = self.client.get_order_status(self.sl_order_id)
|
||||
if sl_info.get('status') == "FILLED":
|
||||
fill_price = sl_info.get('filledPrice', sl_info.get('stopPrice', self.params.get('stop_loss')))
|
||||
self.is_in_position = False
|
||||
return True, "SL Hit", float(fill_price)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking exit status: {e}")
|
||||
|
||||
return False, "", 0.0
|
||||
|
||||
def close_all(self, ticker: str) -> float:
|
||||
"""Forces a close of all open orders and positions. Returns the exit price (or 0.0)."""
|
||||
logger.info(f"Closing all orders and positions for {ticker}...")
|
||||
|
||||
if self.current_order_id:
|
||||
try: self.client.cancel_order(self.current_order_id)
|
||||
except: pass
|
||||
if self.sl_order_id:
|
||||
try: self.client.cancel_order(self.sl_order_id)
|
||||
except: pass
|
||||
if self.tp_order_id:
|
||||
try: self.client.cancel_order(self.tp_order_id)
|
||||
except: pass
|
||||
|
||||
logger.info("Emergency exit triggered. Cancelling pending orders...")
|
||||
|
||||
exit_price = 0.0
|
||||
# Flatten any active position
|
||||
if self.is_in_position:
|
||||
try:
|
||||
positions = self.client.get_all_open_positions()
|
||||
for pos in positions:
|
||||
if pos.get('ticker') == ticker:
|
||||
qty = float(pos.get('quantity', 0))
|
||||
exit_price = float(pos.get('currentPrice', 0.0)) # Use current market price as approx fill
|
||||
if qty != 0:
|
||||
# To close, we sell if we are long (positive qty), buy if short (negative qty)
|
||||
exit_qty = -qty
|
||||
logger.info(f"Flattening position: Placing market order for {exit_qty} shares of {ticker} at approx {exit_price}")
|
||||
self.client.place_market_order(ticker, exit_qty)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to flatten position during emergency exit: {e}")
|
||||
|
||||
self.is_in_position = False
|
||||
logger.info("Cleanup complete.")
|
||||
return exit_price
|
||||
|
||||
Reference in New Issue
Block a user