public async Task AddValidTradeToQueue() { var context = Helpers.GetMockContext(); var stateManager = new MockReliableStateManager(); var service = new Fulfillment(context, stateManager); var ask = new Order("user1", CurrencyPair.GBPUSD, 10, 10); var bid = new Order("user2", CurrencyPair.GBPUSD, 10, 10); var settlement = new Order(ask.Pair, bid.Amount, ask.Price); var request = new TradeRequestModel { Ask = ask, Bid = bid, Settlement = settlement }; var tradeId = await service.AddTradeAsync(request); var expected = new Trade(tradeId, ask, bid, settlement); var queue = await stateManager.TryGetAsync <IReliableConcurrentQueue <Trade> >(Fulfillment.TradeQueueName); var actual = (await queue.Value.TryDequeueAsync(new MockTransaction(stateManager, 1))).Value; Assert.True(expected.Equals(actual)); }
public async Task <IActionResult> PostAsync([FromBody] TradeRequestModel tradeRequest) { try { var tradeId = await this.fulfillment.AddTradeAsync(tradeRequest); return(this.Ok(tradeId)); } catch (InvalidTradeRequestException ex) { return(new ContentResult { StatusCode = 400, Content = ex.Message }); } catch (FabricNotPrimaryException) { return(new ContentResult { StatusCode = 410, Content = "The primary replica has moved. Please re-resolve the service." }); } catch (MaxPendingTradesExceededException ex) { return(new ContentResult { StatusCode = 429, Content = $"{ex.Message}" }); } catch (FabricException) { return(new ContentResult { StatusCode = 503, Content = "The service was unable to process the request. Please try again." }); } }
public void ThrowIfDuplicateOrder() { var askId = "ask1"; var bidId = "bid1"; var sellerId = "user1"; var buyerId = "user2"; var tradeRequest = new TradeRequestModel(); tradeRequest.Ask = new Order(askId, sellerId, CurrencyPair.GBPUSD, 10, 10, DateTime.UtcNow); tradeRequest.Bid = new Order(bidId, buyerId, CurrencyPair.GBPUSD, 10, 10, DateTime.UtcNow); tradeRequest.Settlement = new Order(CurrencyPair.GBPUSD, 10, 10); Trade trade = tradeRequest; var sellerCurrencyAmounts = new Dictionary <string, double>(); sellerCurrencyAmounts.Add(CurrencyPair.GBPUSD.GetBuyerWantCurrency(), 100); var seller = new User(sellerId, "seller", sellerCurrencyAmounts, new List <string>() { trade.Id }); var buyerCurrencyAmounts = new Dictionary <string, double>(); buyerCurrencyAmounts.Add(CurrencyPair.GBPUSD.GetSellerWantCurrency(), 100); var buyer = new User(buyerId, "buyer", buyerCurrencyAmounts, new List <string>() { trade.Id }); Assert.Throws <DuplicateBidException>(() => Validation.ThrowIfNotValidTrade(tradeRequest, seller, buyer)); }
public void NotThrowIfValidOrder() { var askId = "ask1"; var bidId = "bid1"; var sellerId = "user1"; var buyerId = "user2"; var sellerCurrencyAmounts = new Dictionary <string, double>(); sellerCurrencyAmounts.Add(CurrencyPair.GBPUSD.GetBuyerWantCurrency(), 100); var seller = new User(sellerId, "seller", sellerCurrencyAmounts, null); var buyerCurrencyAmounts = new Dictionary <string, double>(); buyerCurrencyAmounts.Add(CurrencyPair.GBPUSD.GetSellerWantCurrency(), 100); var buyer = new User(buyerId, "buyer", buyerCurrencyAmounts, null); var tradeRequest = new TradeRequestModel(); tradeRequest.Ask = new Order(askId, sellerId, CurrencyPair.GBPUSD, 10, 10, DateTime.UtcNow); tradeRequest.Bid = new Order(bidId, buyerId, CurrencyPair.GBPUSD, 10, 10, DateTime.UtcNow); tradeRequest.Settlement = new Order(CurrencyPair.GBPUSD, 10, 10); Validation.ThrowIfNotValidTrade(tradeRequest, seller, buyer); }
public ActionResult Delete(TradeRequestModel request) { Trade trade = db.Trades.Find(request.TradeID); db.Trades.Remove(trade); db.SaveChanges(); return(Json(new { Success = true })); }
public ActionResult List(TradeRequestModel request) { int?buildingId = request.BuildingID ?? null; if (buildingId != null) { return(Json( new { Trades = db.Trades.Where(fk => fk.BuildingID == buildingId) })); } return(Json(new { Success = false })); }
public ActionResult List(TradeRequestModel request) { int?buildingId = request.BuildingID ?? null; if (buildingId != null) { // Select Buildings where foreign key is equal to current User Id var trades = db.Trades.Where(fk => fk.BuildingID == buildingId).ToList(); return(Json(new { Trades = trades })); } return(Json(new { Success = false })); }
public async Task ThrowIfAskQuantityIsLowerThanBidQuantity() { var context = Helpers.GetMockContext(); var stateManager = new MockReliableStateManager(); var service = new Fulfillment(context, stateManager); var ask = new Order("user1", CurrencyPair.GBPUSD, 5, 10); var bid = new Order("user2", CurrencyPair.GBPUSD, 100, 10); var request = new TradeRequestModel { Ask = ask, Bid = bid, }; await Assert.ThrowsAsync <InvalidTradeRequestException>(() => service.AddTradeAsync(request)); }
/// <summary> /// Checks whether the provided trade /// meets the validity requirements. If it /// does, adds it the trade queue. /// </summary> /// <param name="trade"></param> /// <returns></returns> public async Task <string> AddTradeAsync(TradeRequestModel tradeRequest, CancellationToken cancellationToken = default(CancellationToken)) { Validation.ThrowIfNotValidTradeRequest(tradeRequest); // REQUIRED, DO NOT REMOVE. var pendingTrades = await Trades.CountAsync(); if (pendingTrades > maxPendingTrades) { ServiceEventSource.Current.ServiceMaxPendingLimitHit(); throw new MaxPendingTradesExceededException(pendingTrades); } var tradeId = await this.Trades.EnqueueAsync(tradeRequest, cancellationToken); return(tradeId); }
public async Task ThrowIfAskValueIsHigherThanBidValue() { var context = Helpers.GetMockContext(); var stateManager = new MockReliableStateManager(); var service = new Fulfillment(context, stateManager); var ask = new Order("user1", CurrencyPair.GBPUSD, 40, 150); var bid = new Order("user2", CurrencyPair.GBPUSD, 40, 100); var settlement = new Order(ask.Pair, bid.Amount, ask.Price); var request = new TradeRequestModel { Ask = ask, Bid = bid, Settlement = settlement }; await Assert.ThrowsAsync <InvalidTradeRequestException>(() => service.AddTradeAsync(request)); }
public async Task AddTooManyTradesToQueue() { var context = Helpers.GetMockContext(); var stateManager = new MockReliableStateManager(); var service = new Fulfillment(context, stateManager); var ask = new Order("user1", CurrencyPair.GBPUSD, 10, 10); var bid = new Order("user2", CurrencyPair.GBPUSD, 10, 10); var settlement = new Order(ask.Pair, bid.Amount, ask.Price); var request = new TradeRequestModel { Ask = ask, Bid = bid, Settlement = settlement }; await Assert.ThrowsAsync <MaxPendingTradesExceededException>(async() => { for (int i = 0; i < 20; i++) { await service.AddTradeAsync(request); } }); }
/// <summary> /// RunAsync is called by the Service Fabric runtime once the service is ready /// to begin processing. /// We use it select the maximum bid and minimum ask. We then /// match these in a trade and hand them over to the fulfillment /// service to execute the exchange. /// </summary> /// <param name="cancellationToken"></param> /// <returns></returns> protected override async Task RunAsync(CancellationToken cancellationToken) { client.DefaultRequestHeaders .Accept .Add(new MediaTypeWithQualityHeaderValue("application/json")); var rand = new Random(); Order maxBid = null; Order minAsk = null; Order leftOverAsk = null; Order settlement = null; while (true) { cancellationToken.ThrowIfCancellationRequested(); try { // Get the maximum bid and minimum ask from our secondary index. maxBid = this.bids.GetMaxOrder(); minAsk = this.asks.GetMinOrder(); if (maxBid == null && minAsk == null) { // Help avoid CPU spinning await Task.Delay(500, cancellationToken); continue; } if (maxBid != null) { // Enforce TTL: Remove unmatched bids after 5mins. if (maxBid.Timestamp.AddMinutes(5) < DateTime.UtcNow) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Removing expired bid {maxBid.Id}"); await this.bids.RemoveAsync(maxBid, cancellationToken); maxBid = null; } } if (minAsk != null) { // Enforce TTL: Remove unmatched asks after 5mins. if (minAsk.Timestamp.AddMinutes(5) < DateTime.UtcNow) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Removing expired asks {minAsk.Id}"); await this.asks.RemoveAsync(minAsk, cancellationToken); minAsk = null; } } ServiceEventSource.Current.ServiceMessage(this.Context, $"Checking for new match"); if (IsMatch(maxBid, minAsk)) { ServiceEventSource.Current.ServiceMessage(this.Context, $"New match made between Bid {maxBid.Id} and Ask {minAsk.Id}"); MetricsLog?.OrderMatched(maxBid, minAsk); try { // We split the ask incase the seller had a bigger // amount of currency than the buyer wished to buy. // The cost per unit is kept consistent between the // original ask and any left over asks. (var settledOrder, var leftOver) = SettleTrade(maxBid, minAsk); settlement = settledOrder; leftOverAsk = leftOver; } catch (InvalidAskException) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Dropping invalid Ask"); await this.asks.RemoveAsync(minAsk, cancellationToken); continue; } catch (InvalidBidException) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Dropping invalid Bid"); await this.asks.RemoveAsync(maxBid, cancellationToken); continue; } if (leftOverAsk != null) { try { await AddAskAsync(leftOverAsk, cancellationToken); // Add the left over ask as a new order } catch (FabricNotPrimaryException ex) { // If the fabric is not primary we should // return control back to the platform. ServiceEventSource.Current.ServiceException(this.Context, "Failed to add left over ask as fabric is not primary", ex); return; } catch (FabricNotReadableException) { // Fabric is not in a readable state. This // is a transient error and should be retried. ServiceEventSource.Current.ServiceMessage(this.Context, $"Fabric is not currently readable, aborting and will retry"); await BackOff(cancellationToken); continue; } catch (FabricException ex) { ServiceEventSource.Current.ServiceException(this.Context, "Failed to add left over ask as fabric exception throw", ex); if (IsTransientError(ex.ErrorCode)) { // Transient error, we can backoff and retry await BackOff(cancellationToken); continue; } // Non transient error, re-throw throw ex; } catch (MaxOrdersExceededException ex) { // If we have hit the maximum number of // orders - drop the left over ask to // avoid deadlock. The seller will have // to resubmit the ask manually. ServiceEventSource.Current.ServiceException(this.Context, "Failed to add left over ask as max orders exceeded", ex); } } var trade = new TradeRequestModel { Ask = minAsk, // Original ask order Bid = maxBid, // Original bid order Settlement = settlement, // Settled order }; // Send the trade request to our fulfillment service to complete. var content = new StringContent(JsonConvert.SerializeObject(trade), Encoding.UTF8, "application/json"); HttpResponseMessage res = null; try { var randomPartitionId = NextInt64(rand); // Send to any partition - it doesn't matter. res = await client.PostAsync($"http://localhost:{reverseProxyPort}/Exchange/Fulfillment/api/trades?PartitionKey={randomPartitionId.ToString()}&PartitionKind=Int64Range", content, cancellationToken); } catch (HttpRequestException ex) { // Exception thrown when attempting to make HTTP POST to fulfillment API. // Possibly a DNS, network connectivity or timeout issue. We'll treat it // as transient, back off and retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"HTTP error sending trade to fulfillment service, error: '{ex.Message}'"); await BackOff(cancellationToken); continue; } catch (TimeoutException ex) { // Call to Fulfillment service timed out, likely because it is currently down or // under extreme load ServiceEventSource.Current.ServiceMessage(this.Context, $"Call to fulfillment service timed out with error: '{ex.Message}'"); await BackOff(cancellationToken); continue; } catch (TaskCanceledException ex) { // Task has been cancelled, assume SF want's to close us. ServiceEventSource.Current.ServiceMessage(this.Context, $"Request to fulfillment service got cancelled"); var wasCancelled = ex.CancellationToken.IsCancellationRequested; if (wasCancelled) { return; } else { await BackOff(cancellationToken); continue; } } // If the response from the Fulfillment API was not 2xx if (res?.StatusCode == HttpStatusCode.BadRequest) { // Invalid request - fail the orders and // remove them from our order book. ServiceEventSource.Current.ServiceMessage(this.Context, $"Error response from fulfillment service '{res.StatusCode}'"); using (var tx = this.StateManager.CreateTransaction()) { await this.asks.RemoveAsync(tx, minAsk, cancellationToken); await this.bids.RemoveAsync(tx, maxBid, cancellationToken); await tx.CommitAsync(); } continue; } if (res?.StatusCode == HttpStatusCode.Gone) { // Transient error indicating the service // has moved and needs to be re-resolved. // Retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"Error response from fulfillment service '{res.StatusCode}'"); continue; } if (res?.StatusCode == (HttpStatusCode)429) { // 429 is a custom error that indicates // that the service is under heavy load. // Back off and retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"Error response from fulfillment service '{res.StatusCode}'"); await BackOff(cancellationToken); continue; } if (res?.IsSuccessStatusCode == true) { using (var tx = this.StateManager.CreateTransaction()) { // If the response is successful, assume orders are safe to remove. await this.asks.RemoveAsync(tx, minAsk, cancellationToken); await this.bids.RemoveAsync(tx, maxBid, cancellationToken); await tx.CommitAsync(); // Committing our transaction will remove both the ask and the bid from our orders. ServiceEventSource.Current.ServiceMessage(this.Context, $"Removed Ask {minAsk.Id} and Bid {maxBid.Id}"); } var tradeId = await res.Content.ReadAsStringAsync(); ServiceEventSource.Current.ServiceMessage(this.Context, $"Created new trade with id '{tradeId}'"); } else { // Unhandled error condition. // Log it, back off and retry. ServiceEventSource.Current.ServiceMessage(this.Context, $"Error response from fulfillment service '{res.StatusCode}'"); await BackOff(cancellationToken); continue; } } } catch (FabricNotReadableException) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Fabric is not currently readable, aborting and will retry"); await BackOff(cancellationToken); continue; } catch (InvalidOperationException) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Invalid operation performed, aborting and will retry"); await BackOff(cancellationToken); continue; } catch (FabricNotPrimaryException) { ServiceEventSource.Current.ServiceMessage(this.Context, $"Fabric cannot perform write as it is not the primary replica"); return; } } }