public void AddRate(Rate newRate) { if(newRate == null) { throw new ArgumentNullException(nameof(newRate)); } var roundedTime = newRate.Time.RoundDown(period); if(roundedTime < this.timestamp) { throw new Exception("Data stream is not advancing in time"); } if(timestamp == null) { this.timestamp = roundedTime; this.open = newRate.MidPoint; } if(roundedTime > this.timestamp) { OnNewCandleCreated(new CandleBuilderEventArgs(CurrentCandle)); this.timestamp = roundedTime; this.open = newRate.MidPoint; } this.high = newRate.MidPoint >= this.high.GetValueOrDefault(newRate.MidPoint) ? newRate.MidPoint : high; this.low = newRate.MidPoint <= this.low.GetValueOrDefault(newRate.MidPoint) ? newRate.MidPoint : low; this.close = newRate.MidPoint; }
public void SetRate(Rate newRate, Rate accountCurrencyRate) { //TODO: Check margin call //TODO: Handle error on leverage and insuficient funds //TODO: Handle target profit this.currentRate = newRate; this.currentAccountCurrencyRate = accountCurrencyRate; var currentTrade = this.trades.FirstOrDefault(); if (currentTrade == null) { return; } decimal amountToCompare; if (currentTrade.Side == OrderSideBuy) //In case of a long trade { if (currentTrade.TrailingStop > 0) { var newTrailingAmount = newRate.Bid - currentTrade.TrailingStop * newRate.QuoteCurrency.GetPipFraction(); currentTrade.TrailingAmount = newTrailingAmount >= currentTrade.TrailingAmount ? newTrailingAmount : currentTrade.TrailingAmount; amountToCompare = currentTrade.TrailingAmount; } else if (currentTrade.StopLoss > 0) { currentTrade.TrailingAmount = 0; amountToCompare = currentTrade.StopLoss; } else { amountToCompare = 0; //TODO: To code a strategy without stops } } else //In case of short trade { if (currentTrade.TrailingStop > 0) { var newTrailingAmount = newRate.Ask + currentTrade.TrailingStop * newRate.QuoteCurrency.GetPipFraction(); currentTrade.TrailingAmount = newTrailingAmount < currentTrade.TrailingAmount ? newTrailingAmount : currentTrade.TrailingAmount; amountToCompare = currentTrade.TrailingAmount; } else if (currentTrade.StopLoss > 0) { currentTrade.TrailingAmount = 0; amountToCompare = currentTrade.StopLoss; } else { amountToCompare = 0; } } this.LiquidateTrade(newRate, accountCurrencyRate, currentTrade, amountToCompare); }
public override Rate GetRate(string instrument) { var currentCandle = this.GetCurrentCandle(this.nbOfCalls); var rateValue = currentCandle.FullRange * (decimal)this.rateGenerator.NextDouble() + currentCandle.Low; var minuteFraction = (double)(this.nbOfCalls % this.ticksInPeriod) / this.ticksInPeriod; this.nbOfCalls++; this.CurrentRate = new Rate { Ask = rateValue, Bid = rateValue, Instrument = instrument, Time = currentCandle.Timestamp.AddMinutes(this.periodInMinutes * minuteFraction) }; if (this.currentTrade == null) { return this.CurrentRate; } var amountToCompare = 0m; if (this.currentTrade.Side == OrderSideBuy) { if (this.currentTrade.TrailingStop > 0) { var newTrailingAmount = this.CurrentRate.Bid - this.currentTrade.TrailingStop * 0.0001m; this.currentTrade.TrailingAmount = newTrailingAmount >= this.currentTrade.TrailingAmount ? newTrailingAmount : this.currentTrade.TrailingAmount; amountToCompare = this.currentTrade.TrailingAmount; } else { this.currentTrade.TrailingAmount = 0; amountToCompare = this.currentTrade.StopLoss; } } else { if (this.currentTrade.TrailingStop > 0) { var newTrailingAmount = this.CurrentRate.Ask + this.currentTrade.TrailingStop * 0.0001m; this.currentTrade.TrailingAmount = newTrailingAmount < this.currentTrade.TrailingAmount ? newTrailingAmount : this.currentTrade.TrailingAmount; amountToCompare = this.currentTrade.TrailingAmount; } else { this.currentTrade.TrailingAmount = 0; amountToCompare = this.currentTrade.StopLoss; } } var gainLoss = 0m; if (this.currentTrade.Side == OrderSideBuy) { if (this.CurrentRate.Bid < amountToCompare) { gainLoss = (this.currentTrade.Price - amountToCompare) / DolarsByPip; Console.WriteLine("Stop loss triggered=>Gain/Loss={0}", gainLoss); this.balancePips += gainLoss * this.currentTrade.Units * DolarsByPip; Console.WriteLine("{1} - Balance = {0}", this.balancePips, this.currentTrade.Time); this.currentTrade = null; } } else { if (this.CurrentRate.Ask > amountToCompare) { gainLoss = (amountToCompare - this.currentTrade.Price) / DolarsByPip; Console.WriteLine("Stop loss triggered=>Gain/Loss={0}", gainLoss); this.balancePips += gainLoss * this.currentTrade.Units * DolarsByPip; Console.WriteLine("{1} - Balance = {0}", this.balancePips, this.currentTrade.Time); this.currentTrade = null; } } return this.CurrentRate; }
private void LiquidateTrade(Rate newRate, Rate accountCurrencyRate, Trade currentTrade, decimal referencePrice) { var gainLoss = 0m; if (currentTrade.Side == OrderSideBuy) { if ((newRate.Bid - referencePrice) > DebouncingLimit) return; gainLoss = referencePrice - currentTrade.Price; } else { if (referencePrice - newRate.Ask > DebouncingLimit) return; gainLoss = currentTrade.Price - referencePrice; } Console.WriteLine("Stop loss triggered=>Gain/Loss={0} pips", gainLoss / newRate.QuoteCurrency.GetPipFraction()); var gainLossInQuoteCurrency = gainLoss * currentTrade.Units; var gainLossConversionRate = this.GetAccountCurrencyRate(newRate, accountCurrencyRate); this.accountInformation.Balance += gainLossInQuoteCurrency * gainLossConversionRate; Console.WriteLine("{1} - Balance = {0}", this.accountInformation.Balance, currentTrade.Time); this.trades.Remove(currentTrade); }
private bool ValidateIndicatorsState(Rate currentRate) { return this.candles.Count >= MinNbOfCandles && (this.candles.Last().Timestamp - currentRate.Time).Minutes <= this.PeriodInMinutes; }
private decimal GetAccountCurrencyRate(Rate newRate, Rate accountCurrencyRate) { var quoteInstrument = newRate.QuoteCurrency.Safe().Trim().ToUpper(); var baseInstrument = this.accountInformation.AccountCurrency.Safe().Trim().ToUpper(); if (quoteInstrument == baseInstrument) return 1.00m; var price = (accountCurrencyRate.Ask + accountCurrencyRate.Bid)/2.00m; if (accountCurrencyRate.BaseCurrency == this.accountInformation.AccountCurrency) { return 1.00m / price; } return price; }
private void PlaceOrder(string side, Rate rate) { var stopLossDistance = this.CalculateStopLossDistanceInPips(side, rate.QuoteCurrency, rate); var positionSizeInUnits = this.CalculatePositionSize(stopLossDistance, side, rate); //TODO: Decide if to user lower-upper bounds or just market order and assume the slippage var newOrder = new Order { Instrument = this.Instrument, Units = positionSizeInUnits, Side = side, OrderType = OrderTypeMarket, TrailingStop = stopLossDistance, AcountId = this.AccountId, Timestamp = rate.Time }; this.tradingAdapter.PlaceOrder(newOrder); Trace.TraceInformation("Order placed :{0}", JsonConvert.SerializeObject(newOrder)); }
private bool ShouldSetStopLoss(Trade currentTrade, Rate currentRate) { if (currentTrade.StopLoss > 0) { return false; } var pipFraction = currentTrade.QuoteCurrency.GetPipFraction(); decimal? spreadPips = (currentRate.Ask - currentRate.Bid) / (2 * pipFraction); var cushionDeltaPrice = (spreadPips + SlippagePips + MarginalGainPips) * pipFraction; switch (currentTrade.Side) { case OrderSideBuy: return currentTrade.TrailingAmount >= currentTrade.Price + cushionDeltaPrice; default: return currentTrade.TrailingAmount <= currentTrade.Price - cushionDeltaPrice; } }
private bool CanGoShort(Rate rate) { var slowSmaLowValue = this.slowSmaLow.Values.LastOrDefault(); var fastEmaLowValue = this.fastEmaLow.Values.LastOrDefault(); if (slowSmaLowValue < rate.Bid) { return false; } if (fastEmaLowValue < rate.Bid) { return false; } var currentCandle = this.candles.LastOrDefault(); if (currentCandle == null) { return false; } if (!currentCandle.IsDown) { return false; } if (fastEmaLowValue < currentCandle.Open) { return false; } if (currentCandle.IsReversal(GetThreshold())) { return false; } var previousCandle = this.candles.TakeLast(2).Skip(1).FirstOrDefault(); if (previousCandle == null) { return false; } if (previousCandle.IsReversal(GetThreshold())) { return false; } if (ConfirmPreviousCandleForBid(previousCandle, currentCandle)) { return true; } previousCandle = this.candles.TakeLast(3).Skip(2).FirstOrDefault(); if (previousCandle == null) { return false; } return ConfirmPreviousCandleForBid(previousCandle, currentCandle); }
private decimal CalculateStopLossDistanceInPips(string side, string quoteCurrency, Rate currentRate) { var pipFraction = quoteCurrency.GetPipFraction(); if (side == OrderSideBuy) { var lowLimit = this.fastEmaLow.Values.LastOrDefault(); return Math.Ceiling((currentRate.Ask - lowLimit) / pipFraction); } var highLimit = this.fastEmaHigh.Values.LastOrDefault(); return Math.Abs((highLimit - currentRate.Bid) / pipFraction); }
private int CalculatePositionSize(decimal stopLoss, string side, Rate currentRate) { var accountInformation = this.tradingAdapter.GetAccountInformation(this.AccountId); //Account currency should be USD var balance = accountInformation.Balance.SafeParseDecimal().GetValueOrDefault(); var maxRiskAmount = balance * BaseRiskPercentage; var pipFraction = currentRate.QuoteCurrency.GetPipFraction(); var useRate = side == OrderSideBuy ? currentRate.Ask : currentRate.Bid; //Instead of hardcoding the account currency, better to read it from the api. var maxRiskAmountInQuote = currentRate.QuoteCurrency == "USD" ? maxRiskAmount : maxRiskAmount * useRate; var positionSize = (maxRiskAmountInQuote / stopLoss) / pipFraction; var accountMarginRate = accountInformation.MarginRate.SafeParseDecimal().GetValueOrDefault(); var accountLeverage = 1m; if (accountMarginRate > 0) { accountLeverage = 1m / accountMarginRate; } var availablePositionSize = Math.Min(positionSize, balance * accountLeverage * useRate); //TODO: Use Kelly Criterior to calculate position size //TODO: Implement criterias for minimum stop loss condition return (int)availablePositionSize; }
/// <summary> /// Should happen every minute because the check needs to be frequent, /// but the candles are queried in the predefined timeframe /// </summary> public void CheckRate(Rate newRate) { Trace.CorrelationManager.ActivityId = Guid.NewGuid(); validations.Clear(); if (this.CurrentRate != null && newRate.Time < this.CurrentRate.Time) { //This is likely to happen in a backtest or practice scenario validations.AddErrorMessage("Rate timer is going backwards"); } else { this.CurrentRate = newRate; } candleBuilder.AddRate(newRate); Trace.TraceInformation("New rate : {0}", JsonConvert.SerializeObject(newRate)); if (!this.isbacktesting) { var systemTimeDiff = this.dateProvider.GetCurrentUtcDate() - newRate.Time.ToUniversalTime(); if (systemTimeDiff.TotalMinutes >= MaxRateStaleTime) { validations.AddErrorMessage("Price timer lagging behind current time"); } } if (!this.ValidateIndicatorsState(newRate)) { validations.AddErrorMessage("Incomplete indicator values"); } if (this.tradingAdapter.HasOpenTrade(this.AccountId)) { var currentTrade = this.tradingAdapter.GetOpenTrade(this.AccountId); //Not sure if doing this or just keep the trailing stop if (!validations.IsValid && currentTrade.StopLoss > 0) { Trace.TraceInformation("Closing trade because of validation errors and open trade with stop loss"); //If the trade is open with stop loss, it is likely that we can close the trade with a profit this.tradingAdapter.CloseTrade(this.AccountId, currentTrade.Id); Trace.TraceError(validations.ToString()); return; } if (!validations.IsValid) { Trace.TraceInformation("Exit early because of validation errors and open trade"); Trace.TraceError(validations.ToString()); return; } if (this.ShouldCloseTrade(currentTrade)) { Trace.TraceInformation("Closing trade"); this.tradingAdapter.CloseTrade(this.AccountId, currentTrade.Id); return; } if (this.ShouldSetStopLoss(currentTrade, newRate)) { Trace.TraceInformation("Break even"); var updatedTrade = new Trade { Id = currentTrade.Id, StopLoss = currentTrade.TrailingAmount, TrailingStop = 0, TakeProfit = 0, AccountId = this.AccountId }; this.tradingAdapter.UpdateTrade(updatedTrade); return; } Trace.TraceInformation("Exit early because of open trade, trailing amount {0}", currentTrade.TrailingAmount); return; } if (!validations.IsValid) { Trace.TraceInformation("Exit early because of validation errors"); Trace.TraceError(validations.ToString()); return; } if (this.tradingAdapter.HasOpenOrder(this.AccountId)) { Trace.TraceInformation("Open order"); return; } if (!this.IsTradingDay()) { Trace.TraceInformation("Non trading day"); return; } if (!this.IsTradingTime()) { Trace.TraceInformation("Non trading time"); return; } var currentSpread = Math.Abs(newRate.Ask - newRate.Bid) * (1.00m / newRate.QuoteCurrency.GetPipFraction()); if (currentSpread > MaxSpread) { Trace.TraceInformation($"Not enough liquidity, spread = {currentSpread}"); return; } var currentAdxValue = this.adx.Values.LastOrDefault() * 100; if (currentAdxValue < AdxTrendLevel) { Trace.TraceInformation("ADX ({0}) below threshold ({1})", currentAdxValue, AdxTrendLevel); return; } //TODO: Set history to calculate adx direction in app config //Calculating adx trend with more than one candle back will cause to lost many chances to enter and will enter late var previousAdxValue = this.adx.Values.TakeLast(2).Skip(1).FirstOrDefault() * 100m; if (currentAdxValue <= previousAdxValue) { Trace.TraceInformation("ADX not increasing {0} => {1}", previousAdxValue, currentAdxValue); return; } if (this.CanGoLong(newRate)) { this.PlaceOrder(OrderSideBuy, newRate); return; } if (this.CanGoShort(newRate)) { this.PlaceOrder(OrderSideSell, newRate); } }
//Return structure containig reason why cannot go long to improve testability public bool CanGoLong(Rate rate) { var slowSmaHighValue = this.slowSmaHigh.Values.LastOrDefault(); var fastEmaHighValue = this.fastEmaHigh.Values.LastOrDefault(); if (slowSmaHighValue > rate.Ask) { return false; } if (fastEmaHighValue > rate.Ask) { return false; } var currentCandle = this.candles.LastOrDefault(); if (currentCandle == null) { return false; } if (!currentCandle.IsUp) { return false; } if (fastEmaHighValue > currentCandle.Open) { return false; } if (currentCandle.IsReversal(GetThreshold())) { return false; } var previousCandle = this.candles.TakeLast(2).Skip(1).FirstOrDefault(); if (previousCandle == null || previousCandle.IsReversal(GetThreshold())) { return false; } if (!this.ConfirmPreviousCandleForAsk(previousCandle, currentCandle)) { previousCandle = this.candles.TakeLast(3).Skip(2) .FirstOrDefault(); if (previousCandle == null || previousCandle.IsReversal(GetThreshold())) { return false; } if (!this.ConfirmPreviousCandleForAsk(previousCandle, currentCandle)) { return false; } } return true; }