Example #1
0
        public static (decimal, decimal, decimal, decimal) MatchOrderBalanceModifications(
            MatchOrderEventEntry eventEntry)
        {
            int actionUserBaseModifier;
            int actionUserQuoteModifier;

            switch (eventEntry.ActionSide)
            {
            case OrderSide.Buy:
                actionUserBaseModifier  = 1;
                actionUserQuoteModifier = -1;
                break;

            case OrderSide.Sell:
                actionUserBaseModifier  = -1;
                actionUserQuoteModifier = 1;
                break;

            default:
                throw new InvalidOperationException();
            }

            // Action user Base
            // Action user Quote
            // Target user Base
            // Target user Quote
            return(
                actionUserBaseModifier * eventEntry.Qty,
                actionUserQuoteModifier *eventEntry.Qty *eventEntry.Price,
                -actionUserBaseModifier * eventEntry.Qty,
                -actionUserQuoteModifier * eventEntry.Qty * eventEntry.Price);
        }
Example #2
0
        public void ProcessEvent(MatchOrderEventEntry eventEntry)
        {
            _tradingOrderService.MatchOrder(eventEntry);
            var currencies    = eventEntry.Instrument.Split("_");
            var baseCurrency  = currencies[0];
            var quoteCurrency = currencies[1];

            var(actionBase, actionQuote, targetBase, targetQuote) = MatchOrderBalanceModifications(eventEntry);

            var parallelTasks = new List <Task>
            {
                _userService.ModifyBalance(
                    eventEntry.ActionUser,
                    eventEntry.ActionAccountId,
                    baseCurrency,
                    actionBase),
                _userService.ModifyBalance(
                    eventEntry.ActionUser,
                    eventEntry.ActionAccountId,
                    quoteCurrency,
                    actionQuote),
                _userService.ModifyBalance(
                    eventEntry.TargetUser,
                    eventEntry.TargetAccountId,
                    baseCurrency,
                    targetBase),
                _userService.ModifyBalance(
                    eventEntry.TargetUser,
                    eventEntry.TargetAccountId,
                    quoteCurrency,
                    targetQuote),
                _userService.ModifyReservedBalance(
                    eventEntry.ActionUser,
                    eventEntry.ActionAccountId,
                    // Unlock the opposite side of the limit order owner
                    eventEntry.ActionSide == OrderSide.Buy ? quoteCurrency : baseCurrency,
                    eventEntry.ActionSide == OrderSide.Buy ? actionQuote : actionBase),
                _userService.ModifyReservedBalance(
                    eventEntry.TargetUser,
                    eventEntry.TargetAccountId,
                    eventEntry.ActionSide == OrderSide.Buy ? baseCurrency : quoteCurrency,
                    eventEntry.ActionSide == OrderSide.Buy ? targetBase : targetQuote)
            };

            foreach (var task in parallelTasks)
            {
                task.Wait();
            }
        }
        private void AssertMatchOrderQty(
            MatchOrderEventEntry matchOrder, OrderBookEntry actionOrder, OrderBookEntry targetOrder)
        {
            if (actionOrder != null && matchOrder.ActionOrderQtyRemaining !=
                actionOrder.Qty - (actionOrder.FilledQty + matchOrder.Qty))
            {
                throw new Exception(
                          $"Integrity assertion failed! {nameof(MatchOrderEventEntry)} ID {matchOrder.Id} attempted to increase {nameof(targetOrder.FilledQty)} of action order ID {actionOrder.Id} from {actionOrder.FilledQty.ToString(CultureInfo.CurrentCulture)} by {matchOrder.Qty}, but that didn't add up to event entry-asserted value of {matchOrder.ActionOrderQtyRemaining.ToString(CultureInfo.CurrentCulture)}!");
            }

            if (matchOrder.TargetOrderQtyRemaining != targetOrder.Qty - (targetOrder.FilledQty + matchOrder.Qty))
            {
                throw new Exception(
                          $"Integrity assertion failed! {nameof(MatchOrderEventEntry)} ID {matchOrder.Id} attempted to increase {nameof(targetOrder.FilledQty)} of target order ID {targetOrder.Id} from {targetOrder.FilledQty.ToString(CultureInfo.CurrentCulture)} by {matchOrder.Qty}, but that didn't add up to event entry-asserted value of {matchOrder.TargetOrderQtyRemaining.ToString(CultureInfo.CurrentCulture)}!");
            }
        }
