Overall Statistics
Total Orders
119
Average Win
0.60%
Average Loss
-0.53%
Compounding Annual Return
3.197%
Drawdown
3.600%
Expectancy
0.412
Start Equity
100000
End Equity
107869.72
Net Profit
7.870%
Sharpe Ratio
-1.358
Sortino Ratio
-1.355
Probabilistic Sharpe Ratio
46.093%
Loss Rate
34%
Win Rate
66%
Profit-Loss Ratio
1.15
Alpha
-0.047
Beta
0.151
Annual Standard Deviation
0.023
Annual Variance
0.001
Information Ratio
-1.14
Tracking Error
0.115
Treynor Ratio
-0.208
Total Fees
$71.00
Estimated Strategy Capacity
$890000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
1.60%
from AlgorithmImports import *

class LimitOrderAlphaStrategy(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2023, 1, 1)
        self.SetEndDate(2025, 5, 28)
        self.SetCash(100000)
        
        self.spy = self.AddEquity("SPY", Resolution.Hour).Symbol
        
        self.atr = self.ATR(self.spy, 14, MovingAverageType.Simple, Resolution.Hour)
        self.window_size = 20
        self.window = RollingWindow[TradeBar](self.window_size)
        
        self.last_order_ticket = None
        self.last_order_time = None
        self.order_ttl = timedelta(hours=4)
        
        self.Schedule.On(self.DateRules.EveryDay(self.spy), 
                         self.TimeRules.Every(TimeSpan.FromHours(1)), 
                         self.EvaluateLimitOrderStrategy)

    def OnData(self, data: Slice):
        if data.Bars.ContainsKey(self.spy):
            self.window.Add(data[self.spy])

    def EvaluateLimitOrderStrategy(self):
        if self.window.Count < self.window_size or not self.atr.IsReady:
            return
        
        price = self.Securities[self.spy].Price
        volatility = self.atr.Current.Value

        # Cancel stale limit order
        if self.last_order_ticket and self.Time - self.last_order_time > self.order_ttl:
            self.last_order_ticket.Cancel("Canceled: TTL expired")
            self.last_order_ticket = None

        # Don't place new order if one is pending
        if self.last_order_ticket and self.last_order_ticket.Status in [OrderStatus.Submitted, OrderStatus.PartiallyFilled]:
            return

        holdings = self.Portfolio[self.spy].Quantity
        recent_closes = [bar.Close for bar in self.window]
        avg_price = sum(recent_closes) / len(recent_closes)

        direction = 0
        threshold = 0.005  # 0.5% instead of 2%

        if price < avg_price * (1 - threshold) and holdings == 0:
            direction = 1  # Buy
        elif price > avg_price * (1 + threshold) and holdings > 0:
            direction = -1  # Sell

        self.Debug(f"{self.Time} | Price: {price:.2f}, Avg: {avg_price:.2f}, ATR: {volatility:.2f}, Dir: {direction}")

        if direction == 0:
            return

        quantity = self.CalculateOrderQuantity(self.spy, 0.2)
        if quantity == 0:
            return

        # Tighter limit buffer
        buffer = volatility * 0.2

        if direction == 1:
            limit_price = price - buffer
            self.last_order_ticket = self.LimitOrder(self.spy, quantity, round(limit_price, 2), "Buy limit order")
        else:
            limit_price = price + buffer
            self.last_order_ticket = self.LimitOrder(self.spy, -holdings, round(limit_price, 2), "Sell limit order")

        self.last_order_time = self.Time
        self.Debug(f"Submitted {'BUY' if direction == 1 else 'SELL'} limit at {round(limit_price, 2)}")

    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            self.Debug(f"FILLED: {orderEvent.Symbol}, Qty: {orderEvent.FillQuantity}, Price: {orderEvent.FillPrice}")