Overall Statistics |
Total Trades 1380 Average Win 0.8% Average Loss -4.54% Compounding Annual Return 9.159% Drawdown 59.400% Expectancy 0.031 Net Profit 276.411% Sharpe Ratio 0.5 Loss Rate 12% Win Rate 88% Profit-Loss Ratio 0.18 Alpha 0.095 Beta 0.31 Annual Standard Deviation 0.228 Annual Variance 0.052 Information Ratio 0.2 Tracking Error 0.26 Treynor Ratio 0.367 |
namespace QuantConnect { /* * QuantConnect University * Martingale Position Sizing Experiment * * Martingale is a [risky/gambling] technique which can be applied to trading: * When a trade is going against you, double down and flip the position (long -> short). * If the position is still going against you continue flipping direction until you win. * * Typically martingale curves are perfectly straight, until they drop off sharply a cliff! * This is because they hide the intra-trade risk and only show the closing profits. QC shows the full * equity curve exposing true martingale risks. */ public partial class QCUMartingalePositionSizing : QCAlgorithm { //Algorithm Settings: decimal peakTroughDeltaBeforeFlip = 0.03m; // Percentage below high watermark before we flip our position decimal targetProfit = 0.02m; //Target profit for strategy. When achieve this exit. //Algorithm Working Variables: string symbol = "SPY"; decimal magnitudeDirection = 0.2m, tradeStringProfit = 0m, leverage = 4m; DateTime exitDate = new DateTime(); RelativeStrengthIndexCustom rsi = new RelativeStrengthIndexCustom(14); //Set up the initial algorithm backtesting settings: public override void Initialize() { SetStartDate(2000, 1, 1); SetEndDate(DateTime.Now.Date.AddDays(-1)); SetCash(25000); AddSecurity(SecurityType.Equity, symbol, Resolution.Minute, true, leverage, false); } ///<summary> /// Loss per share to exit/flip direction on the asset ///</summary> private decimal MaxLossPerShare { get { //Simpler calc for max loss per share: max loss % * stock price. return Portfolio[symbol].AveragePrice * peakTroughDeltaBeforeFlip; } } ///<summary> ///Sum of the martingale flip-losses + current unrealised profit ///</summary> private decimal UnrealizedTradeStringProfit { get { return tradeStringProfit + Portfolio.TotalUnrealizedProfit; } } ///<summary> ///Short hand bool for detecting if the algorithm has reached minimum profit target // -> Profit on holdings rather than cash -- makes gains achievable. ///</summary> private bool MinimumProfitTargetReached { get { return (Portfolio.TotalUnrealizedProfit / Math.Abs(Portfolio.TotalUnleveredAbsoluteHoldingsCost)) > targetProfit; } } ///<summary> /// Enter the market, monitor the loss/peak gain and code up a fake stop. ///</summary> public void OnData(TradeBars data) { TradeBar SPY = data[symbol]; decimal price = data[symbol].Close; //Update calculation for SPY-RSI: rsi.AddSample(SPY); //Don't trade on same day we just exited. if (exitDate.Date == Time.Date) return; //We dont have stock yet: this is a completely dumb entry strategy so lets just go long to kick it off. if (!Portfolio.HoldStock) { ScanForEntry(SPY); return; } //We have stock, but scan for a profit taking opportunity: if (MinimumProfitTargetReached) { ScanForExit(SPY); return; } //Finally we're not in green, but not flipped yet: monitor the loss to change direction: if (Math.Abs(price - Portfolio[symbol].AveragePrice) > MaxLossPerShare && Portfolio.TotalUnrealizedProfit < 0) { SetMagnitudeDirection(-2); Flip(); } } ///<summary> /// Scan for an entry signal to invest. ///</summary> public void ScanForEntry(TradeBar SPY) { //Once we have enough data, start the Entry detection. if (rsi.Ready) { if (rsi.RSI > 70) { magnitudeDirection = -0.2m; SetHoldings(symbol, -magnitudeDirection); //Over bought Log("Entry-Short: " + magnitudeDirection + " Holdings: " + Portfolio[symbol].Quantity); } else if (rsi.RSI < 30) { magnitudeDirection = 0.2m; SetHoldings(symbol, magnitudeDirection); //Over sold Log("Entry-Long: " + magnitudeDirection + " Holdings: " + Portfolio[symbol].Quantity); } } } ///<summary> /// For now dumb exit; ///</summary> public void ScanForExit(TradeBar SPY) { Log("Exit: " + magnitudeDirection + " Realized Profit/Loss: " + UnrealizedTradeStringProfit.ToString("C")); Liquidate(); tradeStringProfit = 0; magnitudeDirection = 0.2m; exitDate = Time.Date; return; } ///<summary> /// Set and Normalise MagnitudeDirection Multiplier ///</summary> public void SetMagnitudeDirection(decimal multiplier) { //Apply multiplier: magnitudeDirection = magnitudeDirection * multiplier; decimal direction = magnitudeDirection / Math.Abs(magnitudeDirection); //Normalize Max Investment to Max Leverage if (Math.Abs(magnitudeDirection) > leverage) { magnitudeDirection = direction * leverage; } //Normalize Minimum Investment to 20%; if (Math.Abs(magnitudeDirection) < 0.2m) { magnitudeDirection = direction * 0.2m; } } ///<summary> /// Flip & Invert our Holdings When We're Wrong: ///</summary> public void Flip() { //Record the loss tradeStringProfit += Portfolio.TotalUnrealisedProfit; exitDate = Time.Date; SetHoldings(symbol, magnitudeDirection); Log("Flip: " + magnitudeDirection + " Holdings: " + Portfolio[symbol].Quantity + " String Loss: " + tradeStringProfit.ToString("C")); } } }
using System.Collections.Concurrent; namespace QuantConnect { /* * Relative Strength Index Indicator: * * 100 * RSI = 100 - ------------ * 1 + RS * * Where RS = Avg of X Period Close Up / Absolute(Avg) X of Period Close Down. * */ public class RelativeStrengthIndexCustom { //Public Access to the RSI Output public decimal RSI { get { return (100 - (100 / (1 + _rs))); } } //Public Access to Know if RSI Indicator Ready public bool Ready { get { return (_upward.Count >= _period) && (_downward.Count >= _period); } } //Private Class Variables: private decimal _rs = 0; private bool _ema = false; private decimal _period = 14; private decimal _joinBars = 1; private Candle _superCandle = new Candle(); private Candle _previousCandle = new Candle(); private FixedSizedQueue<decimal> _downward = new FixedSizedQueue<decimal>(0); private FixedSizedQueue<decimal> _upward = new FixedSizedQueue<decimal>(0); private decimal _upwardSum = 0, _avgUpward = 0; private decimal _downwardSum = 0, _avgDownward = 0; //Initialize the RSI with 'period' candles public RelativeStrengthIndexCustom(int period, int joinBars = 1, bool useEMA = false) { //Range check variables: if (period < 2) period = 2; //Class settings: _period = (decimal)period; // How many samples is the RSI? _ema = useEMA; // Use the EMA average for RSI _joinBars = joinBars; // Join multiple tradebars together //Remember the upward and downward movements in a FIFO queue: _upward = new FixedSizedQueue<decimal>(period); _downward = new FixedSizedQueue<decimal>(period); //Online implementation of SMA - needs moving sum of all components: _upwardSum = 0; _downwardSum = 0; } //Add a new sample to build the RSI Indicator: public void AddSample(TradeBar bar) { //Build a multibar candle, until join reached return. _superCandle.Update(bar); if (_superCandle.Samples < _joinBars) return; //Initialize the first loop. if (_previousCandle.Samples == 0) { _previousCandle = _superCandle; _superCandle = new Candle(); return; } //Get the difference between this bar and previous bar: decimal difference = _superCandle.Close - _previousCandle.Close; //Update the Moving Average Calculations: if (difference >= 0) { if (_ema) { _avgUpward = UpdateDirectionalEMA(ref _upward, difference); _avgDownward = UpdateDirectionalEMA(ref _downward, 0); } else { _avgUpward = UpdateDirectionalSMA(ref _upward, ref _upwardSum, difference); _avgDownward = UpdateDirectionalSMA(ref _downward, ref _downwardSum, 0); } } if (difference <= 0) { difference = Math.Abs(difference); if (_ema) { _avgUpward = UpdateDirectionalEMA(ref _upward, 0); _avgDownward = UpdateDirectionalEMA(ref _downward, difference); } else { _avgUpward = UpdateDirectionalSMA(ref _upward, ref _upwardSum, 0); _avgDownward = UpdateDirectionalSMA(ref _downward, ref _downwardSum, difference); } } //Refresh RS Factor: //RS Index Automatically Updated in the Public Property Above: if (_avgDownward != 0) { _rs = _avgUpward / _avgDownward; } else { _rs = Decimal.MaxValue - 1; } //Reset for next loop: _previousCandle = _superCandle; _superCandle = new Candle(); } // Update the moving average and fixed length queue in a generic fashion to work for up and downward movement. // Return the average. private decimal UpdateDirectionalSMA(ref FixedSizedQueue<decimal> queue, ref decimal sum, decimal sample) { //Increment Sum sum += sample; //If we've shuffled off queue, remove from sum: if(queue.Enqueue(sample)) { sum -= queue.LastDequeued; } //When less than period samples, only divide by the number of samples. if (queue.Count < _period) { return (sum / (decimal)queue.Count); } else { return (sum / _period); } } // Update the moving average and fixed length queue in a generic fashion to work for up and downward movement. // Return the average. private decimal UpdateDirectionalEMA(ref FixedSizedQueue<decimal> queue, decimal sample) { queue.Enqueue(sample); if (queue.Count == 1) { return sample; } else { return (1m / _period) * sample + ((_period - 1m) / _period) * queue.LastEnqueued; } } //Fixed length queue that dumps things off when no more space in queue. private class FixedSizedQueue<T> : ConcurrentQueue<T> { public int Size { get; private set; } public T LastDequeued { get; private set; } public T LastEnqueued {get; private set;} public bool Dequeued { get; private set; } public FixedSizedQueue(int size) { Size = size; } public new bool Enqueue(T obj) { base.Enqueue(obj); LastEnqueued = obj; Dequeued = false; lock (this) { if (base.Count > Size) { T outObj; Dequeued = base.TryDequeue(out outObj); LastDequeued = outObj; } } return Dequeued; } } /// <summary> /// Simple online "super-tradebar" generator for making an OHLC from multiple bars. /// </summary> public class Candle { public decimal Open = 0; public decimal High = Decimal.MinValue; public decimal Low = Decimal.MaxValue; public decimal Close = 0; public int Samples = 0; public void Update(TradeBar bar) { if (Open == 0) Open = bar.Open; if (High < bar.High) High = bar.High; if (Low > bar.Low) Low = bar.Low; Close = bar.Close; Samples++; } } } }