fix: simplify logging for systemd compatibility and add ATR-based Stop Loss padding
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force unbuffered output for systemd/logging
|
||||||
|
os.environ['PYTHONUNBUFFERED'] = '1'
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import pytz
|
import pytz
|
||||||
@@ -13,47 +18,22 @@ from src.strategy.touch_turn import TouchTurnStrategy
|
|||||||
from src.execution.manager import ExecutionManager
|
from src.execution.manager import ExecutionManager
|
||||||
from scripts.find_isa_candidates import find_best_isa_tickers
|
from scripts.find_isa_candidates import find_best_isa_tickers
|
||||||
from scripts.backtest import backtest_ticker
|
from scripts.backtest import backtest_ticker
|
||||||
import sys
|
|
||||||
|
|
||||||
# Stream redirector to capture print() statements from sub-scripts into the log file
|
|
||||||
class StreamToLogger:
|
|
||||||
def __init__(self, logger, log_level=logging.INFO):
|
|
||||||
self.logger = logger
|
|
||||||
self.log_level = log_level
|
|
||||||
self.linebuf = ''
|
|
||||||
|
|
||||||
def write(self, buf):
|
|
||||||
for line in buf.rstrip().splitlines():
|
|
||||||
self.logger.log(self.log_level, line.rstrip())
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Force flush handler to ensure bot logs are written to disk immediately
|
|
||||||
class FlushHandler(logging.FileHandler):
|
|
||||||
def emit(self, record):
|
|
||||||
super().emit(record)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
# Ensure logs directory exists
|
# Ensure logs directory exists
|
||||||
os.makedirs("logs", exist_ok=True)
|
os.makedirs("logs", exist_ok=True)
|
||||||
log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log")
|
log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log")
|
||||||
|
|
||||||
# Configure logging
|
# Simple, robust logging setup
|
||||||
file_handler = FlushHandler(log_filename, mode='a')
|
|
||||||
stream_handler = logging.StreamHandler()
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s',
|
format='%(asctime)s [%(threadName)s] %(levelname)s - %(message)s',
|
||||||
handlers=[file_handler, stream_handler]
|
handlers=[
|
||||||
|
logging.FileHandler(log_filename, mode='a'),
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Redirect stdout and stderr
|
|
||||||
sys.stdout = StreamToLogger(logger, logging.INFO)
|
|
||||||
sys.stderr = StreamToLogger(logger, logging.ERROR)
|
|
||||||
|
|
||||||
def flush_logs():
|
def flush_logs():
|
||||||
for handler in logging.getLogger().handlers:
|
for handler in logging.getLogger().handlers:
|
||||||
handler.flush()
|
handler.flush()
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ class TouchTurnStrategy:
|
|||||||
4. Bullish candle -> Short at High, 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):
|
def __init__(self, ticker: str, risk_percent_atr: float = 25.0, rr_ratio: float = 2.0, min_stop_atr_pct: float = 10.0):
|
||||||
self.ticker = ticker
|
self.ticker = ticker
|
||||||
self.risk_percent_atr = risk_percent_atr
|
self.risk_percent_atr = risk_percent_atr
|
||||||
self.rr_ratio = rr_ratio
|
self.rr_ratio = rr_ratio
|
||||||
|
self.min_stop_atr_pct = min_stop_atr_pct
|
||||||
self.tz = pytz.timezone('US/Eastern')
|
self.tz = pytz.timezone('US/Eastern')
|
||||||
|
|
||||||
self.valid_setup = False
|
self.valid_setup = False
|
||||||
@@ -48,7 +49,6 @@ class TouchTurnStrategy:
|
|||||||
daily_atr = daily_data['ATRr_14'].iloc[-2] # Use yesterday's ATR
|
daily_atr = daily_data['ATRr_14'].iloc[-2] # Use yesterday's ATR
|
||||||
|
|
||||||
# 2. Fetch 15m Candle for today's opening (09:30 - 09:45)
|
# 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')
|
start_date = now.strftime('%Y-%m-%d')
|
||||||
intraday_data = yf.download(self.ticker, start=start_date, interval="15m", progress=False)
|
intraday_data = yf.download(self.ticker, start=start_date, interval="15m", progress=False)
|
||||||
|
|
||||||
@@ -59,13 +59,12 @@ class TouchTurnStrategy:
|
|||||||
if isinstance(intraday_data.columns, pd.MultiIndex):
|
if isinstance(intraday_data.columns, pd.MultiIndex):
|
||||||
intraday_data.columns = intraday_data.columns.droplevel(1)
|
intraday_data.columns = intraday_data.columns.droplevel(1)
|
||||||
|
|
||||||
# Timezone Correction: Convert the index to Eastern Time before filtering
|
# Timezone Correction
|
||||||
if intraday_data.index.tz is None:
|
if intraday_data.index.tz is None:
|
||||||
intraday_data.index = intraday_data.index.tz_localize('UTC').tz_convert(self.tz)
|
intraday_data.index = intraday_data.index.tz_localize('UTC').tz_convert(self.tz)
|
||||||
else:
|
else:
|
||||||
intraday_data.index = intraday_data.index.tz_convert(self.tz)
|
intraday_data.index = intraday_data.index.tz_convert(self.tz)
|
||||||
|
|
||||||
# The first candle of the session (09:30)
|
|
||||||
opening_candle = intraday_data.between_time('09:30', '09:30')
|
opening_candle = intraday_data.between_time('09:30', '09:30')
|
||||||
if opening_candle.empty:
|
if opening_candle.empty:
|
||||||
logger.warning(f"Opening 15m candle (09:30) not yet available for {self.ticker}")
|
logger.warning(f"Opening 15m candle (09:30) not yet available for {self.ticker}")
|
||||||
@@ -103,22 +102,33 @@ class TouchTurnStrategy:
|
|||||||
self.entry_price = high
|
self.entry_price = high
|
||||||
logger.info(f"Bullish opening candle detected. Preparing SHORT at {self.entry_price:.2f}")
|
logger.info(f"Bullish opening candle detected. Preparing SHORT at {self.entry_price:.2f}")
|
||||||
|
|
||||||
# 3. Calculate Fibonacci 38.2% Target
|
# 3. Calculate Fibonacci 38.2% Target and Stop Loss with ATR Padding
|
||||||
# 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
|
if self.direction == -1: # LONG
|
||||||
self.target_price = low + (self.range_size * 0.382)
|
self.target_price = low + (self.range_size * 0.382)
|
||||||
target_distance = self.target_price - self.entry_price
|
target_distance = self.target_price - self.entry_price
|
||||||
stop_distance = target_distance / self.rr_ratio
|
stop_distance = target_distance / self.rr_ratio
|
||||||
|
|
||||||
|
# SL Padding: Ensure SL is at least X% of ATR away to avoid noise
|
||||||
|
min_stop = daily_atr * (self.min_stop_atr_pct / 100.0)
|
||||||
|
if stop_distance < min_stop:
|
||||||
|
logger.info(f"Widening SL for {self.ticker} from {stop_distance:.2f} to min ATR distance {min_stop:.2f}")
|
||||||
|
stop_distance = min_stop
|
||||||
|
|
||||||
self.stop_loss = self.entry_price - stop_distance
|
self.stop_loss = self.entry_price - stop_distance
|
||||||
else: # SHORT
|
else: # SHORT
|
||||||
self.target_price = high - (self.range_size * 0.382)
|
self.target_price = high - (self.range_size * 0.382)
|
||||||
target_distance = self.entry_price - self.target_price
|
target_distance = self.entry_price - self.target_price
|
||||||
stop_distance = target_distance / self.rr_ratio
|
stop_distance = target_distance / self.rr_ratio
|
||||||
|
|
||||||
|
min_stop = daily_atr * (self.min_stop_atr_pct / 100.0)
|
||||||
|
if stop_distance < min_stop:
|
||||||
|
logger.info(f"Widening SL for {self.ticker} from {stop_distance:.2f} to min ATR distance {min_stop:.2f}")
|
||||||
|
stop_distance = min_stop
|
||||||
|
|
||||||
self.stop_loss = self.entry_price + stop_distance
|
self.stop_loss = self.entry_price + stop_distance
|
||||||
|
|
||||||
# 4. Calculate Percentage Move (needed for ETP scaling)
|
# 4. Calculate Percentage Move (needed for ETP scaling)
|
||||||
# We use absolute percentage move relative to the entry price
|
target_distance = abs(self.entry_price - self.target_price)
|
||||||
self.target_percent = (target_distance / self.entry_price) * 100
|
self.target_percent = (target_distance / self.entry_price) * 100
|
||||||
|
|
||||||
self.valid_setup = True
|
self.valid_setup = True
|
||||||
|
|||||||
Reference in New Issue
Block a user