feat: implement Split-Account Mode for ISA/CFD hybrid execution

This commit is contained in:
pie
2026-07-03 16:52:10 +01:00
parent 7a8f41c57f
commit 41c81bb864
5 changed files with 137 additions and 118 deletions
+12 -2
View File
@@ -1,8 +1,18 @@
TRADING212_API_KEY_ID=your_practice_api_key_id_here
TRADING212_API_KEY=your_practice_api_key_here
# Primary Account (ISA - for LONG trades)
TRADING212_API_KEY_ID=your_isa_api_key_id_here
TRADING212_API_KEY=your_isa_api_key_here
TRADING212_BASE_URL=https://demo.trading212.com/api/v0/
# Optional: Secondary Account (CFD - for SHORT trades)
# If provided, SPLIT_ACCOUNT_MODE will allow direct shorting of any stock.
CFD_API_KEY_ID=your_cfd_api_key_id_here
CFD_API_KEY=your_cfd_api_key_here
CFD_BASE_URL=https://demo.trading212.com/api/v0/
SPLIT_ACCOUNT_MODE=True
# Optional: Override the demo account's large starting balance (e.g. 5000)
# with a smaller amount to keep position sizing realistic for your future live account.
VIRTUAL_STARTING_BALANCE=250
ISA_MODE=True
+14 -12
View File
@@ -1,6 +1,6 @@
# Trading212 Python Scalping Bot - "Touch & Turn" (Opening Range Reversal)
This project implements the "Touch & Turn" scalping strategy for the Trading212 API, optimized for the UK ISA environment.
This project implements the "Touch & Turn" scalping strategy for the Trading212 API, optimized for the UK ISA environment with optional CFD integration for shorting.
## Strategy Logic (The Workflow)
@@ -8,39 +8,41 @@ This project implements the "Touch & Turn" scalping strategy for the Trading212
2. **Filter for Liquidity:** Opening range must be >= 25% of 14-day ATR.
3. **Determine Direction:**
- Bearish (Close < Open): Prepare **LONG** (Buy at Low).
- Bullish (Close > Open): Prepare **SHORT** (Substitute with **3x Inverse ETP BUY** in ISA).
- Bullish (Close > Open): Prepare **SHORT** (Sell at High).
4. **Execution (09:45 EST):**
- Entry via **Market Order** for immediate fill.
- **Actual Fill Price** fetched from portfolio is used for all bracket calculations.
5. **Hybrid Exit Strategy:**
5. **Split-Account Routing (ISA / CFD):**
- **ISA Account:** Used for all **LONG** trades and **SHORT** trades where an Inverse ETP is available.
- **CFD Account:** Used for **SHORT** trades on stocks without Inverse ETPs (requires `SPLIT_ACCOUNT_MODE=True`).
6. **Hybrid Exit Strategy:**
- **Broker-Side:** Physical **Stop Loss** order placed immediately for protection.
- **Bot-Side:** **Take Profit** monitored manually by polling current market price.
- This bypasses ISA restrictions against multiple pending sell orders for the same shares.
6. **Automatic Exit (11:00 EST):** Force close via Market Order and cleanup pending SL.
7. **Automatic Exit (11:00 EST):** Force close via Market Order and cleanup pending SL.
## Risk & Capital Management
* **Virtual Balance Simulation:** In demo mode, subtracts £4,750 from total equity to simulate a realistic £250 starting point.
* **5% Risk Rule:** Risks exactly 5% of the Virtual Balance per trade.
* **Capital Partitioning:** Divides total available capital (£250) and risk budget equally among all active ticker threads for the day (max 3).
* **Capital Partitioning:** Divides total available capital (£250) and risk budget equally among all active ticker threads for the day (max 3) per account.
* **Precision & Minimums:** Automatically detects "precision-mismatch" or "min-quantity-exceeded" errors from T212 and retries with corrected values.
## Technical Architecture
* **`main.py`:** Daily orchestrator. Scan -> Backtest -> Select Top 3 -> Spawn Parallel Threads. Handles early API verification and unbuffered logging.
* **`main.py`:** Daily orchestrator. Handles dual-account initialization, trade routing, and parallel thread management.
* **`src/api/client.py`:** REST wrapper with Basic Auth.
* **`src/strategy/touch_turn.py`:** Setup logic, Fibonacci calculation, and timezone conversion (UTC -> Eastern).
* **`src/execution/manager.py`:** Handles ticker swapping (Inverse ETPs), market entries, hybrid brackets, and retry loops with jitter.
* **`src/strategy/inverse_mapping.py`:** Map of US stocks to 3x Short Inverse ETPs (GraniteShares/Leverage Shares).
* **`src/strategy/touch_turn.py`:** Setup logic, Fibonacci calculation, and ATR-based SL padding.
* **`src/execution/manager.py`:** Handles ticker swapping (Inverse ETPs), market entries, hybrid brackets, and retry loops.
* **`src/strategy/inverse_mapping.py`:** Map of US stocks to 3x Short Inverse ETPs for ISA shorting.
## Resilience Features
* **API Backoff:** Random jitter (1-10s) and exponential retry on 429 errors.
* **Order Tracking:** Uses portfolio checks to infer status if order IDs disappear (404).
* **Unbuffered Logging:** Force-flushes logs to `logs/bot_*.log` immediately for real-time monitoring.
* **Aggressive Logging:** Custom `HardFlushHandler` uses `os.fsync()` to ensure real-time log writes to disk.
## Operation
1. **Timer:** Service managed by `systemd` timer firing at 09:30 America/New_York.
2. **Tracking:** P&L recorded in `pnl_tracking.csv` (R-multiple based).
2. **Tracking:** P&L recorded in `pnl_tracking.csv`.
3. **Verification:** Always run `./venv/bin/python3 test_api_connection.py` before live days.
+42 -36
View File
@@ -1,6 +1,6 @@
# Trading212 "Touch & Turn" Scalping Bot
This project implements the "Touch & Turn" scalping strategy (Opening Range Liquidity Reversal) in Python for the Trading212 API. It is specifically designed to trade US Equities at the 09:30 EST market open.
This project implements the "Touch & Turn" scalping strategy (Opening Range Liquidity Reversal) in Python for the Trading212 API. It is optimized for UK traders using ISA and optional CFD accounts.
## ⚠️ Disclaimer
**This software is for educational purposes only.** Trading in financial markets involves a high degree of risk. Always use the practice/demo environment (`demo.trading212.com`) to test strategies before using real money.
@@ -9,72 +9,78 @@ This project implements the "Touch & Turn" scalping strategy (Opening Range Liqu
## Strategy Overview
The strategy capitalizes on the initial liquidity and volatility of the US market open.
The strategy capitalizes on the initial liquidity and volatility of the US market open (09:30 EST).
1. **The Setup:** Captures the high and low of the first 15-minute candle (09:30 - 09:45 EST).
2. **The Filter:** The range of this opening candle must be at least **25%** of the stock's 14-day Average True Range (ATR).
3. **The Trigger (ISA Optimized):**
- **LONG (Bearish candle):** Bot places an immediate **Market BUY** order for the stock.
- **SHORT (Bullish candle):** Since standard shorting is restricted in UK ISAs, the bot automatically substitutes this with a **Market BUY** order for a **3x Inverse ETP** (e.g., buying `3SLA` if `TSLA` gives a short signal).
1. **The Setup:** Captures the 15-minute opening candle.
2. **The Filter:** Minimum range of 25% of 14-day ATR.
3. **The Trigger (Split-Account Optimized):**
- **LONG (Bearish candle):** Bot executes a **Market BUY** in the ISA account.
- **SHORT (Bullish candle):**
- **ISA Option:** Buys a **3x Inverse ETP** (if available).
- **CFD Option:** Performs a **Direct SELL** in the CFD account (if `SPLIT_ACCOUNT_MODE=True`).
4. **The Targets:**
- Brackets are placed **immediately** after the market order is filled, using the **Actual Fill Price** from your portfolio.
- **Take Profit (TP):** The 38.2% Fibonacci retracement level.
- **Stop Loss (SL):** Placed to ensure a Risk:Reward ratio of 1:2.
5. **Time Exit:** All open positions are forcefully closed via Market Order at **11:00 EST**.
- **Stop Loss (SL):** Physical broker-side order with ATR-based padding.
- **Take Profit (TP):** Manually monitored by the bot to hit 38.2% Fibonacci retracement.
5. **Time Exit:** All positions forcefully closed at **11:00 EST**.
---
## Installation & Setup
1. **Clone the repository and set up a virtual environment:**
1. **Setup environment:**
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
2. **Configure Environment Variables:**
Create a `.env` file in the root directory:
2. **Configure `.env`:**
```ini
TRADING212_API_KEY_ID=your_key_id_here
TRADING212_API_KEY=your_api_key_here
# Primary Account (ISA)
TRADING212_API_KEY_ID=...
TRADING212_API_KEY=...
TRADING212_BASE_URL=https://demo.trading212.com/api/v0/
# Secondary Account (CFD - Optional for Shorting)
CFD_API_KEY_ID=...
CFD_API_KEY=...
CFD_BASE_URL=...
SPLIT_ACCOUNT_MODE=True
VIRTUAL_STARTING_BALANCE=250
ISA_MODE=True
```
---
## Risk Management & Position Sizing
## Split-Account Mode
The bot uses dynamic **Risk-Based Position Sizing** to ensure consistent exposure.
To overcome the lack of Inverse ETPs for certain stocks, the bot can use a Trading212 CFD account for shorting.
- **5% Risk Rule:** By default, the bot risks **5% of your account balance** per trade.
- **Virtual Balance simulation:** If you are testing on a demo account with a large balance (e.g., £5,000) but plan to trade live with £250, the bot can maintain perspective. It automatically calculates a "Virtual Balance" by subtracting £4,750 from your actual total, ensuring your risk amount is exactly what it will be in the real world. (e.g. £12.50 risk on a £250 virtual balance).
- **Leverage Adjusted:** For Inverse ETPs (3x leverage), the bot adjusts the quantity and bracket percentages to ensure the monetary risk remains identical to a standard 1x stock trade.
- **How it works:** When a Short signal is found, the bot checks if an Inverse ETP exists. If not (or if Split-Account mode is preferred), it routes the trade to the CFD account as a direct `SELL` order.
- **Benefit:** 100% coverage of all market opportunities.
---
## Automation Workflow
## Risk Management
The bot is designed to be triggered once per day (e.g., via a **systemd timer** or cron) at exactly **09:30 EST**.
1. **Scan:** Runs the ISA candidate filter to find the most volatile US stocks.
2. **Backtest:** Runs a 60-day historical backtest on the top 10 candidates.
3. **Select:** Picks the **Top 3** tickers that showed a positive historical return (Net PnL > 0 R).
4. **Execute:** Spawns parallel threads to monitor and trade the selected assets.
5. **Clean:** Shuts down automatically after the 11:00 EST exit and cleanup.
- **5% Risk Rule:** Risks 5% of the Virtual Balance (£250 starting point) per trade.
- **Capital Partitioning:** Automatically divides capital among active trades to prevent over-exposure.
- **ATR Padding:** Stop losses are automatically widened to at least 10% of daily ATR to avoid premature stop-outs from noise.
---
## Monitoring
- **Logs:** All activity is recorded in `logs/bot_YYYY-MM-DD.log`.
- **PnL Tracking:** A permanent ledger of every trade (including ETP substitutions) is kept in `pnl_tracking.csv` for graphing and analysis.
- **Journal:** Monitor via `journalctl -u touchturn.service`.
- **Logs:** Real-time mirrored logs in `logs/bot_YYYY-MM-DD.log`.
- **PnL:** Performance ledger in `pnl_tracking.csv`.
---
## Architecture
* **`src/api/client.py`:** REST API wrapper with Basic Auth.
* **`src/strategy/touch_turn.py`:** Logic engine and Fibonacci calculator.
* **`src/strategy/inverse_mapping.py`:** Map of US stocks to 3x Short Inverse ETPs.
* **`src/execution/manager.py`:** Handles market entries, actual fill-based bracketing, and ISA substitutions.
* **`main.py`:** The morning orchestrator.
* **`main.py`:** Daily orchestrator with dual-account routing.
* **`src/execution/manager.py`:** Hybrid exit management (Broker SL / Bot TP).
* **`src/strategy/touch_turn.py`:** Logic engine with ATR padding.
* **`src/strategy/inverse_mapping.py`:** ISA-specific shorting map.
+49 -31
View File
@@ -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()
+20 -37
View File
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class ExecutionManager:
"""
Manages the lifecycle of a trade: Entry, SL placement, and Exit.
Uses a Hybrid Strategy: Broker-side SL and Bot-side TP monitoring.
Supports Hybrid Exit Strategy and Split-Account routing.
"""
def __init__(self, client: Trading212Client):
self.client = client
@@ -35,7 +35,6 @@ class ExecutionManager:
logger.warning(f"Rate limited. Retrying in {wait:.1f}s...")
time.sleep(wait)
elif '400' in str(e) or '403' in str(e):
# For 400/403, logging the body is crucial
if hasattr(e, 'response') and e.response is not None:
logger.error(f"API Error Body: {e.response.text}")
raise e
@@ -43,10 +42,8 @@ class ExecutionManager:
raise e
raise Exception(f"Failed after {max_attempts} attempts")
def execute_trade(self, params: Dict[str, Any], target_risk_amount: float = 0.0, max_capital: float = 0.0):
"""Starts the trade process by placing a MARKET entry order for immediate execution."""
isa_mode = os.getenv("ISA_MODE", "False").lower() == "true"
def execute_trade(self, params: Dict[str, Any], target_risk_amount: float = 0.0, max_capital: float = 0.0, isa_rules: bool = True):
"""Starts the trade process by placing a MARKET entry order."""
self.params = params
ticker = params['ticker']
base_ticker = ticker.split('_')[0]
@@ -55,7 +52,8 @@ class ExecutionManager:
self.is_etp = False
self.leverage = 1.0
if isa_mode and direction == "SELL":
# 1. ISA Rules Substitution (Only if requested)
if isa_rules and direction == "SELL":
if base_ticker in INVERSE_TICKER_MAP:
inverse_ticker = INVERSE_TICKER_MAP[base_ticker]
self.leverage = LEVERAGE_MAP.get(inverse_ticker, 3.0)
@@ -68,6 +66,7 @@ class ExecutionManager:
logger.warning(f"ISA Mode Active: Cannot Short {ticker} and no inverse ETP found. Setup ignored.")
return False
else:
# Direct trading (CFD or Long ISA)
self.params['trading_ticker'] = ticker
approx_price = params.get('current_price', params['entry_price'])
@@ -93,45 +92,38 @@ class ExecutionManager:
logger.info(f"Attempting {direction} market order for {ticker} (Qty: {quantity})...")
# 3. Execution with Smart Retry for Common Broker Errors
# 3. Execution
try:
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity)
self.current_order_id = order.get('id')
logger.info(f"Market order placed successfully. ID: {self.current_order_id}")
return True
except Exception as e:
# Precision/Min Qty Fallback
if hasattr(e, 'response') and e.response is not None:
try:
err_data = e.response.json()
err_type = err_data.get('type', '')
err_detail = err_data.get('detail', '')
# Error A: Quantity Precision Mismatch
if "precision-mismatch" in err_type or "precision" in err_detail.lower():
logger.warning(f"Precision mismatch for {ticker}. Retrying with 2 decimal places...")
if "precision" in err_detail.lower():
logger.warning(f"Precision mismatch for {ticker}. Retrying with 2 decimals...")
trade_quantity = round(trade_quantity, 2)
self.current_quantity = abs(trade_quantity)
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity)
self.current_order_id = order.get('id')
return True
# Error B: Minimum Quantity Exceeded
if "min-quantity-exceeded" in err_type:
if "min-quantity" in err_detail.lower():
import re
match = re.search(r"at least ([\d.]+)", err_detail)
if match:
min_qty = float(match.group(1))
if (min_qty * approx_price) <= (max_capital * 1.05): # Small buffer
logger.warning(f"Quantity too low for {ticker}. Upping to minimum: {min_qty}")
if (min_qty * approx_price) <= (max_capital * 1.1):
logger.warning(f"Quantity too low for {ticker}. Upping to min: {min_qty}")
trade_quantity = -min_qty if direction == "SELL" else min_qty
self.current_quantity = min_qty
order = self._call_with_retry(self.client.place_market_order, ticker, trade_quantity)
self.current_order_id = order.get('id')
return True
else:
logger.error(f"Required minimum {min_qty} exceeds available capital for {ticker}.")
except Exception as retry_e:
logger.error(f"Retry logic failed for {ticker}: {retry_e}")
except: pass
logger.error(f"Failed to place entry market order for {ticker}: {e}")
return False
@@ -187,11 +179,11 @@ class ExecutionManager:
risk_distance = (tp_price - actual_entry_price) / 2.0
sl_price = actual_entry_price - risk_distance
sl_qty = -quantity
else: # SHORT (Normal stock)
else: # SHORT (Direct CFD)
tp_price = actual_entry_price - (range_size * 0.382)
risk_distance = (actual_entry_price - tp_price) / 2.0
sl_price = actual_entry_price + risk_distance
sl_qty = quantity
sl_qty = quantity # Since it's a SELL position, BUY to close
tp_price = round(tp_price, 2)
sl_price = round(sl_price, 2)
@@ -202,7 +194,6 @@ class ExecutionManager:
try:
logger.info(f"Hybrid Mode: Placing Broker SL for {ticker} @ {sl_price}. Monitoring TP @ {tp_price} manually.")
# Use retry with possible precision fix for SL too
try:
sl_order = self._call_with_retry(self.client.place_stop_order, ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL")
self.sl_order_id = sl_order.get('id')
@@ -212,8 +203,7 @@ class ExecutionManager:
sl_qty = round(sl_qty, 2)
sl_order = self._call_with_retry(self.client.place_stop_order, ticker, sl_qty, sl_price, time_validity="GOOD_TILL_CANCEL")
self.sl_order_id = sl_order.get('id')
else:
raise sl_e
else: raise sl_e
return True
except Exception as e:
logger.error(f"Failed to place SL bracket for {ticker}: {e}")
@@ -260,8 +250,7 @@ class ExecutionManager:
self.is_in_position = False
fallback_price = float(self.params.get('final_sl', 0.0))
return True, "SL Hit (Broker)", fallback_price
else:
raise e
else: raise e
except Exception as e:
logger.error(f"Error checking exit status: {e}")
@@ -289,14 +278,8 @@ class ExecutionManager:
qty = float(pos.get('quantity', 0))
exit_price = float(pos.get('currentPrice', 0.0))
if qty != 0:
# Try to close with precision fix
try:
self._call_with_retry(self.client.place_market_order, trading_ticker, -qty)
except Exception as close_e:
if "precision" in str(close_e).lower():
self._call_with_retry(self.client.place_market_order, trading_ticker, round(-qty, 2))
else:
raise close_e
try: self._call_with_retry(self.client.place_market_order, trading_ticker, -qty)
except: self._call_with_retry(self.client.place_market_order, trading_ticker, round(-qty, 2))
break
except Exception as e:
logger.error(f"Failed to flatten position: {e}")