/// <summary> /// Queue a trade based on a proposal, the callback must return the trade execution /// which will be used to update the allocation. /// </summary> /// <param name="p">TradeProposal to be verified.</param> /// <param name="tradeCallback">Trade callback to be executed if verification was successful.</param> /// <returns>Boolean indicating successful execution of the callback.</returns> public virtual ResponseObject <OrderUpdate> QueueTrade(TradeProposal p, Func <OrderUpdate> tradeCallback) { Guard.Argument(_allocation).NotNull("Initialise allocations first"); Guard.Argument(p).NotNull(); var alloc = GetAvailableFunds(p.From.Symbol); if (alloc.Free < p.From.Free || alloc.Locked < p.From.Locked) { _logger.LogCritical($"Got trade proposal for ({p.From}, but allocation " + $"showed only ({alloc}) was available\n" + "Trade will not be executed."); return(ResponseObject.OrderRefused); } // Let the provider execute the trade and save the execution report var order = tradeCallback(); // TradingProvider can give a null execution report when it decides not to trade. // if this happens the portfolio will be checked against the remote using an 'empty' or 'monoid' trade execution. if (order is null) { _logger.LogWarning("TradingProvider implementation returned a null OrderUpdate"); return(ResponseObject.OrderPlacementFailed("Implementation returned a null OrderUpdate")); } TradeExecution exec = TradeExecution.FromOrder(order); // Update the local information UpdateAllocation(exec); return(new ResponseObject <OrderUpdate>(order)); }
public void ConstructorHappyFlow() { var pair = TradingPairTests.GetTradingPair("BNB", "ETH", 20); var proposal = new TradeProposal(pair, new Balance(new Currency("BNB"), 1, 0.00000001M)); Assert.Equal(1, proposal.From.Free); Assert.Equal(0.00000001M, proposal.From.Locked); }
public void ConstructorPerformsRounding() { var pair = TradingPairTests.GetTradingPair("BNB", "ETH", 2); var balance = new Balance(new Currency("BNB"), 42.1234569M, 0.000000000099M); var proposal = new TradeProposal(pair, balance); Assert.Equal(42.12M, proposal.From.Free); Assert.Equal(0, proposal.From.Locked); }
/// <inheritdoc /> public ResponseObject <OrderUpdate> QueueTrade(TradeProposal p, Func <OrderUpdate> tradeCallback) { if (p.From.Symbol == new Currency(RefuseCoin)) { return(ResponseObject.OrderRefused); } return(new ResponseObject <OrderUpdate>(tradeCallback())); }
/// <summary> /// Place a buy market order given a non base quantity. /// </summary> /// <param name="pair">TradingPair to consider.</param> /// <param name="quantity">Quantity of non base currency.</param> /// <returns>ResponseObject containing an OrderUpdate.</returns> public OrderUpdate ExecuteMarketOrderBuy(TradingPair pair, decimal quantity) { _logger.LogDebug($"Executing buy market order with pair {pair}, quantity {quantity}"); Guard.Argument(pair).NotNull(nameof(pair)); Guard.Argument(quantity).NotZero(nameof(quantity)).NotNegative(); var currency = pair.Right; var priceEstimate = _dataProvider.GetCurrentPriceTopAsk(pair); var proposal = new TradeProposal(pair, new Balance(currency, quantity * priceEstimate, 0)); var result = _allocationManager.QueueTrade(proposal, () => { var orderQuery = HelperMethods.RetryMethod( () => { var attempt = Implementation.ExecuteMarketOrder(pair, OrderSide.Buy, quantity, TradeId); if (attempt.Success) { return(attempt); } _logger.LogInformation($"Retrying with slightly lower quantity"); quantity *= 0.999M; return(new ResponseObject <OrderUpdate>(ResponseCode.Error, attempt.Message)); }, _logger); if (orderQuery.Success) { _openOrders.Add(orderQuery.Data.OrderId, orderQuery.Data); return(WaitForOrderStatus(orderQuery.Data.OrderId, OrderUpdate.OrderStatus.Filled)); } throw new OrderFailedException(orderQuery.Message); }); if (!result.Success) { throw new OrderRefusedException(result.Message); } if (Program.CommandLineArgs.Trading) { _logger.LogInformation($"Executed Market Buy: {JsonConvert.SerializeObject(result)}"); } return(result.Data .IsBuy() .IsMarket() .IsFilled()); }
/// <summary> /// Place a buy stoploss order. /// </summary> /// <param name="pair">TradingPair.</param> /// <param name="quantity">Quantity of none base currency to trade with.</param> /// <param name="price">Price to set the order at.</param> /// <returns>ResponseObject containing an OrderUpdate.</returns> public OrderUpdate PlaceStoplossBuy(TradingPair pair, decimal quantity, decimal price) { _logger.LogDebug($"Placing stoploss buy order for pair {pair}, quantity {quantity}, price {price}"); Guard.Argument(pair).NotNull(nameof(pair)); Guard.Argument(quantity).NotZero(nameof(quantity)).NotNegative(); Guard.Argument(price).NotZero(nameof(price)).NotNegative(); var currency = pair.Right; var proposal = new TradeProposal(pair, new Balance(currency, quantity * price, 0)); var result = _allocationManager.QueueTrade(proposal, () => { return(HelperMethods.RetryMethod( context => { var query = Implementation.PlaceStoplossOrder(pair, OrderSide.Buy, quantity, price, TradeId); if (query.Code == ResponseCode.ImmediateOrderTrigger) { _logger.LogWarning($"Increase stop price and retry."); price += pair.MinPriceTick * context.Iteration; // Linear price backoff context.DisableBackoff(); // No need for delays } return query; }, _logger).Data); }); if (result.Success) { _openOrders[result.Data.OrderId] = result.Data; var order = WaitForOrderStatus(result.Data.OrderId, OrderUpdate.OrderStatus.New); order.IsStopLoss() .IsBuy() .IsNew(); if (Program.CommandLineArgs.Trading) { _logger.LogInformation($"Placed Stoploss Buy: {JsonConvert.SerializeObject(result)}"); } return(result.Data); } throw new OrderRefusedException(result.Message); }
public void QueueTradeInvalid() { var alloc = MakeDefaultAllocation(); Balance balance = alloc.GetAvailableFunds(new Currency("ETH")); var proposal = new TradeProposal(TradingPair.Parse("EOSETH"), new Balance( balance.Symbol, balance.Free + 1, balance.Locked)); var result = alloc.QueueTrade(proposal, () => { Logger.LogCritical("Invalid trade is being executed"); Assert.True(false, "Trade callback of invalid trade is being executed"); return(null); }); Assert.False(result.Success, "Invalid proposal was reported as executed"); }
public void QueueTradeHappyFlow() { var alloc = MakeDefaultAllocation(); // Get most valuable asset from backtesting settings. decimal total = alloc.GetAvailableFunds(new Currency("ETH")).Free; decimal quote = alloc.GetAvailableFunds(new Currency("EOS")).Free; Balance balance = new Balance(new Currency("ETH"), total, 0); var proposal = new TradeProposal(TradingPair.Parse("EOSETH"), balance); var result = alloc.QueueTrade(proposal, () => { Logger.LogInformation($"Trading all of the {proposal.From.Free}{proposal.From.Symbol}"); var order = new OrderUpdate( orderId: 0, tradeId: 0, orderStatus: OrderUpdate.OrderStatus.New, orderType: OrderUpdate.OrderTypes.Market, createdTimestamp: 0, setPrice: 1, side: OrderSide.Buy, pair: TradingPair.Parse("EOSETH"), setQuantity: proposal.From.Free) { AverageFilledPrice = 1, FilledQuantity = proposal.From.Free, LastFillPrice = 1, LastFillIncrement = proposal.From.Free, }; return(order); }); Assert.True(result.Success, "Valid trade was declared invalid"); Assert.Equal(total - proposal.From.Free, alloc.GetAvailableFunds(balance.Symbol).Free); Assert.Equal(quote + proposal.From.Free, alloc.GetAvailableFunds(new Currency("EOS")).Free); }
/// <summary> /// Place a buy limit order given a non base quantity and a target price. /// </summary> /// <param name="pair">TradingPair to consider.</param> /// <param name="quantity">Quantity of non base currency to trade with.</param> /// <param name="price">Price to set order at.</param> /// <returns>ResponseObject containing an OrderUpdate.</returns> public OrderUpdate PlaceLimitOrderSell(TradingPair pair, decimal quantity, decimal price) { _logger.LogDebug($"Placing limit sell order with pair {pair}, quantity {quantity}, price {price}"); Guard.Argument(pair).NotNull(nameof(pair)); Guard.Argument(quantity).NotZero(nameof(quantity)).NotNegative(); Guard.Argument(price).NotZero(nameof(price)).NotNegative(); var currency = pair.Left; var proposal = new TradeProposal(pair, new Balance(currency, quantity, 0)); var result = _allocationManager.QueueTrade(proposal, () => { return(HelperMethods.RetryMethod( () => Implementation.PlaceLimitOrder( pair, OrderSide.Sell, quantity, price, TradeId), _logger).Data); }); if (result.Success) { _openOrders[result.Data.OrderId] = result.Data; var order = WaitForOrderStatus(result.Data.OrderId, OrderUpdate.OrderStatus.New); order.IsLimit() .IsSell() .IsNew(); if (Program.CommandLineArgs.Trading) { _logger.LogInformation($"Placed Limit Sell: {JsonConvert.SerializeObject(result)}"); } return(order); } throw new OrderRefusedException(result.Message); }
/// <summary> /// Place a sell market order given a non base quantity. /// </summary> /// <param name="pair">TradingPair to consider.</param> /// <param name="quantity">Quantity of non base currency.</param> /// <returns>ResponseObject containing an OrderUpdate.</returns> public OrderUpdate ExecuteMarketOrderSell(TradingPair pair, decimal quantity) { _logger.LogDebug($"Executing sell market order with pair {pair}, quantity {quantity}"); Guard.Argument(pair).NotNull(nameof(pair)); Guard.Argument(quantity).NotZero(nameof(quantity)).NotNegative(); var currency = pair.Left; var proposal = new TradeProposal(pair, new Balance(currency, quantity, 0)); var result = _allocationManager.QueueTrade(proposal, () => { var orderQuery = HelperMethods.RetryMethod( () => Implementation.ExecuteMarketOrder(pair, OrderSide.Sell, quantity, TradeId), _logger); if (orderQuery.Success) { _openOrders.Add(orderQuery.Data.OrderId, orderQuery.Data); return(WaitForOrderStatus(orderQuery.Data.OrderId, OrderUpdate.OrderStatus.Filled)); } throw new OrderFailedException(orderQuery.Message); }); if (!result.Success) { throw new OrderRefusedException(result.Message); } if (Program.CommandLineArgs.Trading) { _logger.LogInformation($"Executed Market Sell: {JsonConvert.SerializeObject(result)}"); } return(result.Data .IsSell() .IsMarket() .IsFilled()); }
public void QueueTradeReportZero() { var alloc = MakeDefaultAllocation(); Balance balance = alloc.GetAvailableFunds(new Currency("ETH")); var proposal = new TradeProposal(TradingPair.Parse("EOSETH"), balance); var result = alloc.QueueTrade(proposal, () => new OrderUpdate( 0, 0, OrderUpdate.OrderStatus.Filled, OrderUpdate.OrderTypes.Limit, 0, 0, OrderSide.Buy, TradingPair.Parse("EOSETH"), 0)); Assert.True(result.Success, "Valid proposal was not executed"); // Assert that the allocation was not mutated Assert.Equal(proposal.From.Free, alloc.GetAvailableFunds(proposal.From.Symbol).Free); Assert.Equal(proposal.From.Locked, alloc.GetAvailableFunds(proposal.From.Symbol).Locked); }