Example #4
0
        private (List <MatchOrderEventEntry>, decimal) PlanMatchOrdersLocked(
            string user, string accountId, string instrument, OrderSide orderSide, decimal?limitPriceValue,
            decimal quantityRemaining, long lockedEventVersionNumber, string requestId,
            Func <string, Exception> reportInvalidMessage)
        {
            var plannedEvents = new List <MatchOrderEventEntry>();
            var baseCurrency  = instrument.Split("_")[0];
            var quoteCurrency = instrument.Split("_")[1];

            // Start the process of matching relevant offers
            var matchingOffers = orderSide == OrderSide.Buy
                ? TradingOrderService.MatchSellers(limitPriceValue, instrument).Result
                : TradingOrderService.MatchBuyers(limitPriceValue, instrument).Result;

            matchingOffers.MoveNext();
            var matchingOfferBatch = matchingOffers.Current.ToList();

            while (quantityRemaining > 0 && matchingOfferBatch.Count > 0)
            {
                _logger.LogInformation(
                    $"Request {requestId} matched a batch of {matchingOfferBatch.Count} {(orderSide == OrderSide.Buy ? "buyers" : "sellers")}");
                foreach (var other in matchingOfferBatch)
                {
                    var     otherRemaining = other.Qty - other.FilledQty;
                    decimal matchedQuantity;
                    if (otherRemaining >= quantityRemaining)
                    {
                        // Entire command order remainder is consumed by the seller offer
                        matchedQuantity = quantityRemaining;
                        _logger.LogInformation(
                            $"New {instrument} {(orderSide == OrderSide.Buy ? "buy" : "sell")} limit order planning entirely matched order id {other.Id}");
                    }
                    else
                    {
                        // Fraction of order will remain, but the seller offer will be consumed
                        matchedQuantity = otherRemaining;
                        _logger.LogInformation(
                            $"New {instrument} {(orderSide == OrderSide.Buy ? "buy" : "sell")} limit order planning partially matched order id {other.Id}");
                    }

                    quantityRemaining -= matchedQuantity;

                    var matchEvent = new MatchOrderEventEntry
                    {
                        VersionNumber              = lockedEventVersionNumber,
                        ActionUser                 = user,
                        ActionAccountId            = accountId,
                        TargetOrderOnVersionNumber = other.CreatedOnVersionId,
                        TargetUser                 = other.User,
                        TargetAccountId            = other.AccountId,
                        Instrument                 = instrument,
                        Qty        = matchedQuantity,
                        ActionSide = orderSide,
                        Price      = other.LimitPrice,
                        ActionOrderQtyRemaining = quantityRemaining,
                        TargetOrderQtyRemaining = other.Qty - other.FilledQty - matchedQuantity,
                    };

                    // Calculating new balances for double-check purposes
                    var(actionBaseMod, actionQuoteMod, targetBaseMod, targetQuoteMod) =
                        TradeEventProcessor.MatchOrderBalanceModifications(matchEvent);
                    try
                    {
                        matchEvent.ActionBaseNewBalance =
                            UserService.GetBalanceAndReservedBalance(
                                matchEvent.ActionUser, matchEvent.ActionAccountId, baseCurrency
                                ).Item1 + actionBaseMod;
                        matchEvent.ActionQuoteNewBalance =
                            UserService.GetBalanceAndReservedBalance(
                                matchEvent.ActionUser, matchEvent.ActionAccountId, quoteCurrency
                                ).Item1 + actionQuoteMod;
                        matchEvent.TargetBaseNewBalance =
                            UserService.GetBalanceAndReservedBalance(
                                matchEvent.TargetUser, matchEvent.TargetAccountId, baseCurrency
                                ).Item1 + targetBaseMod;
                        matchEvent.TargetQuoteNewBalance =
                            UserService.GetBalanceAndReservedBalance(
                                matchEvent.TargetUser, matchEvent.TargetAccountId, quoteCurrency
                                ).Item1 + targetQuoteMod;
                    }
                    catch (Exception e)
                    {
                        // This can happen if a user didn't generate his balances yet, so it's not a fatal error
                        throw reportInvalidMessage(
                                  $"There was a problem with your coin balances. {e.GetType().Name}: {e.Message}");
                    }

                    plannedEvents.Add(matchEvent);
                    if (quantityRemaining == 0)
                    {
                        break;
                    }
                }

                if (quantityRemaining == 0)
                {
                    break;
                }


                // Keep the iteration going in order to find further matching orders as long as remaining qty > 0
                if (!matchingOffers.MoveNext())
                {
                    break;
                }

                matchingOfferBatch = matchingOffers.Current.ToList();
            }

            return(plannedEvents, quantityRemaining);
        }
        internal void MatchOrder(MatchOrderEventEntry matchOrder)
        {
            _logger.LogDebug("Called match order @ version number " + matchOrder.VersionNumber);

            // Old incorrect way:
            // In order to find actionOrderId, we must go a little roundabout way
//            var matchOrderRelatedCreateOrder = _eventHistoryRepository.Events<CreateOrderEventEntry>().Find(
//                Builders<CreateOrderEventEntry>.Filter.Eq(e => e.VersionNumber, matchOrder.VersionNumber)
//            ).First();
//            var actionOrderId = matchOrderRelatedCreateOrder.Id;

            var now = matchOrder.EntryTime;
            // Action order is not used in case it's a market order
            var actionOrder = OrderBook.Find(
                Builders <OrderBookEntry> .Filter.Eq(e => e.CreatedOnVersionId, matchOrder.VersionNumber)
                ).SingleOrDefault();
            var targetOrder = OrderBook.Find(
                Builders <OrderBookEntry> .Filter.Eq(e => e.CreatedOnVersionId, matchOrder.TargetOrderOnVersionNumber)
                ).Single();

            AssertMatchOrderQty(matchOrder, actionOrder, targetOrder);

            if (actionOrder != null)
            {
                if (matchOrder.ActionOrderQtyRemaining == 0)
                {
                    OrderBook.DeleteOne(
                        Builders <OrderBookEntry> .Filter.Eq(e => e.CreatedOnVersionId, matchOrder.VersionNumber)
                        );
                    // The entire order quantity was filled
                    InsertOrderHistoryEntry(actionOrder.Qty, actionOrder, OrderStatus.Filled, now);
                }
                else
                {
                    OrderBook.UpdateOne(
                        Builders <OrderBookEntry> .Filter.Eq(e => e.CreatedOnVersionId, matchOrder.VersionNumber),
                        Builders <OrderBookEntry> .Update.Set(
                            e => e.FilledQty, actionOrder.Qty - matchOrder.ActionOrderQtyRemaining)
                        );
                }
            }
            else if (
                // This condition can be completely deleted without a worry, it's just a double-check
                ((CreateOrderEventEntry)_eventHistoryService
                 .FindByVersionNumber(matchOrder.VersionNumber)
                 .First()
                ).Type != OrderType.Market
                )
            {
                // We don't have to update the OrderHistoryEntry, because it already contains the matched quantity
                throw new Exception(
                          $"Match order id {matchOrder.Id} did not have action being a limit order, and it was not a market order");
            }

            if (matchOrder.TargetOrderQtyRemaining == 0)
            {
                OrderBook.DeleteOne(
                    Builders <OrderBookEntry> .Filter.Eq(e => e.CreatedOnVersionId, matchOrder.TargetOrderOnVersionNumber)
                    );
                // The entire order quantity was filled
                InsertOrderHistoryEntry(targetOrder.Qty, targetOrder, OrderStatus.Filled, now);
            }
            else
            {
                OrderBook.UpdateOne(
                    Builders <OrderBookEntry> .Filter.Eq(
                        e => e.CreatedOnVersionId, matchOrder.TargetOrderOnVersionNumber),
                    Builders <OrderBookEntry> .Update.Set(
                        e => e.FilledQty, targetOrder.Qty - matchOrder.TargetOrderQtyRemaining)
                    );
            }

            TransactionHistory.InsertMany(
                new[]
            {
                new TransactionHistoryEntry
                {
                    ExecutionTime = now,
                    User          = matchOrder.ActionUser,
                    AccountId     = matchOrder.ActionAccountId,
                    Instrument    = matchOrder.Instrument,
                    Side          = targetOrder.Side == OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy,
                    OrderId       = actionOrder?.Id,
                    // The entire quantity was filled
                    FilledQty = matchOrder.Qty,
                    Price     = targetOrder.LimitPrice,
                },
                new TransactionHistoryEntry
                {
                    ExecutionTime = now,
                    User          = targetOrder.User,
                    AccountId     = targetOrder.AccountId,
                    Instrument    = targetOrder.Instrument,
                    Side          = targetOrder.Side,
                    OrderId       = targetOrder.Id,
                    // The entire quantity was filled
                    FilledQty = matchOrder.Qty,
                    Price     = targetOrder.LimitPrice,
                }
            }
                );

            _logger.LogDebug("Persisted match order @ version number " + matchOrder.VersionNumber);
        }