Initial commit: Touch & Turn Scalping Bot with fully automated execution, backtesting, and ISA screening
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user