public MarginTradingAccount GetAccountById(string accountId) { if (string.IsNullOrWhiteSpace(accountId)) { throw new ArgumentNullException(nameof(accountId)); } if (_draftSnapshotKeeper.Initialized()) { _log.WriteInfoAsync(nameof(AccountsProvider), nameof(GetAccountById), _draftSnapshotKeeper.TradingDay.ToJson(), "Draft snapshot keeper initialized and will be used as accounts provider"); var accounts = _draftSnapshotKeeper .GetAccountsAsync() .GetAwaiter() .GetResult(); return(accounts .SingleOrDefault(a => a.Id == accountId)); } return(_accountsCacheService.TryGet(accountId)); }
public async Task <MtBackendResponse <MarginTradingAccountModel> > SetTradingCondition( [FromBody] SetTradingConditionModel model) { if (!_tradingConditionsCacheService.IsTradingConditionExists(model.TradingConditionId)) { return(MtBackendResponse <MarginTradingAccountModel> .Error( $"No trading condition {model.TradingConditionId} found in cache")); } var tradingCondition = _tradingConditionsCacheService.GetTradingCondition(model.TradingConditionId); IMarginTradingAccount account = _accountsCacheService.TryGet(model.ClientId, model.AccountId); if (account == null) { return(MtBackendResponse <MarginTradingAccountModel> .Error( $"Account for client [{model.ClientId}] with id [{model.AccountId}] was not found")); } if (tradingCondition.LegalEntity != account.LegalEntity) { return(MtBackendResponse <MarginTradingAccountModel> .Error( $"Account for client [{model.ClientId}] with id [{model.AccountId}] has LegalEntity " + $"[{account.LegalEntity}], but trading condition wit id [{tradingCondition.Id}] has " + $"LegalEntity [{tradingCondition.LegalEntity}]")); } account = await _accountManager.SetTradingCondition(model.ClientId, model.AccountId, model.TradingConditionId); return(MtBackendResponse <MarginTradingAccountModel> .Ok(account.ToBackendContract())); }
private void ContinueOrFinishLiquidation(string operationId, LiquidationOperationData data, ICommandSender sender) { void FinishWithReason(string reason) => sender.SendCommand(new FinishLiquidationInternalCommand { OperationId = operationId, CreationTime = _dateService.Now(), Reason = reason, LiquidationType = data.LiquidationType, ProcessedPositionIds = data.ProcessedPositionIds, LiquidatedPositionIds = data.LiquidatedPositionIds, }, _cqrsContextNamesSettings.TradingEngine); var account = _accountsCacheService.TryGet(data.AccountId); if (account == null) { sender.SendCommand(new FailLiquidationInternalCommand { OperationId = operationId, CreationTime = _dateService.Now(), Reason = "Account does not exist", LiquidationType = data.LiquidationType, }, _cqrsContextNamesSettings.TradingEngine); return; } var accountLevel = account.GetAccountLevel(); if (data.LiquidationType == LiquidationType.Forced) { if (!_ordersCache.Positions.GetPositionsByAccountIds(data.AccountId) .Any(x => (string.IsNullOrWhiteSpace(data.AssetPairId) || x.AssetPairId == data.AssetPairId) && x.OpenDate < data.StartedAt && (data.Direction == null || x.Direction == data.Direction))) { FinishWithReason("All positions are closed"); } else { LiquidatePositionsIfAnyAvailable(operationId, data, sender); } return; } if (accountLevel < AccountLevel.StopOut) { FinishWithReason($"Account margin level is {accountLevel}"); } else { LiquidatePositionsIfAnyAvailable(operationId, data, sender); } }
public void RemoveLiquidationStateIfNeeded(string accountId, string reason, string liquidationOperationId = null, LiquidationType liquidationType = LiquidationType.Normal) { var account = _accountsCacheService.TryGet(accountId); if (account == null) { return; } if (!string.IsNullOrEmpty(account.LiquidationOperationId) && (liquidationType == LiquidationType.Forced || account.GetAccountLevel() != AccountLevel.StopOut)) { _accountsCacheService.TryFinishLiquidation(accountId, reason, liquidationOperationId); } }
public async Task TestAccountCreation() { var account = Accounts[1]; var time = DateService.Now(); var accountsProjection = AssertEnv(); await accountsProjection.Handle(new AccountChangedEvent(time, "test", account, AccountChangedEventTypeContract.Created)); var createdAccount = _accountsCacheService.TryGet(account.Id); Assert.True(createdAccount != null); Assert.AreEqual(account.Id, createdAccount.Id); Assert.AreEqual(account.Balance, createdAccount.Balance); Assert.AreEqual(account.TradingConditionId, createdAccount.TradingConditionId); }
//has to be beyond global lock public void Validate(Order order) { #region Validate input params if (_assetDayOffService.IsDayOff(order.Instrument)) { throw new ValidateOrderException(OrderRejectReason.NoLiquidity, "Trades for instrument are not available"); } if (order.Volume == 0) { throw new ValidateOrderException(OrderRejectReason.InvalidVolume, "Volume cannot be 0"); } var asset = _assetPairsCache.TryGetAssetPairById(order.Instrument); if (asset == null) { throw new ValidateOrderException(OrderRejectReason.InvalidInstrument, "Instrument not found"); } var account = _accountsCacheService.TryGet(order.ClientId, order.AccountId); if (account == null) { throw new ValidateOrderException(OrderRejectReason.InvalidAccount, "Account not found"); } if (!_quoteCashService.TryGetQuoteById(order.Instrument, out var quote)) { throw new ValidateOrderException(OrderRejectReason.NoLiquidity, "Quote not found"); } #endregion order.AssetAccuracy = asset.Accuracy; order.AccountAssetId = account.BaseAssetId; order.TradingConditionId = account.TradingConditionId; //check ExpectedOpenPrice for pending order if (order.ExpectedOpenPrice.HasValue) { if (_assetDayOffService.ArePendingOrdersDisabled(order.Instrument)) { throw new ValidateOrderException(OrderRejectReason.NoLiquidity, "Trades for instrument are not available"); } if (order.ExpectedOpenPrice <= 0) { throw new ValidateOrderException(OrderRejectReason.InvalidExpectedOpenPrice, "Incorrect expected open price"); } order.ExpectedOpenPrice = Math.Round(order.ExpectedOpenPrice ?? 0, order.AssetAccuracy); if (order.GetOrderType() == OrderDirection.Buy && order.ExpectedOpenPrice > quote.Ask || order.GetOrderType() == OrderDirection.Sell && order.ExpectedOpenPrice < quote.Bid) { var reasonText = order.GetOrderType() == OrderDirection.Buy ? string.Format(MtMessages.Validation_PriceAboveAsk, order.ExpectedOpenPrice, quote.Ask) : string.Format(MtMessages.Validation_PriceBelowBid, order.ExpectedOpenPrice, quote.Bid); throw new ValidateOrderException(OrderRejectReason.InvalidExpectedOpenPrice, reasonText, $"{order.Instrument} quote (bid/ask): {quote.Bid}/{quote.Ask}"); } } var accountAsset = _accountAssetsCacheService.GetAccountAsset(order.TradingConditionId, order.AccountAssetId, order.Instrument); if (accountAsset.DealLimit > 0 && Math.Abs(order.Volume) > accountAsset.DealLimit) { throw new ValidateOrderException(OrderRejectReason.InvalidVolume, $"Margin Trading is in beta testing. The volume of a single order is temporarily limited to {accountAsset.DealLimit} {accountAsset.Instrument}. Thank you for using Lykke Margin Trading, the limit will be cancelled soon!"); } //check TP/SL if (order.TakeProfit.HasValue) { order.TakeProfit = Math.Round(order.TakeProfit.Value, order.AssetAccuracy); } if (order.StopLoss.HasValue) { order.StopLoss = Math.Round(order.StopLoss.Value, order.AssetAccuracy); } ValidateOrderStops(order.GetOrderType(), quote, accountAsset.DeltaBid, accountAsset.DeltaAsk, order.TakeProfit, order.StopLoss, order.ExpectedOpenPrice, order.AssetAccuracy); ValidateInstrumentPositionVolume(accountAsset, order); if (!_accountUpdateService.IsEnoughBalance(order)) { throw new ValidateOrderException(OrderRejectReason.NotEnoughBalance, MtMessages.Validation_NotEnoughBalance, $"Account available balance is {account.GetTotalCapital()}"); } }
public async Task Handle(StartLiquidationInternalCommand command, IEventPublisher publisher) { #region Private Methods void PublishFailedEvent(string reason) { publisher.PublishEvent(new LiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = reason, LiquidationType = command.LiquidationType.ToType <LiquidationTypeContract>(), AccountId = command.AccountId, AssetPairId = command.AssetPairId, Direction = command.Direction?.ToType <PositionDirectionContract>(), }); _liquidationEndEventChannel.SendEvent(this, new LiquidationEndEventArgs { OperationId = command.OperationId, CreationTime = _dateService.Now(), AccountId = command.AccountId, LiquidatedPositionIds = new List <string>(), FailReason = reason, }); } #endregion #region Validations if (string.IsNullOrEmpty(command.AccountId)) { PublishFailedEvent("AccountId must be specified"); return; } if (_accountsCache.TryGet(command.AccountId) == null) { PublishFailedEvent("Account does not exist"); return; } #endregion var(executionInfo, _) = await _operationExecutionInfoRepository.GetOrAddAsync( operationName : LiquidationSaga.OperationName, operationId : command.OperationId, factory : () => new OperationExecutionInfo <LiquidationOperationData>( operationName: LiquidationSaga.OperationName, id: command.OperationId, lastModified: _dateService.Now(), data: new LiquidationOperationData { State = LiquidationOperationState.Initiated, AccountId = command.AccountId, AssetPairId = command.AssetPairId, QuoteInfo = command.QuoteInfo, Direction = command.Direction, LiquidatedPositionIds = new List <string>(), ProcessedPositionIds = new List <string>(), LiquidationType = command.LiquidationType, OriginatorType = command.OriginatorType, AdditionalInfo = command.AdditionalInfo, StartedAt = command.CreationTime } )); if (executionInfo.Data.State == LiquidationOperationState.Initiated) { if (!_accountsCache.TryStartLiquidation(command.AccountId, command.OperationId, out var currentOperationId)) { if (currentOperationId != command.OperationId) { PublishFailedEvent( $"Liquidation is already in progress. Initiated by operation : {currentOperationId}"); return; } } _chaosKitty.Meow( $"{nameof(StartLiquidationInternalCommand)}:" + $"Publish_LiquidationStartedInternalEvent:" + $"{command.OperationId}"); publisher.PublishEvent(new LiquidationStartedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), AssetPairId = executionInfo.Data.AssetPairId, AccountId = executionInfo.Data.AccountId, LiquidationType = executionInfo.Data.LiquidationType.ToType <LiquidationTypeContract>() }); } }
public async Task <(Order order, List <Order> relatedOrders)> ValidateRequestAndCreateOrders( OrderPlaceRequest request) { #region Validate properties if (request.Volume == 0) { throw new ValidateOrderException(OrderRejectReason.InvalidVolume, "Volume can not be 0"); } var assetPair = GetAssetPairIfAvailableForTrading(request.InstrumentId, request.Type.ToType <OrderType>(), request.ForceOpen, false); if (request.Type != OrderTypeContract.Market || request.AdditionalInfo.IsCancellationTrade(out _)) { if (!request.Price.HasValue) { throw new ValidateOrderException(OrderRejectReason.InvalidExpectedOpenPrice, $"Price is required for {request.Type} order"); } else { request.Price = Math.Round(request.Price.Value, assetPair.Accuracy); if (request.Price <= 0) { throw new ValidateOrderException(OrderRejectReason.InvalidExpectedOpenPrice, $"Price should be more than 0"); } } } else { //always ignore price for market orders request.Price = null; } var account = _accountsCacheService.TryGet(request.AccountId); if (account == null) { throw new ValidateOrderException(OrderRejectReason.InvalidAccount, "Account not found"); } ValidateValidity(request.Validity, request.Type.ToType <OrderType>()); ITradingInstrument tradingInstrument; try { tradingInstrument = _tradingInstrumentsCache.GetTradingInstrument(account.TradingConditionId, assetPair.Id); } catch { throw new ValidateOrderException(OrderRejectReason.InvalidInstrument, $"Instrument {assetPair.Id} is not available for trading on selected account"); } if (tradingInstrument.DealMinLimit > 0 && Math.Abs(request.Volume) < tradingInstrument.DealMinLimit) { throw new ValidateOrderException(OrderRejectReason.MinOrderSizeLimit, $"The minimum volume of a single order is limited to {tradingInstrument.DealMinLimit} {tradingInstrument.Instrument}."); } #endregion var equivalentSettings = GetReportingEquivalentPricesSettings(account.LegalEntity); if (request.Type == OrderTypeContract.StopLoss || request.Type == OrderTypeContract.TakeProfit || request.Type == OrderTypeContract.TrailingStop) { var order = await ValidateAndGetSlOrTpOrder(request, request.Type, request.Price, equivalentSettings, null); return(order, new List <Order>()); } var initialParameters = await GetOrderInitialParameters(request.InstrumentId, account.LegalEntity, equivalentSettings, account.BaseAssetId); var volume = request.Direction == OrderDirectionContract.Sell ? -Math.Abs(request.Volume) : request.Direction == OrderDirectionContract.Buy ? Math.Abs(request.Volume) : request.Volume; var originator = GetOriginator(request.Originator); var baseOrder = new Order(initialParameters.Id, initialParameters.Code, request.InstrumentId, volume, initialParameters.Now, initialParameters.Now, request.Validity, account.Id, account.TradingConditionId, account.BaseAssetId, request.Price, equivalentSettings.EquivalentAsset, OrderFillType.FillOrKill, string.Empty, account.LegalEntity, request.ForceOpen, request.Type.ToType <OrderType>(), request.ParentOrderId, null, originator, initialParameters.EquivalentPrice, initialParameters.FxPrice, initialParameters.FxAssetPairId, initialParameters.FxToAssetPairDirection, OrderStatus.Placed, request.AdditionalInfo); ValidateBaseOrderPrice(baseOrder, baseOrder.Price); var relatedOrders = new List <Order>(); if (request.StopLoss.HasValue) { request.StopLoss = Math.Round(request.StopLoss.Value, assetPair.Accuracy); if (request.StopLoss <= 0) { throw new ValidateOrderException(OrderRejectReason.InvalidStoploss, $"StopLoss should be more then 0"); } var orderType = request.UseTrailingStop ? OrderTypeContract.TrailingStop : OrderTypeContract.StopLoss; var sl = await ValidateAndGetSlOrTpOrder(request, orderType, request.StopLoss, equivalentSettings, baseOrder); if (sl != null) { relatedOrders.Add(sl); } } if (request.TakeProfit.HasValue) { request.TakeProfit = Math.Round(request.TakeProfit.Value, assetPair.Accuracy); if (request.TakeProfit <= 0) { throw new ValidateOrderException(OrderRejectReason.InvalidTakeProfit, $"TakeProfit should be more then 0"); } var tp = await ValidateAndGetSlOrTpOrder(request, OrderTypeContract.TakeProfit, request.TakeProfit, equivalentSettings, baseOrder); if (tp != null) { relatedOrders.Add(tp); } } await ValidateProductComplexityConfirmation(request, account); return(baseOrder, relatedOrders); }
private async Task Handle(StartSpecialLiquidationInternalCommand command, IEventPublisher publisher) { if (!_marginTradingSettings.SpecialLiquidation.Enabled) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = "Special liquidation is disabled in settings", }); return; } //validate the list of positions contain only the same instrument var positions = _orderReader.GetPositions().Where(x => command.PositionIds.Contains(x.Id)).ToList(); if (!string.IsNullOrEmpty(command.AccountId)) { if (_accountsCacheService.TryGet(command.AccountId) == null) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = $"Account {command.AccountId} does not exist", }); return; } positions = positions.Where(x => x.AccountId == command.AccountId).ToList(); } if (positions.Select(x => x.AssetPairId).Distinct().Count() > 1) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = "The list of positions is of different instruments", }); return; } if (!positions.Any()) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = "No positions to liquidate", }); return; } if (!TryGetExchangeNameFromPositions(positions, out var externalProviderId)) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = "All requested positions must be open on the same external exchange", }); return; } var assetPairId = positions.First().AssetPairId; if (_assetPairsCache.GetAssetPairById(assetPairId).IsDiscontinued) { publisher.PublishEvent(new SpecialLiquidationFailedEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Reason = $"Asset pair {assetPairId} is discontinued", }); return; } var executionInfo = await _operationExecutionInfoRepository.GetOrAddAsync( operationName : SpecialLiquidationSaga.OperationName, operationId : command.OperationId, factory : () => new OperationExecutionInfo <SpecialLiquidationOperationData>( operationName: SpecialLiquidationSaga.OperationName, id: command.OperationId, lastModified: _dateService.Now(), data: new SpecialLiquidationOperationData { State = SpecialLiquidationOperationState.Initiated, Instrument = assetPairId, PositionIds = positions.Select(x => x.Id).ToList(), ExternalProviderId = externalProviderId, AccountId = command.AccountId, CausationOperationId = command.CausationOperationId, AdditionalInfo = command.AdditionalInfo, OriginatorType = command.OriginatorType } )); if (executionInfo.Data.SwitchState(SpecialLiquidationOperationState.Initiated, SpecialLiquidationOperationState.Started)) { publisher.PublishEvent(new SpecialLiquidationStartedInternalEvent { OperationId = command.OperationId, CreationTime = _dateService.Now(), Instrument = positions.FirstOrDefault()?.AssetPairId, }); _chaosKitty.Meow(command.OperationId); await _operationExecutionInfoRepository.Save(executionInfo); } }
public async Task Handle(AccountChangedEvent e) { var executionInfo = await _operationExecutionInfoRepository.GetOrAddAsync( operationName : OperationName, operationId : e.OperationId, factory : () => new OperationExecutionInfo <OperationData>( operationName: OperationName, id: e.OperationId, lastModified: _dateService.Now(), data: new OperationData { State = OperationState.Initiated } )); if (executionInfo.Data.SwitchState(OperationState.Initiated, OperationState.Finished)) { var updatedAccount = Convert(e.Account); switch (e.EventType) { case AccountChangedEventTypeContract.Created: _accountsCacheService.TryAddNew(MarginTradingAccount.Create(updatedAccount)); break; case AccountChangedEventTypeContract.Updated: { var account = _accountsCacheService.TryGet(e.Account.Id); if (await ValidateAccount(account, e) && await _accountsCacheService.UpdateAccountChanges(updatedAccount.Id, updatedAccount.TradingConditionId, updatedAccount.WithdrawTransferLimit, updatedAccount.IsDisabled, updatedAccount.IsWithdrawalDisabled, e.ChangeTimestamp)) { _accountUpdateService.RemoveLiquidationStateIfNeeded(e.Account.Id, "Trading conditions changed"); } break; } case AccountChangedEventTypeContract.BalanceUpdated: { if (e.BalanceChange != null) { var account = _accountsCacheService.TryGet(e.Account.Id); if (await ValidateAccount(account, e)) { switch (e.BalanceChange.ReasonType) { case AccountBalanceChangeReasonTypeContract.Withdraw: await _accountUpdateService.UnfreezeWithdrawalMargin(updatedAccount.Id, e.BalanceChange.Id); break; case AccountBalanceChangeReasonTypeContract.UnrealizedDailyPnL: if (_ordersCache.Positions.TryGetPositionById(e.BalanceChange.EventSourceId, out var position)) { position.ChargePnL(e.BalanceChange.Id, e.BalanceChange.ChangeAmount); } else { _log.WriteWarning("AccountChangedEvent Handler", e.ToJson(), $"Position [{e.BalanceChange.EventSourceId} was not found]"); } break; case AccountBalanceChangeReasonTypeContract.RealizedPnL: await _accountUpdateService.UnfreezeUnconfirmedMargin(e.Account.Id, e.BalanceChange.EventSourceId); break; } if (await _accountsCacheService.UpdateAccountBalance(updatedAccount.Id, e.BalanceChange.Balance, e.ChangeTimestamp)) { _accountUpdateService.RemoveLiquidationStateIfNeeded(e.Account.Id, "Balance updated"); _accountBalanceChangedEventChannel.SendEvent(this, new AccountBalanceChangedEventArgs(updatedAccount)); } } } else { _log.WriteWarning("AccountChangedEvent Handler", e.ToJson(), "BalanceChange info is empty"); } break; } case AccountChangedEventTypeContract.Deleted: //account deletion from cache is double-handled by CQRS flow _accountsCacheService.Remove(e.Account.Id); break; default: await _log.WriteErrorAsync(nameof(AccountsProjection), nameof(AccountChangedEvent), e.ToJson(), new Exception("AccountChangedEventTypeContract was in incorrect state")); break; } _chaosKitty.Meow(e.OperationId); await _operationExecutionInfoRepository.Save(executionInfo); } }
public bool?IsMarginTradingEnabledByAccountId(string accountId) { var acc = _accountsCacheService.TryGet(accountId); return(!acc?.IsDisabled); // todo review and fix? }