private async Task <IList <EventEntry> > PlanStopOrderEvents( string user, string accountId, string instrument, decimal quantity, OrderSide orderSide, decimal stopPriceValue, string durationType, decimal?duration, decimal?stopLoss, decimal?takeProfit, string requestId, Func <string, Exception> reportInvalidMessage) { if (quantity <= 0) { throw reportInvalidMessage("You cannot create an order with a quantity of 0 or less"); } if (stopPriceValue <= 0 || stopLoss.HasValue && stopLoss.Value <= 0 || takeProfit.HasValue && takeProfit.Value <= 0) { throw reportInvalidMessage("You cannot create a stop order with a price of 0 or less"); } var stopOrderEvent = new CreateOrderEventEntry { User = user, AccountId = accountId, Instrument = instrument, Qty = quantity, Side = orderSide, Type = OrderType.Stop, LimitPrice = null, StopPrice = stopPriceValue, DurationType = durationType, Duration = duration, StopLoss = stopLoss, TakeProfit = takeProfit, }; var plannedEvents = new List <EventEntry> { stopOrderEvent }; VersionControl.ExecuteUsingFixedVersion(currentVersionNumber => { var eventVersionNumber = currentVersionNumber + 1; stopOrderEvent.VersionNumber = eventVersionNumber; AssertUnreservedBalance( user, accountId, instrument, quantity, orderSide, stopPriceValue, reportInvalidMessage); }); return(plannedEvents); }
private async Task <IList <EventEntry> > PlanMarketOrderEvents( string user, string accountId, string instrument, decimal quantity, OrderSide orderSide, string durationType, decimal?duration, decimal?stopLoss, decimal?takeProfit, string requestId, Func <string, Exception> reportInvalidMessage) { if (quantity <= 0) { throw reportInvalidMessage("You cannot create an order with a quantity of 0 or less"); } if (stopLoss.HasValue && stopLoss.Value <= 0 || takeProfit.HasValue && takeProfit.Value <= 0) { throw reportInvalidMessage("You cannot create a market order with a price of 0 or less"); } var marketOrderEvent = new CreateOrderEventEntry { User = user, AccountId = accountId, Instrument = instrument, Qty = quantity, //FilledMarketOrderQty Side = orderSide, Type = OrderType.Market, //LimitPrice, StopPrice = null, DurationType = durationType, Duration = duration, StopLoss = stopLoss, TakeProfit = takeProfit, }; var plannedEvents = new List <EventEntry> { marketOrderEvent }; VersionControl.ExecuteUsingFixedVersion(currentVersionNumber => { var eventVersionNumber = currentVersionNumber + 1; marketOrderEvent.VersionNumber = eventVersionNumber; // In the case of buying we cannot be sure that the entire quantity will get matched if (orderSide == OrderSide.Sell) { AssertUnreservedBalance( user, accountId, instrument, quantity, orderSide, null, reportInvalidMessage); } var matchedOrders = PlanMatchOrdersLocked( user, accountId, instrument, orderSide, null, quantity, eventVersionNumber, requestId, reportInvalidMessage ); if (matchedOrders.Item1.Count == 0) { throw reportInvalidMessage("Cannot execute a market order, as there are no other orders"); } matchedOrders.Item1.ForEach(plannedEvents.Add); marketOrderEvent.FilledMarketOrderQty = quantity - matchedOrders.Item2; marketOrderEvent.LimitPrice = matchedOrders.Item1.Last().Price; }); return(plannedEvents); }
protected override async Task ListenForBlockchainEvents() { // Copy the collection as it's modified during iteration var keysIteration = new List <string>(_knownPublicKeyBalances.Keys); // Listen for deposits foreach (var publicKey in keysIteration) { decimal balance; try { try { balance = GetBalance(publicKey).Result - _lockedPublicKeyBalances[publicKey]; } catch (AggregateException e) { foreach (var ex in e.InnerExceptions) { _logger.LogWarning(ex.GetType().Name + ": " + ex.Message); } throw; } } catch (Exception) { _logger.LogWarning( $"Could not receive blockchain balance of {ThisCoinSymbol} public key {publicKey}, " + $"service is probably offline, skipping the wallet and waiting a few seconds"); Task.Delay(2000).Wait(); continue; } var oldBalance = GetCurrentlyCachedBalance(publicKey).Result; if (balance == oldBalance) { continue; } // Generate event does not change, so we won't request it multiple times var generateEvent = _eventHistoryService.FindWalletGenerateByPublicKey(publicKey); if (balance < oldBalance) { _logger.LogError( "Detected a negative value deposit event. This is really unexpected, check your implementation stability. Invoking a value-driven deposit revocation!"); await new WalletCommandProcessor( _versionControl, _eventHistoryService, _walletOperationService, _logger) .RetryPersist(lastAttempt => { IList <EventEntry> result = null; _versionControl.ExecuteUsingFixedVersion(currentVersionNumber => { var eventVersionNumber = currentVersionNumber + 1; try { balance = GetBalance(publicKey).Result - _lockedPublicKeyBalances[publicKey]; } catch (Exception) { _logger.LogError( $"Could not receive blockchain balance of {ThisCoinSymbol} public key {publicKey}, " + $"service is probably offline, waiting a few seconds"); Task.Delay(5000).Wait(); return; } // Example: balance is 30 minus locked 10. It was previously 40. // 20 + returned 10 - 40 = negativeDeposit of -10. // It's below zero, so we invoke a negative deposit. // New balance of 30 still includes the locked withdrawal of 10. var negativeDeposit = balance + _lockedPublicKeyBalances[publicKey] - GetCurrentlyCachedBalance(publicKey).Result; result = negativeDeposit < 0 ? new List <EventEntry> { new WalletDepositEventEntry { VersionNumber = eventVersionNumber, DepositWalletPublicKey = publicKey, User = generateEvent.User, AccountId = generateEvent.AccountId, CoinSymbol = ThisCoinSymbol, LastWalletPublicKey = null, DepositQty = negativeDeposit, NewSourcePublicKeyBalance = balance + _lockedPublicKeyBalances[publicKey] } } : null; }); if (result == null) { _logger.LogInformation( "Negative value deposit event has resolved itself, canceling the value-driven deposit revocation!"); } return(result, persistedEvents => { _logger.LogInformation( "Successfully persisted negative value deposit event"); }); }); continue; } var retry = false; do { long lastTriedVersion = 0; _versionControl.ExecuteUsingFixedVersion(currentVersion => { if (retry && currentVersion == lastTriedVersion) { _logger.LogInformation( $"Blockchain deposit @ version number {currentVersion + 1} waiting for new events' integration..."); Task.Delay(1000).Wait(); return; } // We have acquired a lock, so the deposit event may have been already processed try { balance = GetBalance(publicKey).Result - _lockedPublicKeyBalances[publicKey]; } catch (Exception) { _logger.LogError( $"Could not receive blockchain balance of {ThisCoinSymbol} public key {publicKey}, " + $"service is probably offline, waiting a few seconds"); Task.Delay(5000).Wait(); retry = true; return; } lastTriedVersion = currentVersion; oldBalance = GetCurrentlyCachedBalance(publicKey).Result; if (balance <= oldBalance) { _logger.LogInformation( "Blockchain deposit canceled, it was already processed by a newer event"); retry = false; return; } _logger.LogInformation( $"{(retry ? "Retrying" : "Trying")} a detected {ThisCoinSymbol} blockchain deposit event @ public key {publicKey}, balance {oldBalance} => {balance}, event persistence @ version number {currentVersion + 1}"); var deposit = new WalletDepositEventEntry { User = generateEvent.User, AccountId = generateEvent.AccountId, CoinSymbol = ThisCoinSymbol, DepositQty = balance - oldBalance, NewSourcePublicKeyBalance = balance, LastWalletPublicKey = _walletOperationService.GetLastPublicKey(publicKey), DepositWalletPublicKey = publicKey, VersionNumber = currentVersion + 1 }; IList <EventEntry> persist = new List <EventEntry> { deposit }; retry = null == _eventHistoryService.Persist(persist, currentVersion).Result; }); }while (retry); _logger.LogInformation( $"{ThisCoinSymbol} blockchain deposit event successfully persisted @ public key {publicKey}, balance {oldBalance} => {balance}"); } }
private async Task <IList <EventEntry> > PlanLimitOrderEvents( string user, string accountId, string instrument, decimal quantity, OrderSide orderSide, decimal limitPriceValue, string durationType, decimal?duration, decimal?stopLoss, decimal?takeProfit, string requestId, Func <string, Exception> reportInvalidMessage) { if (quantity <= 0) { throw reportInvalidMessage("You cannot create an order with a quantity of 0 or less"); } if (limitPriceValue <= 0 || stopLoss.HasValue && stopLoss.Value <= 0 || takeProfit.HasValue && takeProfit.Value <= 0) { throw reportInvalidMessage("You cannot create a limit order with a price of 0 or less"); } var limitOrderEvent = new CreateOrderEventEntry { User = user, AccountId = accountId, Instrument = instrument, Qty = quantity, Side = orderSide, Type = OrderType.Limit, LimitPrice = limitPriceValue, StopPrice = null, DurationType = durationType, Duration = duration, StopLoss = stopLoss, TakeProfit = takeProfit, }; var plannedEvents = new List <EventEntry> { limitOrderEvent }; var finished = false; // Note: using async prefix on the lambda caused it to never finish! VersionControl.ExecuteUsingFixedVersion(currentVersionNumber => { var eventVersionNumber = currentVersionNumber + 1; // We are now locked to a specific version number limitOrderEvent.VersionNumber = eventVersionNumber; AssertUnreservedBalance( user, accountId, instrument, quantity, orderSide, limitPriceValue, reportInvalidMessage); PlanMatchOrdersLocked( user, accountId, instrument, orderSide, limitPriceValue, quantity, eventVersionNumber, requestId, reportInvalidMessage ).Item1.ForEach(plannedEvents.Add); finished = true; }); if (finished == false) { throw new Exception($"{nameof(VersionControl.ExecuteUsingFixedVersion)} didn't finish"); } return(plannedEvents); }
/// <inheritdoc /> protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { var beforeIntegration = true; // This enables the version control semaphore _versionControl.Initialize(_currentVersion); _logger.LogInformation( $"Initialized {GetType().Name}, listening for wallet event entries to be processed"); while (!_stopped) { // We can batch events by using a second parameter, just make sure to consume the entire version var missingEvents = await _eventHistoryService.LoadMissingEvents(_currentVersion); if (missingEvents.Count > 0) { // Only integrate new events as long as no one is currently assuming a fixed current version _versionControl.IncreaseVersion(() => { foreach (var missingEvent in missingEvents) { if (missingEvent is WalletEventEntry walletEvent) { AbstractProvider.ProviderLookup[walletEvent.CoinSymbol].ProcessEvent(walletEvent); _logger.LogDebug( $"Processed {walletEvent.CoinSymbol} event {walletEvent.GetType().Name}"); } var eventVersionNumber = missingEvent.VersionNumber; if (eventVersionNumber == _currentVersion) { continue; } if (eventVersionNumber == _currentVersion + 1) { _currentVersion++; continue; } throw new Exception( $"Integrity error: the event ID {missingEvent.Id} attempted to jump version from {_currentVersion} to {eventVersionNumber}. This cannot be recovered from and requires a manual fix by administrator"); } if (beforeIntegration) { beforeIntegration = false; _walletOperationService.HideWalletsAfterVersionNumber(_currentVersion); } _logger.LogInformation($"Current version is increased to {_currentVersion}"); return(_currentVersion); }); _listeningInterval = 50; } else { if (beforeIntegration) { _versionControl.ExecuteUsingFixedVersion(_currentVersion => { beforeIntegration = false; _walletOperationService.HideWalletsAfterVersionNumber(_currentVersion); }); } if (_listeningInterval < _listeningIntervalMax) { _listeningInterval += 50; } await Task.Delay(TimeSpan.FromMilliseconds(_listeningInterval), stoppingToken); _logger.LogDebug($"{GetType().Name} is still listening for event entries..."); } } } catch (Exception e) { _logger.LogError($"{e.Message}\n{e.StackTrace}"); Program.Shutdown(); throw; } }