mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-02-16 14:13:46 +00:00
241 lines
9.3 KiB
C#
241 lines
9.3 KiB
C#
using CryptoExchange.Net.Objects;
|
|
using CryptoExchange.Net.Objects.Sockets;
|
|
using CryptoExchange.Net.SharedApis;
|
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|
{
|
|
/// <summary>
|
|
/// Position tracker
|
|
/// </summary>
|
|
public class PositionTracker : UserDataItemTracker<SharedPosition>
|
|
{
|
|
private readonly IFuturesOrderRestClient _restClient;
|
|
private readonly IPositionSocketClient? _socketClient;
|
|
private readonly ExchangeParameters? _exchangeParameters;
|
|
|
|
/// <summary>
|
|
/// Whether websocket position updates are full snapshots and missing positions should be considered 0
|
|
/// </summary>
|
|
protected bool WebsocketPositionUpdatesAreFullSnapshots { get; }
|
|
|
|
/// <summary>
|
|
/// ctor
|
|
/// </summary>
|
|
public PositionTracker(
|
|
ILogger logger,
|
|
UserDataSymbolTracker symbolTracker,
|
|
IFuturesOrderRestClient restClient,
|
|
IPositionSocketClient? socketClient,
|
|
TrackerItemConfig config,
|
|
IEnumerable<SharedSymbol> symbols,
|
|
bool onlyTrackProvidedSymbols,
|
|
bool websocketPositionUpdatesAreFullSnapshots,
|
|
ExchangeParameters? exchangeParameters = null
|
|
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
|
|
{
|
|
if (_socketClient == null)
|
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
|
|
|
_restClient = restClient;
|
|
_socketClient = socketClient;
|
|
_exchangeParameters = exchangeParameters;
|
|
|
|
WebsocketPositionUpdatesAreFullSnapshots = websocketPositionUpdatesAreFullSnapshots;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override bool Update(SharedPosition existingItem, SharedPosition updateItem)
|
|
{
|
|
// Some other way to way to determine sequence? Maybe timestamp?
|
|
var changed = false;
|
|
if (existingItem.AverageOpenPrice != updateItem.AverageOpenPrice)
|
|
{
|
|
existingItem.AverageOpenPrice = updateItem.AverageOpenPrice;
|
|
changed = true;
|
|
}
|
|
|
|
if (existingItem.Leverage != updateItem.Leverage)
|
|
{
|
|
existingItem.Leverage = updateItem.Leverage;
|
|
changed = true;
|
|
}
|
|
|
|
if (existingItem.LiquidationPrice != updateItem.LiquidationPrice)
|
|
{
|
|
existingItem.LiquidationPrice = updateItem.LiquidationPrice;
|
|
changed = true;
|
|
}
|
|
|
|
if (existingItem.PositionSize != updateItem.PositionSize)
|
|
{
|
|
existingItem.PositionSize = updateItem.PositionSize;
|
|
changed = true;
|
|
}
|
|
|
|
if (existingItem.StopLossPrice != updateItem.StopLossPrice)
|
|
{
|
|
existingItem.StopLossPrice = updateItem.StopLossPrice;
|
|
changed = true;
|
|
}
|
|
|
|
if (existingItem.TakeProfitPrice != updateItem.TakeProfitPrice)
|
|
{
|
|
existingItem.TakeProfitPrice = updateItem.TakeProfitPrice;
|
|
changed = true;
|
|
}
|
|
|
|
if (updateItem.UnrealizedPnl != null && existingItem.UnrealizedPnl != updateItem.UnrealizedPnl)
|
|
{
|
|
existingItem.UnrealizedPnl = updateItem.UnrealizedPnl;
|
|
changed = true;
|
|
}
|
|
|
|
if (updateItem.UpdateTime != null && existingItem.UpdateTime != updateItem.UpdateTime)
|
|
{
|
|
existingItem.UpdateTime = updateItem.UpdateTime;
|
|
// If update time is the only changed prop don't mark it as changed
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedPosition[] @event)
|
|
{
|
|
LastUpdateTime = DateTime.UtcNow;
|
|
|
|
List<SharedPosition>? toRemove = null;
|
|
foreach (var item in @event)
|
|
{
|
|
if (item is SharedSymbolModel symbolModel)
|
|
{
|
|
if (symbolModel.SharedSymbol == null)
|
|
{
|
|
toRemove ??= new List<SharedPosition>();
|
|
toRemove.Add(item);
|
|
_logger.LogTrace("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, item.Symbol);
|
|
}
|
|
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
|
|
{
|
|
toRemove ??= new List<SharedPosition>();
|
|
toRemove.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (toRemove != null)
|
|
@event = @event.Except(toRemove).ToArray();
|
|
|
|
_symbolTracker.UpdateTrackedSymbols(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
|
|
|
|
|
// Update local store
|
|
var updatedItems = @event.Select(GetKey).ToList();
|
|
|
|
if (WebsocketPositionUpdatesAreFullSnapshots)
|
|
{
|
|
// Reset any tracking position to zero/null values when it's no longer in the snapshot as it means there is no open position any more
|
|
var notInSnapshot = _store.Where(x => !updatedItems.Contains(x.Key) && x.Value.PositionSize != 0).ToList();
|
|
foreach (var position in notInSnapshot)
|
|
{
|
|
position.Value.UpdateTime = DateTime.UtcNow;
|
|
position.Value.AverageOpenPrice = null;
|
|
position.Value.LiquidationPrice = null;
|
|
position.Value.PositionSize = 0;
|
|
position.Value.StopLossPrice = null;
|
|
position.Value.TakeProfitPrice = null;
|
|
position.Value.UnrealizedPnl = null;
|
|
updatedItems.Add(position.Key);
|
|
|
|
LastChangeTime = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
foreach (var item in @event)
|
|
{
|
|
bool existed = false;
|
|
_store.AddOrUpdate(GetKey(item), item, (key, existing) =>
|
|
{
|
|
existed = true;
|
|
if (CheckIfUpdateShouldBeApplied(existing, item) == false)
|
|
{
|
|
updatedItems.Remove(key);
|
|
}
|
|
else
|
|
{
|
|
var updated = Update(existing, item);
|
|
if (!updated)
|
|
{
|
|
updatedItems.Remove(key);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Updated {DataType} {Item}", DataType, key);
|
|
LastChangeTime = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
return existing;
|
|
});
|
|
|
|
if (!existed)
|
|
{
|
|
_logger.LogDebug("Added {DataType} {Item}", DataType, GetKey(item));
|
|
LastChangeTime = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
if (updatedItems.Count > 0)
|
|
{
|
|
await InvokeUpdate(
|
|
new UserDataUpdate<SharedPosition[]>(source, _exchange, _store.Where(x => updatedItems.Contains(x.Key)).Select(x => x.Value).ToArray())).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override string GetKey(SharedPosition item) =>
|
|
item.SharedSymbol!.TradingMode + item.SharedSymbol.BaseAsset + item.SharedSymbol.QuoteAsset + item.PositionMode + (item.PositionMode != SharedPositionMode.OneWay ? item.PositionSide.ToString() : "");
|
|
|
|
/// <inheritdoc />
|
|
protected override bool? CheckIfUpdateShouldBeApplied(SharedPosition existingItem, SharedPosition updateItem) => true;
|
|
|
|
/// <inheritdoc />
|
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
|
{
|
|
if (_socketClient == null)
|
|
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
|
|
|
return ExchangeHelpers.ProcessQueuedAsync<SharedPosition[]>(
|
|
async handler => await _socketClient.SubscribeToPositionUpdatesAsync(new SubscribePositionRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
|
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task<bool> DoPollAsync()
|
|
{
|
|
var anyError = false;
|
|
var positionsResult = await _restClient.GetPositionsAsync(new GetPositionsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
|
if (!positionsResult.Success)
|
|
{
|
|
anyError = true;
|
|
|
|
_initialPollingError ??= positionsResult.Error;
|
|
if (!_firstPollDone)
|
|
return anyError;
|
|
}
|
|
else
|
|
{
|
|
await HandleUpdateAsync(UpdateSource.Poll, positionsResult.Data).ConfigureAwait(false);
|
|
}
|
|
|
|
return anyError;
|
|
}
|
|
}
|
|
}
|