Exemplo n.º 1
0
        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));
        }
Exemplo n.º 2
0
        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."
                });
            }
        }
Exemplo n.º 3
0
        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));
        }
Exemplo n.º 4
0
        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);
        }
Exemplo n.º 5
0
        public ActionResult Delete(TradeRequestModel request)
        {
            Trade trade = db.Trades.Find(request.TradeID);

            db.Trades.Remove(trade);
            db.SaveChanges();
            return(Json(new { Success = true }));
        }
Exemplo n.º 6
0
        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 }));
        }
Exemplo n.º 8
0
        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));
        }
Exemplo n.º 9
0
        /// <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);
        }
Exemplo n.º 10
0
        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));
        }
Exemplo n.º 11
0
 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);
         }
     });
 }
Exemplo n.º 12
0
        /// <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;
                }
            }
        }