| Overall Statistics |
|
Total Orders 409 Average Win 0.22% Average Loss -0.17% Compounding Annual Return -77.425% Drawdown 22.100% Expectancy -0.710 Start Equity 100000 End Equity 78724.41 Net Profit -21.276% Sharpe Ratio -6.809 Sortino Ratio -6.685 Probabilistic Sharpe Ratio 0% Loss Rate 87% Win Rate 13% Profit-Loss Ratio 1.28 Alpha -0.582 Beta 0.27 Annual Standard Deviation 0.098 Annual Variance 0.01 Information Ratio -2.397 Tracking Error 0.147 Treynor Ratio -2.47 Total Fees $612.56 Estimated Strategy Capacity $23000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 691.98% Drawdown Recovery 0 |
# region imports
from AlgorithmImports import *
from random import Random
# endregion
class NoiseFilter:
"""An ISecurityDataFilter that perturbs prices inside the subscription pipeline.
SubscriptionFilterEnumerator.MoveNext calls this on every data point as it
leaves the subscription, BEFORE TimeSliceFactory assembles the slice. The
factory hands the SAME BaseData instance to the security cache, the
consolidator update list and the Slice, so mutating it here reaches all
three -- including consolidators, which never read the security cache.
"""
def __init__(self, seed: int, sigma: float):
self._seed = seed
self._sigma = sigma
def filter(self, vehicle: Security, data: BaseData) -> bool:
if self._sigma <= 0:
return True
# Seeded on (symbol, end_time): the trade and quote subscriptions are
# separate enumerators with no guaranteed relative order, but both
# derive the identical shock for the same bar.
rng = Random(f"{self._seed}:{data.symbol.id}:{data.end_time}")
shock = 1 + rng.gauss(0, self._sigma)
if isinstance(data, TradeBar):
data.open *= shock
data.high *= shock
data.low *= shock
data.close *= shock # Close's setter also assigns Value
elif isinstance(data, QuoteBar):
for side in (data.bid, data.ask):
if side is not None:
side.open *= shock
side.high *= shock
side.low *= shock
side.close *= shock
data.value *= shock # not derived from Bid/Ask, set it too
else:
data.value *= shock
# One shock for the whole bar means high >= max(o, c) and ask > bid are
# preserved for free -- no invariant repair needed. Independent per-field
# draws would bring both problems back.
return True # returning False DROPS the data point
class RandomizedPricesAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2020, 3, 1)
self.set_cash(100_000)
self._seed = self.get_parameter("seed", 0)
self._sigma = self.get_parameter("sigma", 0.0005)
equity = self.add_equity("SPY", Resolution.MINUTE)
self._spy = equity.symbol
equity.set_data_filter(NoiseFilter(self._seed, self._sigma))
# Registered the NORMAL way. These are driven by the consolidator path
# (AlgorithmManager.cs:443), which is fed timeSlice.ConsolidatorUpdateData
# directly and never touches the security cache -- so if these move with
# the seed, the filter really did reach the whole pipeline.
self._fast = self.ema(self._spy, 20, Resolution.MINUTE)
self._slow = self.ema(self._spy, 60, Resolution.MINUTE)
self._ten_min_close = None
self.consolidate(self._spy, timedelta(minutes=10), self._on_ten_minute)
self._audited = 0
self._audit_after = datetime(2020, 1, 15, 10, 0)
def _on_ten_minute(self, bar: TradeBar):
self._ten_min_close = bar.close
def on_data(self, data: Slice):
if not self._slow.is_ready:
return
self._audit()
invested = self.portfolio[self._spy].invested
if self._fast.current.value > self._slow.current.value and not invested:
self.set_holdings(self._spy, 1)
elif self._fast.current.value < self._slow.current.value and invested:
self.liquidate(self._spy)
def _audit(self):
"""Snapshot the pipeline at a fixed instant so runs can be diffed.
Nothing here can compare against the original data -- by the time the
algorithm sees anything it is already perturbed. The comparison is
across runs: baseline (sigma=0) vs each seed.
"""
if self._audited >= 2 or self.time < self._audit_after:
return
self._audited += 1
n = self._audited
s = self.securities[self._spy]
self.set_runtime_statistic(f"a{n} time", str(self.time))
self.set_runtime_statistic(f"a{n} ema20", f"{self._fast.current.value:.6f}")
self.set_runtime_statistic(f"a{n} ema60", f"{self._slow.current.value:.6f}")
self.set_runtime_statistic(f"a{n} 10min close", f"{self._ten_min_close}")
self.set_runtime_statistic(f"a{n} cache close", f"{s.close}")
self.set_runtime_statistic(f"a{n} cache price", f"{s.price}")
self.set_runtime_statistic(f"a{n} cache bid", f"{s.bid_price}")
self.set_runtime_statistic(f"a{n} cache ask", f"{s.ask_price}")