Scaling Into and Out of SPY With Double-Seven Signals
Summary
This strategy scales exposure to SPY using rolling price extremes, with a 200-day moving average as a market-regime filter. When SPY is above that average, a close at a period low increases the target share allocation in increments. A close at a period high reduces it in increments, and the final bar also triggers a reduction. The default period is seven days, with up to four increments when scaling is enabled.
The document provides implementation logic and configurable settings, but no performance results or empirical comparison. The increment size is tied to the maximum number of open trades; disabling scaling changes the increment behavior. The code tracks allocation by adjusting the share percentage, rounds weights, and records balances and trade statistics. This describes a long-only equity strategy framework, not evidence that the signals are profitable. It does not specify transaction-cost assumptions, slippage, or a tested sample period, so those factors and the effects of parameter choices remain unassessed.
Key ideas
- The strategy only adds exposure when SPY is above its 200-day moving average.
- A close at a rolling period low increases the share allocation by an increment.
- A close at a rolling period high reduces exposure, and the final data bar also triggers a reduction.
- The default settings use a seven-day period and allow up to four allocation increments.
- The document gives implementation details but reports no backtest evidence or trading costs.
Tags
Full text
# scaling-in-out
# scaling-in-out
Scaling in and out of using the double-7s strategy.
1. The SPY is above its 200-day moving average.
2. The SPY closes at a X-day low, buy some shares. If it sets further
lows, buy some more.
3. If the SPY closes at a X-day high, sell some. If it sets further
highs, sell some more, etc...
## Source (MIT)
```python
"""
Scaling in and out of using the double-7s strategy.
1. The SPY is above its 200-day moving average.
2. The SPY closes at a X-day low, buy some shares. If it sets further
lows, buy some more.
3. If the SPY closes at a X-day high, sell some. If it sets further
highs, sell some more, etc...
"""
import pandas as pd
import pinkfish as pf
default_options = {
'use_adj' : False,
'use_cache' : False,
'margin' : 1,
'period' : 7,
'max_open_trades' : 4,
'enable_scale_in' : True,
'enable_scale_out' : True
}
def _round_weight(weight):
if weight > 99/100: weight = 1
elif weight < 5/100: weight = 0
else: weight = round(int(weight*100)) / 100
return weight
class Strategy:
def __init__(self, symbol, capital, start, end, options=default_options):
self.symbol = symbol
self.capital = capital
self.start = start
self.end = end
self.options = options.copy()
self.ts = None
self.rlog = None
self.tlog = None
self.dbal = None
self.stats = None
def _algo(self):
pf.TradeLog.cash = self.capital
pf.TradeLog.margin = self.options['margin']
for i, row in enumerate(self.ts.itertuples()):
date = row.Index.to_pydatetime()
close = row.close
end_flag = pf.is_last_row(self.ts, i)
max_open_trades = self.options['max_open_trades']
enable_scale_in = self.options['enable_scale_in']
enable_scale_out = self.options['enable_scale_out']
max_open_trades_buy = max_open_trades if enable_scale_in else 1
max_open_trades_sell = max_open_trades if enable_scale_out else 1
# Buy Logic
# - Buy if still open trades slots left
# and bull regime
# and price closes at period low
# and not end end_flag
if (row.regime > 0 and close == row.period_low and not end_flag):
# Get current, then set new weight
weight = self.tlog.share_percent(close)
weight += 1 / max_open_trades_buy
weight = _round_weight(weight)
self.tlog.adjust_percent(date, close, weight)
# Sell Logic
# First we check if we have any open trades, then
# - Sell if price closes at X day high.
# - Sell if end of data.
elif (self.tlog.shares > 0
and (close == row.period_high or end_flag)):
# Get current, then set new weight
weight = self.tlog.share_percent(close)
weight -= 1 / max_open_trades_sell
weight = _round_weight(weight)
self.tlog.adjust_percent(date, close, weight)
# Record daily balance.
self.dbal.append(date, close)
def run(self):
# Fetch and select timeseries.
self.ts = pf.fetch_timeseries(self.symbol, use_cache=self.options['use_cache'])
self.ts = pf.select_tradeperiod(self.ts, self.start, self.end,
use_adj=self.options['use_adj'])
# Add technical indicator: 200 day sma regime filter.
self.ts['regime'] = pf.CROSSOVER(self.ts, timeperiod_fast=1, timeperiod_slow=200)
# Add technical indicators: X day high, and X day low.
self.ts['period_high'] = pd.Series(self.ts.close).rolling(self.options['period']).max()
self.ts['period_low'] = pd.Series(self.ts.close).rolling(self.options['period']).min()
# Finalize timeseries.
self.ts, self.start = pf.finalize_timeseries(self.ts, self.start,
dropna=True, drop_columns=['open', 'high', 'low'])
# Create tlog and dbal objects.
self.tlog = pf.TradeLog(self.symbol)
self.dbal = pf.DailyBal()
# Run algo, get logs, and get stats.
self._algo()
self._get_logs()
self._get_stats()
def _get_logs(self):
self.rlog = self.tlog.get_log_raw()
self.tlog = self.tlog.get_log()
self.dbal = self.dbal.get_log(self.tlog)
def _get_stats(self):
self.stats = pf.stats(self.ts, self.tlog, self.dbal, self.capital)
```Shown in full with attribution under the source's licence. Licence: MIT
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.