Initial commit: Touch & Turn Scalping Bot with fully automated execution, backtesting, and ISA screening

This commit is contained in:
pie
2026-04-22 21:19:33 +01:00
commit dc111abf8c
15 changed files with 1518 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
TRADING212_API_KEY_ID=your_practice_api_key_id_here
TRADING212_API_KEY=your_practice_api_key_here
TRADING212_BASE_URL=https://demo.trading212.com/api/v0/
# 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
+19
View File
@@ -0,0 +1,19 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Virtual Environments
venv/
env/
.env
# Application Data
*.log
logs/
*.csv
*.json
# IDE
.vscode/
.idea/
+53
View File
@@ -0,0 +1,53 @@
# Trading212 Python Scalping Bot - "Touch & Turn" (Opening Range Reversal)
This project implements the "Touch & Turn" scalping strategy, originally designed for ProRealTime, translated into Python for the Trading212 API.
## Project Overview
* **Strategy:** Opening Range Liquidity Reversal (Touch & Turn).
* **Asset Class:** US Stocks (e.g., Netflix, Apple, Tesla).
* **Timeframe:** 15-minute chart.
* **Operating Window:** 09:30 - 11:00 EST (Opening of the US Regular Trading Session).
## Strategy Logic (The Workflow)
1. **Identify the Opening Candle:** Capture the `High`, `Low`, `Open`, and `Close` of the first 15-minute candle of the session (09:30 to 09:45 EST).
2. **Filter for Liquidity:**
- Calculate the 14-day ATR (Average True Range).
- The opening range (`High - Low`) must be at least **25% of the ATR**. If smaller, the bot stays flat for the day.
3. **Determine Direction:**
- If the candle is **Bearish** (Close < Open): Prepare for a **LONG** entry at the `Low`.
- If the candle is **Bullish** (Close > Open): Prepare for a **SHORT** entry at the `High`.
4. **Calculate Targets (Fibonacci):**
- The target price is the **38.2% Fibonacci level** of the opening candle's range.
5. **Risk Management:**
- **Take Profit (TP):** The distance from the entry to the 38.2% Fib level.
- **Stop Loss (SL):** Half the TP distance (Risk:Reward ratio of 1:2).
6. **Automatic Exit:** Force close any open positions at 11:00 EST.
## Technical Architecture
* **API Client (`src/api/client.py`):** Handles REST calls to Trading212.
* **Strategy Engine (`src/strategy/touch_turn.py`):**
- Monitors the clock for the 09:45 EST trigger.
- Fetches 14-day ATR and the 09:30-09:45 15m candle.
- Calculates entry/TP/SL levels.
* **Execution Engine (`src/execution/manager.py`):** Places the limit orders and manages the position lifetime.
## Getting Started
1. **Setup Environment:**
```bash
pip install -r requirements.txt
```
2. **Configuration:**
- Set `TRADING212_API_KEY` and `TRADING212_BASE_URL` in your `.env` file.
- Ensure your system clock is accurate or handle timezone conversions to EST.
## TODOs
- [x] Document the strategy logic.
- [x] Implement ATR calculation in the strategy engine.
- [x] Implement the 15m candle capture logic.
- [x] Implement the entry/exit order placement logic in the execution manager.
- [x] Create a backtesting script (optional but recommended).
+124
View File
@@ -0,0 +1,124 @@
# 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.
## ⚠️ 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.
---
## Strategy Overview
The strategy capitalizes on the initial liquidity and volatility of the US market open.
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). If the market is too quiet, no trade is taken.
3. **The Trigger:**
- If the opening candle closes **Bearish** (Close < Open), the bot prepares a **LONG** entry at the candle's Low.
- If the opening candle closes **Bullish** (Close > Open), the bot prepares a **SHORT** entry at the candle's High.
4. **The Targets:**
- **Take Profit (TP):** The 38.2% Fibonacci retracement level of the opening candle's range.
- **Stop Loss (SL):** Placed to ensure a Risk:Reward ratio of 1:2 (Risking 1 unit to make 2).
5. **Time Exit:** All open positions are forcefully closed at 11:00 EST to avoid mid-day chop.
---
## Installation & Setup
1. **Clone the repository and set up a virtual environment:**
```bash
python3 -m venv venv
source venv/bin/activate
```
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
*(Note: The `prettytable` library was recently added for backtesting outputs. Run `pip install prettytable` if it's missing from your requirements file).*
3. **Configure Environment Variables:**
Create a `.env` file in the root directory based on `.env.example`:
```ini
TRADING212_API_KEY_ID=your_key_id_here
TRADING212_API_KEY=your_api_key_here
TRADING212_BASE_URL=https://demo.trading212.com/api/v0/
```
*You can generate your API Key and ID inside the Trading212 app under Settings -> API.*
4. **Verify Connection:**
Run the test script to ensure your credentials are correct and you can read your account balance:
```bash
python3 test_api_connection.py
```
---
## The Workflow & Scripts
This repository is split into the live execution bot and several helper scripts to find the best assets to trade.
### 1. Finding ISA-Eligible Tickers
If you are trading from a UK Trading212 Stocks ISA, you are restricted from trading US-domiciled ETFs and certain other assets.
Run the ISA candidate script to fetch all available instruments, filter for ISA-compliant US Stocks, and rank a basket of popular tech stocks by their current volatility (ATR %):
```bash
PYTHONPATH=. python3 scripts/find_isa_candidates.py
```
*This script outputs a leaderboard to the console and saves `isa_watchlist.csv`.*
### 2. Backtesting the Watchlist
High volatility doesn't always guarantee a strategy works on a specific stock. You must backtest.
Run the backtesting engine. It will automatically read the `isa_watchlist.csv` generated in the previous step and simulate the strategy over the last ~60 trading days (using 15m data from Yahoo Finance).
```bash
PYTHONPATH=. python3 scripts/backtest.py
```
*This will output a leaderboard ranked by **Net PnL (in Risk Multiples/R)**. Identify the top 2-3 performing tickers (e.g., `NFLX`, `UBER`, `MSFT`) to configure the live bot.*
### 3. Running the Live Bot
Once you have identified the best tickers for the day, the orchestrator will automatically spin up threads to monitor and trade them.
Start the bot before the US market opens (09:30 EST):
```bash
python3 main.py
```
The bot will:
1. Initialize and run the ISA scanner.
2. Backtest the top candidates and pick the **Top 3** with the highest historical R-multiple profit.
3. Spawn isolated background threads for each ticker to wait for exactly 09:45 EST.
4. Evaluate the opening candle, calculate risk-adjusted position sizes, and place the necessary Entry, Take Profit, and Stop Loss orders.
5. Poll for order fills and gracefully flatten all open positions at exactly 11:00 EST.
---
## Risk Management & Position Sizing
The bot uses dynamic **Risk-Based Position Sizing**. It does not buy a fixed number of shares. Instead, it calculates the distance between the Entry price and the Stop Loss price to determine the "Risk per Share".
By default, the bot risks **1% of your account balance** per trade.
**Virtual Balances:**
If you are testing on a demo account with a massive starting balance (e.g., £5,000) but plan to trade live with a much smaller amount, you can override the risk calculation to maintain psychological perspective. Set `VIRTUAL_STARTING_BALANCE=250` in your `.env` file. The bot will pretend your account only has £250 and will size its fraction share purchases to risk exactly £2.50 per trade.
---
## Logging & PnL Tracking
The bot provides comprehensive monitoring out of the box:
- **Console & File Logging:** All activity (entries, fills, errors) is logged to the console and simultaneously appended to a daily file in the `logs/` directory (e.g., `logs/bot_2026-04-14.log`).
- **PnL Tracking:** A running ledger of all closed trades is kept in `pnl_tracking.csv`. This file records the Ticker, Direction, Entry/Exit prices, the reason for the exit (e.g., "TP Hit" or "11:00 Time Exit"), and the Profit/Loss measured in Risk Multiples (R). You can import this CSV into Excel or Python to chart your strategy's performance over time.
---
## Utility Scripts
- `scripts/get_available_tickers.py`: Fetches the raw metadata for all 16,000+ instruments available on Trading212 and saves them to `available_instruments.json` and `available_tickers.csv`. Useful if you want to manually search for new tickers outside the default tech/growth basket.
## Architecture
* **`src/api/client.py`:** Handles REST HTTP basic authentication and request formatting for the Trading212 API.
* **`src/strategy/touch_turn.py`:** The core logic engine. Fetches market data via Yahoo Finance, calculates ATR and Fibonacci levels, and returns the trade parameters.
* **`src/strategy/scanner.py`:** The ranking engine used to sort tickers by ATR volatility.
* **`src/execution/manager.py`:** Consumes the trade parameters and places the orders via the API client.
+155
View File
@@ -0,0 +1,155 @@
// Generated by ProRealAlgos.com
// Touch & Turn Scalper Opening Range Liquidity Reversal
// RECOMMENDED TIMEFRAME: 15-minute chart (strategy runs
on 15m only)
// Instrument example: Netflix (US stocks) session 09:3011:00
EST
// Entry logic based on first 15m candle of regular session
DEFPARAM CumulateOrders = False
DEFPARAM FlatBefore = 090000
DEFPARAM FlatAfter = 110000
// === USER PARAMETERS ===
riskPercentATR = 25 // % of daily ATR to qualify as liquidity
candle
rrRatio = 2 // Risk:Reward ratio (TP = 2x SL)
atrPeriod = 14
// === SESSION CONTROL (US STOCKS) ===
isNewSession = (Hour = 9 AND Minute = 30)
// === RESET VARIABLES EACH DAY ===
IF isNewSession THEN
rangeHigh = 0
rangeLow = 0
rangeSize = 0
fib38 = 0
direction = 0
validSetup = 0
ENDIF
// === CAPTURE FIRST 15m OPENING RANGE ===
IF Hour = 9 AND Minute = 45 THEN
rangeHigh = High
rangeLow = Low
rangeSize = rangeHigh - rangeLow
// --- DAILY ATR ---
dailyATR = AverageTrueRange[atrPeriod](Close)
// --- LIQUIDITY CANDLE FILTER ---
IF rangeSize >= dailyATR * riskPercentATR / 100 THEN
validSetup = 1
ELSE
validSetup = 0
ENDIF
// --- FIB 38.2 LEVEL ---
fib38 = rangeLow + (rangeHigh - rangeLow) * 0.382
// --- DIRECTION OF LIQUIDITY CANDLE ---
IF Close < Open THEN
direction = -1 // bearish candle → look for LONG
ELSE
direction = 1 // bullish candle → look for SHORT
ENDIF
ENDIF
// === TRADE EXECUTION (Touch & Turn) ===
IF validSetup = 1 AND NOT OnMarket AND Hour >= 9 AND Hour <
11 THEN
// === LONG SETUP ===
IF direction = -1 THEN
entryPrice = rangeLow
targetDistance = fib38 - entryPrice
stopDistance = targetDistance / rrRatio
BUY 1 SHARE AT entryPrice LIMIT
SET TARGET PRICE fib38
SET STOP LOSS stopDistance
ENDIF
// === SHORT SETUP ===
IF direction = 1 THEN
entryPrice = rangeHigh
targetDistance = entryPrice - fib38
stopDistance = targetDistance / rrRatio
SELLSHORT 1 SHARE AT entryPrice LIMIT
SET TARGET PRICE fib38
SET STOP LOSS stopDistance
ENDIF
ENDIF
// === FORCE EXIT AFTER 11:00 ===
IF Hour >= 11 AND OnMarket THEN
SELL AT MARKET
EXITSHORT AT MARKET
ENDIF
// === BACKTEST STATISTICS DISPLAY ===
GRAPH StrategyProfit AS "Net Profit"
GRAPH PositionPerf(1) AS "Last Trade %"
+214
View File
@@ -0,0 +1,214 @@
import os
import time
import logging
import pytz
import threading
import csv
from datetime import datetime, time as dtime
from dotenv import load_dotenv
from src.api.client import Trading212Client
from src.strategy.touch_turn import TouchTurnStrategy
from src.execution.manager import ExecutionManager
from scripts.find_isa_candidates import find_best_isa_tickers
from scripts.backtest import backtest_ticker
# Ensure logs directory exists
os.makedirs("logs", exist_ok=True)
log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log")
# Configure logging to both console and file
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_filename),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
PNL_FILE = "pnl_tracking.csv"
def record_pnl(ticker, direction, entry_price, exit_price, reason, pnl_r):
"""Appends the result of a closed trade to the PnL CSV."""
file_exists = os.path.isfile(PNL_FILE)
with open(PNL_FILE, mode='a', newline='') as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(["Date", "Ticker", "Direction", "Entry Price", "Exit Price", "Reason", "PnL (R)"])
today = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
writer.writerow([today, ticker, direction, round(entry_price, 2), round(exit_price, 2), reason, round(pnl_r, 2)])
logger.info(f"Recorded trade in {PNL_FILE}: {ticker} {direction} | Result: {reason} | PnL: {pnl_r:.2f} R")
def calculate_r_multiple(direction, entry_price, exit_price, stop_loss):
"""Calculates the PnL in terms of Risk Multiples (R)."""
if direction == "BUY": # LONG
risk = entry_price - stop_loss
return (exit_price - entry_price) / risk if risk != 0 else 0
else: # SHORT
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):
"""Handles the full strategy lifecycle for a single ticker in its own thread, then exits."""
strategy = TouchTurnStrategy(yf_ticker)
execution = ExecutionManager(client)
logger.info(f"Bot thread started for {yf_ticker} ({t212_ticker}).")
now = datetime.now(tz)
target_entry_time = now.replace(hour=9, minute=45, second=0, microsecond=0)
# 1. Wait until 09:45 EST
if now < target_entry_time:
wait_seconds = (target_entry_time - now).total_seconds()
logger.info(f"Waiting {wait_seconds:.0f} seconds until 09:45 EST evaluation...")
time.sleep(wait_seconds)
# Re-evaluate current time
now = datetime.now(tz)
if now.hour == 9 and now.minute >= 45:
logger.info(f"Evaluating opening candle for {yf_ticker}...")
if strategy.check_setup():
params = strategy.get_trade_params()
params['ticker'] = t212_ticker
# Fetch Account Balance to calculate risk
try:
account_info = client.get_account_info()
virtual_balance = float(os.getenv("VIRTUAL_STARTING_BALANCE", 0))
if virtual_balance > 0:
risk_amount = virtual_balance * 0.01
else:
available_cash = account_info.get('cash', {}).get('availableToTrade', 1000)
risk_amount = available_cash * 0.01
except Exception as e:
logger.error(f"Failed to fetch account info for risk calculation: {e}. Defaulting to £2.50 risk.")
risk_amount = 2.50
if execution.execute_trade(params, target_risk_amount=risk_amount):
# monitor_and_bracket is blocking, wait for fill (times out at 11:00)
if execution.monitor_and_bracket(params):
# Position is open, monitor for exit via SL/TP
while datetime.now(tz).hour < 11:
is_closed, reason, exit_price = execution.check_exit_status()
if is_closed:
pnl_r = calculate_r_multiple(params['direction'], params['entry_price'], exit_price, params['stop_loss'])
record_pnl(yf_ticker, params['direction'], params['entry_price'], exit_price, reason, pnl_r)
break # Exit the monitoring loop
time.sleep(15)
else:
logger.info(f"No valid setup today for {yf_ticker}. Thread exiting.")
return
# 2. Wait until 11:00 EST for Forced Exit (if we are still in a position or have pending orders)
now = datetime.now(tz)
target_exit_time = now.replace(hour=11, minute=0, second=0, microsecond=0)
if now < target_exit_time and execution.is_in_position:
wait_seconds = (target_exit_time - now).total_seconds()
logger.info(f"Waiting {wait_seconds:.0f} seconds until 11:00 EST forced exit...")
time.sleep(wait_seconds)
# 3. 11:00 EST - Cleanup
logger.info(f"Time exit reached for {yf_ticker}. Cleaning up.")
if execution.is_in_position:
exit_price = execution.close_all(t212_ticker)
if hasattr(execution, 'params') and exit_price > 0:
pnl_r = calculate_r_multiple(execution.params['direction'], execution.params['entry_price'], exit_price, execution.params['stop_loss'])
record_pnl(yf_ticker, execution.params['direction'], execution.params['entry_price'], exit_price, "11:00 Time Exit", pnl_r)
else:
# Cleanup any pending orders if entry wasn't filled
execution.close_all(t212_ticker)
logger.info(f"Lifecycle complete for {yf_ticker}. Thread exiting.")
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')
now = datetime.now(tz)
# Safety Guard: Check if it's a weekend
if now.weekday() >= 5: # 5 = Saturday, 6 = Sunday
logger.warning("Weekend detected. The market is closed. Exiting cleanly.")
return
# Safety Guard: Check if executed outside the expected morning window (allow 09:00 to 09:40 EST)
if now.hour < 9 or (now.hour == 9 and now.minute > 40) or now.hour >= 10:
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")
return
client = Trading212Client(api_key_id, api_key, base_url)
# 1. Morning Routine: Find Candidates
logger.info("Starting Morning Routine: Finding ISA Candidates...")
candidates_df = find_best_isa_tickers()
if candidates_df is None or candidates_df.empty:
logger.error("No candidates found. Exiting.")
return
# 2. Morning Routine: Backtest Candidates to find the 'Edge'
logger.info("Running Backtests on candidates to find current winners...")
profitable_tickers = []
# We'll test the top 10 candidates from the scanner
for _, row in candidates_df.head(10).iterrows():
yf_t = row['Ticker']
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)']
})
# Sort by best backtest performance
profitable_tickers.sort(key=lambda x: x['pnl'], reverse=True)
# Select Top 3
final_watchlist = profitable_tickers[:3]
if not final_watchlist:
logger.warning("No tickers showed a positive backtest return. Bot will not trade today.")
return
logger.info(f"Final Watchlist for today: {[t['yf'] for t in final_watchlist]}")
# 3. Launch execution threads
threads = []
for ticker_info in final_watchlist:
t = threading.Thread(
target=run_ticker_lifecycle,
args=(client, ticker_info['yf'], ticker_info['t212'], tz),
name=f"Bot-{ticker_info['yf']}"
)
t.start()
threads.append(t)
logger.info("All execution threads launched. Waiting for completion...")
for t in threads:
t.join()
logger.info("All threads completed. Bot shutting down for the day.")
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
requests
pandas
pandas-ta
python-dotenv
pytest
yfinance
pytz
+291
View File
@@ -0,0 +1,291 @@
import yfinance as yf
import pandas as pd
import pandas_ta as ta
from datetime import datetime, time, timedelta
import pytz
import logging
from prettytable import PrettyTable
logging.basicConfig(level=logging.WARNING) # Set to INFO for detailed trade logs
logger = logging.getLogger(__name__)
def backtest_ticker(ticker: str, days: int = 59, risk_percent_atr: float = 25.0, rr_ratio: float = 2.0, quiet: bool = False):
"""
Backtests the Touch & Turn strategy on a single ticker over the maximum available 15m history.
"""
if not quiet:
print(f"\nRunning Backtest for {ticker} over {days} days...")
tz = pytz.timezone('US/Eastern')
# Fetch Daily data (Need roughly days + 30 for 14-day ATR buffer)
daily_data = yf.download(ticker, period="6mo", interval="1d", progress=False)
if daily_data.empty:
if not quiet: print(f"Failed to fetch daily data for {ticker}")
return None
# Clean up multi-index columns if present
if isinstance(daily_data.columns, pd.MultiIndex):
daily_data.columns = daily_data.columns.droplevel(1)
daily_data.ta.atr(length=14, append=True)
daily_data.dropna(subset=['ATRr_14'], inplace=True)
# Fetch 15m intraday data
intraday_data = yf.download(ticker, period=f"{days}d", interval="15m", progress=False)
if intraday_data.empty:
if not quiet: print(f"Failed to fetch 15m data for {ticker}")
return None
if isinstance(intraday_data.columns, pd.MultiIndex):
intraday_data.columns = intraday_data.columns.droplevel(1)
# Convert index to US/Eastern timezone to reliably filter by time
if intraday_data.index.tz is None:
intraday_data.index = intraday_data.index.tz_localize('UTC').tz_convert(tz)
else:
intraday_data.index = intraday_data.index.tz_convert(tz)
unique_dates = pd.Series(intraday_data.index.date).unique()
# Metrics
total_trades = 0
wins = 0
losses = 0
time_exits = 0
total_pnl_r = 0.0 # PnL measured in R multiples
trade_log = []
for trade_date in unique_dates:
# 1. Get Yesterday's ATR
try:
# Get daily data up to the day before `trade_date`
prior_daily = daily_data[daily_data.index.date < trade_date]
if prior_daily.empty:
continue
yesterday_atr = prior_daily['ATRr_14'].iloc[-1]
except IndexError:
continue
# 2. Get today's 15m data
day_data = intraday_data[intraday_data.index.date == trade_date]
# 3. Find 09:30 candle
opening_candles = day_data.between_time('09:30', '09:30')
if opening_candles.empty:
continue
open_c = opening_candles.iloc[0]
high, low, open_p, close_p = open_c['High'], open_c['Low'], open_c['Open'], open_c['Close']
range_size = high - low
# 4. Filter for Liquidity/Volatility
if range_size < (yesterday_atr * risk_percent_atr / 100):
continue # Market not volatile enough today
# 5. Determine Setup
direction = -1 if close_p < open_p else 1 # -1 for Long, 1 for Short
if direction == -1: # LONG setup (from Bearish open)
entry_price = low
target_price = low + (range_size * 0.382)
stop_distance = (target_price - entry_price) / rr_ratio
stop_loss = entry_price - stop_distance
trade_dir = "LONG"
else: # SHORT setup (from Bullish open)
entry_price = high
target_price = high - (range_size * 0.382)
stop_distance = (entry_price - target_price) / rr_ratio
stop_loss = entry_price + stop_distance
trade_dir = "SHORT"
# 6. Simulate Execution Window (09:45 to 10:45 inclusive - since 11:00 is forced exit)
sim_data = day_data.between_time('09:45', '10:45')
if sim_data.empty:
continue
position_open = False
trade_result = None
pnl_r = 0.0
exit_price = 0.0
exit_time = None
exit_reason = ""
for timestamp, candle in sim_data.iterrows():
c_high, c_low, c_close = candle['High'], candle['Low'], candle['Close']
# Check for Entry
if not position_open:
if (trade_dir == "LONG" and c_low <= entry_price) or (trade_dir == "SHORT" and c_high >= entry_price):
position_open = True
logger.info(f"{trade_date} - FILLED {trade_dir} at {entry_price:.2f}")
if trade_dir == "LONG":
if c_low <= stop_loss:
trade_result = "LOSS"
exit_price = stop_loss
exit_time = timestamp
exit_reason = "SL Hit"
pnl_r = -1.0
position_open = False
elif c_high >= target_price:
trade_result = "WIN"
exit_price = target_price
exit_time = timestamp
exit_reason = "TP Hit"
pnl_r = rr_ratio
position_open = False
else: # SHORT
if c_high >= stop_loss:
trade_result = "LOSS"
exit_price = stop_loss
exit_time = timestamp
exit_reason = "SL Hit"
pnl_r = -1.0
position_open = False
elif c_low <= target_price:
trade_result = "WIN"
exit_price = target_price
exit_time = timestamp
exit_reason = "TP Hit"
pnl_r = rr_ratio
position_open = False
continue
# If position is already open, check SL/TP for current candle
if position_open:
if trade_dir == "LONG":
if c_low <= stop_loss:
trade_result = "LOSS"
exit_price = stop_loss
exit_time = timestamp
exit_reason = "SL Hit"
pnl_r = -1.0
position_open = False
elif c_high >= target_price:
trade_result = "WIN"
exit_price = target_price
exit_time = timestamp
exit_reason = "TP Hit"
pnl_r = rr_ratio
position_open = False
else: # SHORT
if c_high >= stop_loss:
trade_result = "LOSS"
exit_price = stop_loss
exit_time = timestamp
exit_reason = "SL Hit"
pnl_r = -1.0
position_open = False
elif c_low <= target_price:
trade_result = "WIN"
exit_price = target_price
exit_time = timestamp
exit_reason = "TP Hit"
pnl_r = rr_ratio
position_open = False
if not position_open:
break
# 7. Force Exit at 11:00 (Close of 10:45 candle)
if position_open:
last_candle = sim_data.iloc[-1]
exit_price = last_candle['Close']
exit_time = sim_data.index[-1]
exit_reason = "11:00 Time Exit"
if trade_dir == "LONG":
distance = exit_price - entry_price
pnl_r = distance / stop_distance
else:
distance = entry_price - exit_price
pnl_r = distance / stop_distance
if pnl_r > 0:
trade_result = "WIN (Time)"
else:
trade_result = "LOSS (Time)"
position_open = False
# 8. Record Trade
if trade_result is not None:
total_trades += 1
if "WIN" in trade_result:
wins += 1
else:
losses += 1
if "Time" in trade_result:
time_exits += 1
total_pnl_r += pnl_r
trade_log.append([
trade_date.strftime("%Y-%m-%d"), trade_dir,
f"{entry_price:.2f}", f"{target_price:.2f}", f"{stop_loss:.2f}",
trade_result, f"{exit_price:.2f}", f"{pnl_r:.2f} R"
])
win_rate = (wins / total_trades) * 100 if total_trades > 0 else 0
if not quiet and total_trades > 0:
table = PrettyTable()
table.field_names = ["Date", "Dir", "Entry", "TP", "SL", "Result", "Exit Px", "PnL (R)"]
for row in trade_log:
table.add_row(row)
print(table)
print(f"\n--- Backtest Results: {ticker} ---")
print(f"Total Setups Triggered: {total_trades}")
print(f"Wins: {wins} | Losses: {losses}")
print(f"Win Rate: {win_rate:.2f}%")
print(f"Time Exits (11:00 EST): {time_exits}")
print(f"Net PnL: {total_pnl_r:.2f} R")
elif not quiet:
print(f"No valid setups triggered for {ticker} in the last {days} days.")
return {
"Ticker": ticker,
"Trades": total_trades,
"Wins": wins,
"Losses": losses,
"Win Rate (%)": round(win_rate, 2),
"Time Exits": time_exits,
"Net PnL (R)": round(total_pnl_r, 2)
}
if __name__ == "__main__":
import os
if os.path.exists("isa_watchlist.csv"):
print("Reading tickers from isa_watchlist.csv...")
df = pd.read_csv("isa_watchlist.csv")
tickers_to_test = df['Ticker'].tolist()
else:
print("isa_watchlist.csv not found, using default list...")
tickers_to_test = ["TSLA", "NVDA", "NFLX", "AMD"]
results = []
print(f"Running batch backtest for {len(tickers_to_test)} tickers. This may take a minute...")
for t in tickers_to_test:
res = backtest_ticker(t, quiet=True)
if res and res["Trades"] > 0:
results.append(res)
if results:
results_df = pd.DataFrame(results)
results_df = results_df.sort_values(by="Net PnL (R)", ascending=False).reset_index(drop=True)
print("\n" + "="*80)
print("🚀 BACKTEST LEADERBOARD (LAST ~60 DAYS) 🚀")
print("="*80)
table = PrettyTable()
table.field_names = results_df.columns
for _, row in results_df.iterrows():
table.add_row(row.tolist())
print(table)
print("\n* PnL is measured in 'R' (Risk Multiples). +2.0 R means you made twice what you risked.")
else:
print("No trades generated for any tickers.")
+89
View File
@@ -0,0 +1,89 @@
import pandas as pd
import json
import logging
from src.strategy.scanner import scan_for_candidates
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def find_best_isa_tickers():
"""
1. Reads available Trading212 instruments.
2. Filters for US Stocks (excluding ETFs/Warrants which are often non-ISA compliant or bad for this strategy).
3. Maps T212 tickers to Yahoo Finance tickers.
4. Runs the strategy scanner to find the most volatile/liquid candidates.
"""
try:
with open('available_instruments.json', 'r') as f:
instruments = json.load(f)
except FileNotFoundError:
logger.error("Please run get_available_tickers.py first to generate available_instruments.json")
return
# Filter for US Stocks only
# - type == 'STOCK': Excludes US ETFs (not allowed in UK ISAs) and Warrants
# - currencyCode == 'USD': Ensures it's trading during the US session (09:30 EST)
us_stocks = [
inst for inst in instruments
if inst.get('type') == 'STOCK' and inst.get('currencyCode') == 'USD'
]
logger.info(f"Filtered down to {len(us_stocks)} US Stocks from Trading212.")
# Extract short names (Yahoo Finance tickers)
# Trading212 usually uses 'shortName' for the actual market ticker (e.g., AAPL)
# and 'ticker' for their internal ID (e.g., AAPL_US_EQ).
t212_to_yf_map = {}
for stock in us_stocks:
short_name = stock.get('shortName')
t212_id = stock.get('ticker')
if short_name and t212_id:
# Avoid preferred shares/warrants that might sneak in with hyphens or dots
# if yfinance can't parse them easily, but we'll try them all.
t212_to_yf_map[short_name] = t212_id
yf_tickers = list(t212_to_yf_map.keys())
# We don't want to scan all 6,000+ stocks at once (Yahoo Finance will rate limit us).
# Let's filter locally first using T212 maxOpenQuantity or just take a known subset,
# OR we can pass the top 500 popular ones. For this script, let's scan a robust list of
# well-known highly liquid tech/growth stocks that are definitely ISA eligible.
# Known high-liquidity ISA-eligible US stocks:
focus_list = [
"TSLA", "NVDA", "AMD", "AAPL", "MSFT", "META", "AMZN", "GOOGL", "NFLX",
"COIN", "MSTR", "PLTR", "UBER", "HOOD", "RST", "SNOW", "CRM", "CRWD",
"PANW", "SMCI", "ARM", "SQ", "SHOP", "ROKU", "DDOG", "NET", "DOCN"
]
# Ensure they exist in our T212 account
valid_yf_tickers = [t for t in focus_list if t in t212_to_yf_map]
logger.info(f"Scanning {len(valid_yf_tickers)} highly liquid known ISA-eligible stocks...")
# Run the scanner
results_df = scan_for_candidates(tickers=valid_yf_tickers, min_price=20.0, min_volume=2_000_000)
if results_df.empty:
logger.warning("No candidates met the minimum price/volume criteria.")
return
# Map back to Trading212 internal tickers so the bot knows what to trade
results_df['T212_Ticker'] = results_df['Ticker'].map(t212_to_yf_map)
# Reorder columns
cols = ['Ticker', 'T212_Ticker', 'Close', 'ATR_14', 'ATR_Percent', 'Avg_Volume']
results_df = results_df[cols]
print("\n" + "="*80)
print("🏆 BEST ISA-ELIGIBLE CANDIDATES FOR TOUCH & TURN TODAY 🏆")
print("="*80)
print(results_df.to_string(index=False))
print("\n* Use the 'T212_Ticker' column when configuring the ExecutionManager.")
# Save the day's watchlist
results_df.to_csv("isa_watchlist.csv", index=False)
logger.info("Saved shortlist to isa_watchlist.csv")
return results_df
if __name__ == "__main__":
find_best_isa_tickers()
+60
View File
@@ -0,0 +1,60 @@
import os
import logging
import json
import pandas as pd
from dotenv import load_dotenv
from src.api.client import Trading212Client
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def fetch_and_save_tickers():
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/")
if not api_key_id or not api_key:
logger.error("API Key ID or API Key is missing in .env")
return
client = Trading212Client(api_key_id, api_key, base_url)
try:
logger.info("Fetching available instruments from Trading212...")
instruments = client.get_instruments()
logger.info(f"Retrieved {len(instruments)} instruments.")
# Save raw JSON
with open("available_instruments.json", "w") as f:
json.dump(instruments, f, indent=4)
logger.info("Saved raw data to available_instruments.json")
# Convert to a DataFrame and save as CSV for easier viewing/filtering
df = pd.DataFrame(instruments)
# Select the most useful columns if they exist
expected_columns = ['ticker', 'name', 'type', 'currencyCode', 'exchange']
available_columns = [col for col in expected_columns if col in df.columns]
if available_columns:
df_filtered = df[available_columns]
# Print a quick summary of US Equities (which is what Touch & Turn trades)
if 'type' in df.columns and 'currencyCode' in df.columns:
us_stocks = df[(df['type'] == 'STOCK') & (df['currencyCode'] == 'USD')]
logger.info(f"Found {len(us_stocks)} US Stocks.")
logger.info("\nSample of US Stocks available:")
print(us_stocks.head(10).to_string(index=False))
else:
df_filtered = df
df_filtered.to_csv("available_tickers.csv", index=False)
logger.info("Saved structured list to available_tickers.csv")
except Exception as e:
logger.error(f"Failed to fetch instruments: {e}")
if __name__ == "__main__":
fetch_and_save_tickers()
+85
View File
@@ -0,0 +1,85 @@
import requests
import os
import base64
from typing import Dict, Any, Optional
class Trading212Client:
"""
A basic client for interacting with the Trading212 REST API.
"""
def __init__(self, api_key_id: str, api_key: str, base_url: str):
self.api_key_id = api_key_id
self.api_key = api_key
self.base_url = base_url.rstrip('/')
credentials = f"{self.api_key_id}:{self.api_key}"
encoded_credentials = base64.b64encode(credentials.encode()).decode()
self.headers = {
"Authorization": f"Basic {encoded_credentials}",
"Content-Type": "application/json"
}
def _get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Performs a GET request to the Trading212 API."""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
return response.json()
def _post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Performs a POST request to the Trading212 API."""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
response = requests.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def place_market_order(self, ticker: str, quantity: float) -> Dict[str, Any]:
"""Places a market order."""
data = {
"ticker": ticker,
"quantity": quantity
}
return self._post("equity/orders/market", data)
def place_limit_order(self, ticker: str, quantity: float, limit_price: float, time_validity: str = "DAY") -> Dict[str, Any]:
"""Places a limit order."""
data = {
"ticker": ticker,
"quantity": quantity,
"limitPrice": limit_price,
"timeValidity": time_validity
}
return self._post("equity/orders/limit", data)
def place_stop_order(self, ticker: str, quantity: float, stop_price: float, time_validity: str = "GOOD_TILL_CANCEL") -> Dict[str, Any]:
"""Places a stop order (used for Stop Loss)."""
data = {
"ticker": ticker,
"quantity": quantity,
"stopPrice": stop_price,
"timeValidity": time_validity
}
return self._post("equity/orders/stop", data)
def get_order_status(self, order_id: str) -> Dict[str, Any]:
"""Retrieves the status of a specific order."""
return self._get(f"equity/orders/{order_id}")
def cancel_order(self, order_id: str) -> bool:
"""Cancels a specific order."""
url = f"{self.base_url}/equity/orders/{order_id}"
response = requests.delete(url, headers=self.headers)
return response.status_code == 204
def get_account_info(self) -> Dict[str, Any]:
"""Retrieves general account information."""
return self._get("equity/account/summary")
def get_all_open_positions(self) -> Dict[str, Any]:
"""Retrieves all currently open positions."""
return self._get("equity/portfolio")
def get_instruments(self) -> Any:
"""Retrieves a list of all available instruments."""
return self._get("equity/metadata/instruments")
+177
View File
@@ -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
+77
View File
@@ -0,0 +1,77 @@
import yfinance as yf
import pandas_ta as ta
import pandas as pd
import logging
from typing import List, Dict
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
DEFAULT_TICKERS = [
"AAPL", "MSFT", "NVDA", "AMZN", "META", "GOOGL", "TSLA",
"AMD", "NFLX", "QCOM", "INTC", "BA", "DIS", "SPY", "QQQ"
]
def scan_for_candidates(tickers: List[str] = DEFAULT_TICKERS, min_price: float = 20.0, min_volume: int = 2_000_000) -> pd.DataFrame:
"""
Scans a list of tickers to find the best candidates for the Touch & Turn strategy.
Prioritizes high Average True Range (ATR) as a percentage of price, ensuring adequate volume.
"""
logger.info(f"Scanning {len(tickers)} tickers for high volatility/liquidity...")
results = []
# Download daily data for the past 1mo to calculate 14-day ATR and Avg Volume
data = yf.download(tickers, period="1mo", interval="1d", group_by="ticker", progress=False)
for ticker in tickers:
try:
# Handle single ticker vs multi-ticker dataframe structure from yfinance
if len(tickers) == 1:
df = data.copy()
else:
df = data[ticker].copy()
if df.empty or len(df) < 15:
continue
# Clean column names (yfinance multi-index can sometimes leave tuple names)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
df.ta.atr(length=14, append=True)
latest = df.iloc[-1]
yesterday_atr = df['ATRr_14'].iloc[-2]
close_price = latest['Close']
avg_volume = df['Volume'].tail(14).mean()
# Filters
if close_price < min_price or avg_volume < min_volume or pd.isna(yesterday_atr):
continue
atr_percent = (yesterday_atr / close_price) * 100
results.append({
"Ticker": ticker,
"Close": round(float(close_price), 2),
"ATR_14": round(float(yesterday_atr), 2),
"ATR_Percent": round(float(atr_percent), 2),
"Avg_Volume": int(avg_volume)
})
except Exception as e:
logger.debug(f"Failed processing {ticker}: {e}")
results_df = pd.DataFrame(results)
if not results_df.empty:
# Sort by ATR Percentage descending (we want the most volatile stocks)
results_df = results_df.sort_values(by="ATR_Percent", ascending=False).reset_index(drop=True)
return results_df
if __name__ == "__main__":
candidates = scan_for_candidates()
print("\nTop Candidates for Touch & Turn Strategy:")
print("-" * 65)
print(candidates.to_string(index=False))
+129
View File
@@ -0,0 +1,129 @@
import pandas as pd
import pandas_ta as ta
import yfinance as yf
from datetime import datetime, time, timedelta
import pytz
import logging
logger = logging.getLogger(__name__)
class TouchTurnStrategy:
"""
Implements the 'Touch & Turn' (Opening Range Reversal) strategy.
1. Captures the 09:30-09:45 EST 15m candle.
2. Checks if range > 25% of 14-day ATR.
3. Bearish candle -> Long at Low, Target @ 38.2% Fib.
4. Bullish candle -> Short at High, Target @ 38.2% Fib.
"""
def __init__(self, ticker: str, risk_percent_atr: float = 25.0, rr_ratio: float = 2.0):
self.ticker = ticker
self.risk_percent_atr = risk_percent_atr
self.rr_ratio = rr_ratio
self.tz = pytz.timezone('US/Eastern')
self.valid_setup = False
self.direction = 0 # 1 for Short (from High), -1 for Long (from Low)
self.entry_price = 0.0
self.target_price = 0.0
self.stop_loss = 0.0
def get_market_data(self):
"""Fetches 15m candles and daily data for ATR."""
now = datetime.now(self.tz)
# 1. Fetch Daily Data for ATR (need at least 15+ days)
daily_data = yf.download(self.ticker, period="1mo", interval="1d", progress=False)
if daily_data.empty or len(daily_data) < 15:
logger.error(f"Insufficient daily data for {self.ticker}")
return None, None
# Clean column names (yfinance multi-index issue)
if isinstance(daily_data.columns, pd.MultiIndex):
daily_data.columns = daily_data.columns.droplevel(1)
# Calculate ATR
daily_data.ta.atr(length=14, append=True)
daily_atr = daily_data['ATRr_14'].iloc[-2] # Use yesterday's ATR
# 2. Fetch 15m Candle for today's opening (09:30 - 09:45)
# Note: yfinance 15m candles are labeled by start time.
start_date = now.strftime('%Y-%m-%d')
intraday_data = yf.download(self.ticker, start=start_date, interval="15m", progress=False)
if intraday_data.empty:
logger.warning(f"No intraday data yet for {self.ticker}")
return daily_atr, None
if isinstance(intraday_data.columns, pd.MultiIndex):
intraday_data.columns = intraday_data.columns.droplevel(1)
# The first candle of the session (09:30)
opening_candle = intraday_data.between_time('09:30', '09:30')
if opening_candle.empty:
logger.warning(f"Opening 15m candle (09:30) not yet available for {self.ticker}")
return daily_atr, None
return daily_atr, opening_candle.iloc[0]
def check_setup(self):
"""Evaluates the strategy criteria based on the opening candle."""
daily_atr, opening_candle = self.get_market_data()
if daily_atr is None or opening_candle is None:
return False
high = opening_candle['High']
low = opening_candle['Low']
open_p = opening_candle['Open']
close_p = opening_candle['Close']
range_size = high - low
# 1. Liquidity Filter
if range_size < (daily_atr * self.risk_percent_atr / 100):
logger.info(f"Setup invalid: Range ({range_size:.2f}) < 25% of ATR ({daily_atr:.2f})")
self.valid_setup = False
return False
# 2. Determine Direction
if close_p < open_p:
self.direction = -1 # Bearish candle -> Long setup
self.entry_price = low
logger.info(f"Bearish opening candle detected. Preparing LONG at {self.entry_price:.2f}")
else:
self.direction = 1 # Bullish candle -> Short setup
self.entry_price = high
logger.info(f"Bullish opening candle detected. Preparing SHORT at {self.entry_price:.2f}")
# 3. Calculate Fibonacci 38.2% Target
# For LONG (from Low): target is 38.2% up from the Low
# For SHORT (from High): target is 38.2% down from the High
if self.direction == -1: # LONG
self.target_price = low + (range_size * 0.382)
target_distance = self.target_price - self.entry_price
stop_distance = target_distance / self.rr_ratio
self.stop_loss = self.entry_price - stop_distance
else: # SHORT
self.target_price = high - (range_size * 0.382)
target_distance = self.entry_price - self.target_price
stop_distance = target_distance / self.rr_ratio
self.stop_loss = self.entry_price + stop_distance
self.valid_setup = True
logger.info(f"Valid Setup! Entry: {self.entry_price:.2f}, Target: {self.target_price:.2f}, SL: {self.stop_loss:.2f}")
return True
def get_trade_params(self):
"""Returns the parameters for the trade execution."""
if not self.valid_setup:
return None
return {
"ticker": self.ticker,
"direction": "BUY" if self.direction == -1 else "SELL",
"entry_price": self.entry_price,
"target_price": self.target_price,
"stop_loss": self.stop_loss
}
+30
View File
@@ -0,0 +1,30 @@
import os
import logging
from dotenv import load_dotenv
from src.api.client import Trading212Client
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_connection():
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/")
if not api_key_id or not api_key:
logger.error("API Key ID or API Key is missing in .env")
return
client = Trading212Client(api_key_id, api_key, base_url)
try:
logger.info(f"Connecting to Trading212 at {base_url}...")
account_info = client.get_account_info()
logger.info("Successfully connected!")
logger.info(f"Account Info: {account_info}")
except Exception as e:
logger.error(f"Connection failed: {e}")
if __name__ == "__main__":
test_connection()