Beispiel #1
0
        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);
        }
Beispiel #2
0
        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}");
            }
        }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        /// <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;
            }
        }