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); }
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)}!"); } }
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); }