From 6010f66323c032e4af056b3ed98613f8e688d65c Mon Sep 17 00:00:00 2001 From: pie Date: Mon, 1 Jun 2026 16:27:30 +0100 Subject: [PATCH] fix: simplify logging for systemd compatibility and add ATR-based Stop Loss padding --- main.py | 40 ++++++++++---------------------------- src/strategy/touch_turn.py | 26 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/main.py b/main.py index 59335c6..42f07e6 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,9 @@ import os +import sys + +# Force unbuffered output for systemd/logging +os.environ['PYTHONUNBUFFERED'] = '1' + import time import logging import pytz @@ -13,47 +18,22 @@ 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 -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 os.makedirs("logs", exist_ok=True) log_filename = datetime.now().strftime("logs/bot_%Y-%m-%d.log") -# Configure logging -file_handler = FlushHandler(log_filename, mode='a') -stream_handler = logging.StreamHandler() - +# Simple, robust logging setup logging.basicConfig( level=logging.INFO, 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__) -# Redirect stdout and stderr -sys.stdout = StreamToLogger(logger, logging.INFO) -sys.stderr = StreamToLogger(logger, logging.ERROR) - def flush_logs(): for handler in logging.getLogger().handlers: handler.flush() diff --git a/src/strategy/touch_turn.py b/src/strategy/touch_turn.py index 95dcb3e..702cc13 100644 --- a/src/strategy/touch_turn.py +++ b/src/strategy/touch_turn.py @@ -17,10 +17,11 @@ class TouchTurnStrategy: 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.risk_percent_atr = risk_percent_atr self.rr_ratio = rr_ratio + self.min_stop_atr_pct = min_stop_atr_pct self.tz = pytz.timezone('US/Eastern') self.valid_setup = False @@ -48,7 +49,6 @@ class TouchTurnStrategy: 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) @@ -59,13 +59,12 @@ class TouchTurnStrategy: if isinstance(intraday_data.columns, pd.MultiIndex): 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: intraday_data.index = intraday_data.index.tz_localize('UTC').tz_convert(self.tz) else: 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') if opening_candle.empty: logger.warning(f"Opening 15m candle (09:30) not yet available for {self.ticker}") @@ -103,22 +102,33 @@ class TouchTurnStrategy: 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 + # 3. Calculate Fibonacci 38.2% Target and Stop Loss with ATR Padding if self.direction == -1: # LONG self.target_price = low + (self.range_size * 0.382) target_distance = self.target_price - self.entry_price 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 else: # SHORT self.target_price = high - (self.range_size * 0.382) target_distance = self.entry_price - self.target_price 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 # 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.valid_setup = True