From 41c81bb864eb8197321c611ac3ff995c6dde27a7 Mon Sep 17 00:00:00 2001 From: pie Date: Fri, 3 Jul 2026 16:52:10 +0100 Subject: [PATCH] feat: implement Split-Account Mode for ISA/CFD hybrid execution --- .env.example | 14 ++++++- GEMINI.md | 26 +++++++------ README.md | 78 +++++++++++++++++++++------------------ main.py | 80 ++++++++++++++++++++++++---------------- src/execution/manager.py | 57 ++++++++++------------------ 5 files changed, 137 insertions(+), 118 deletions(-) diff --git a/.env.example b/.env.example index bc851dd..6510677 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/GEMINI.md b/GEMINI.md index 61e080a..1c9e19a 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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. diff --git a/README.md b/README.md index e7f003f..4ee19d9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main.py b/main.py index 0fa544a..0d777aa 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/src/execution/manager.py b/src/execution/manager.py index 2decb67..daf9d65 100644 --- a/src/execution/manager.py +++ b/src/execution/manager.py @@ -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}")