Przejdź do treści

Konwersja kursów symboli

Konwersja kursów symboli jest niezbędna, jeśli aplikacja ma zawierać funkcje związane z obliczaniem P&L.

Przypadki użycia

Konwersja kursów symboli jest konieczna podczas obliczania prowizji, swapów i dywidend. Prowizje, swapy i dywidendy są zazwyczaj wyrażane w USD. Jednak w zależności od brokera i Twoich preferencji walutą depozytową konta może być inny aktyw niż USD. Aby dokładnie określić prowizje, swapy i dywidendy, musisz znać kurs wymiany między USD a walutą depozytową konta.

Łańcuchy konwersji

Traktuj cenę każdego symbolu jako kurs konwersji między jego aktywem bazowym a aktywem kwotowanym. Pobieranie kursów konwersji staje się bardziej złożone, jeśli nie ma symboli bezpośrednio łączących dany aktyw z innym aktywem. Na szczęście backend cTrader może automatycznie tworzyć łańcuchy konwersji stanowiące najkrótsze możliwe ścieżki konwersji między aktywami.

Na przykład jeśli chcesz przeliczyć EUR na NZD, a na serwerze tradingowym nie ma symbolu EURNZD, cTrader zaproponuje łańcuch pełniący rolę etapów pośrednich w procesie konwersji. W zależności od dostępności symboli taki łańcuch może wyglądać na przykład tak: EURUSD-USDCAD-CADNZD, EURCFH-CFHUSD-USDNZD lub nawet EURCAD-CADUSD-USDCFH-CFHNZD.

Aby przeliczać wartości między dwoma aktywami, aplikacja musi wykonać następujące czynności:

  • Uzyskaj dostęp do identyfikatorów aktywów, między którymi chcesz dokonać konwersji.
  • Pobierz łańcuch konwersji zawierający co najmniej jeden uproszczony symbol.
  • Zasubskrybuj zdarzenia spot i je obsługuj.
  • Przejdź przez łańcuch konwersji i zwróć końcowy kurs konwersji.

Poniżej szczegółowo opisujemy każdą z tych czynności.

Uzyskaj dostęp do identyfikatorów aktywów

Dostęp do identyfikatorów aktywów można uzyskać bezpośrednio poprzez właściwości encji uproszczonego symbolu (reprezentowanej przez wiadomość modelu ProtoOALightSymbol). Ponadto identyfikator waluty depozytowej konta można pobrać bezpośrednio z powiązanej encji ProtoOaTrader.

Zwróć uwagę, że poniższy przykład pobiera identyfikatory aktywów tylko dla jednego symbolu. Aby wykonać tę samą operację dla wielu symboli, dodaj pętlę lub zastosuj inne odpowiednie rozwiązanie (na przykład metodę map() lub jej odpowiednik).

Praca z JSON

Podczas pracy z JSON nadal możesz wykorzystać poniższy kod oraz inne fragmenty kodu z tego samouczka. Pamiętaj jednak, aby zastąpić nazwy klas Proto... nazwami własnych klas i wprowadzić niezbędne modyfikacje odpowiednie dla wybranego klienta TCP i WebSocket.

symbol jest zmienną typu ProtoOALightSymbol. Jeśli używasz JSON, może to być dowolny niestandardowy typ reprezentujący wiadomość ProtoOALightSymbol.

1
2
int baseAssetId = symbol.BaseAsset.AssetId; 
int quoteAssetId = symbol.QuoteAsset.AssetId;

W poniższym przykładzie pobieramy identyfikator waluty depozytowej obiektu account typu ProtoOATrader.

1
int depositAssetId = account.DepositAssetId;

symbol jest zmienną reprezentującą wiadomość typu ProtoOALightSymbol.

1
2
baseAssetId = symbol.BaseAsset.AssetId
quoteAssetId = symbol.QuoteAsset.AssetId

W poniższym przykładzie pobieramy identyfikator waluty depozytowej obiektu account, który reprezentuje wiadomość modelu ProtoOATrader.

1
depositAssetId = account.depositAssetId

Pobierz łańcuch konwersji

Aby pobrać prawidłowy łańcuch konwersji, aplikacja musi wysłać wiadomość ProtoOASymbolsForConversionReq, ustawiając pola firstAssetId i lastAssetId na identyfikatory aktywów, między którymi chcesz dokonać konwersji. Po otrzymaniu odpowiedzi typu ProtoOASymbolsForConversionRes zapisz jej powtarzalne pole symbol w kolekcji.

Poniżej pokazano, jak wykonać tę czynność za pomocą oficjalnych SDK.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
List<ProtoOALightSymbol> lightSymbolsForConversion = new List<ProtoOALightSymbol>();

var symbolsResult = await GetConversionSymbols(accountId, isLive, baseAssetId, quoteAssetId);

lightSymbolsForConversion.AddRange(symbolsResult);

public Task<ProtoOALightSymbol[]> GetConversionSymbols(long accountId, bool isLive, long baseAssetId, long quoteAssetId)
{
    VerifyConnection();

    var client = GetClient(isLive);

    var taskCompletionSource = new TaskCompletionSource<ProtoOALightSymbol[]>();

    IDisposable disposable = null;

    disposable = client.OfType<ProtoOASymbolsForConversionRes>().Where(response => response.CtidTraderAccountId == accountId)
        .Subscribe(response =>
        {
            taskCompletionSource.SetResult(response.Symbol.ToArray());

            disposable?.Dispose();
        });

    var requestMessage = new ProtoOASymbolsForConversionReq
    {
        CtidTraderAccountId = accountId,
        FirstAssetId = baseAssetId,
        LastAssetId = quoteAssetId
    };

    EnqueueMessage(requestMessage, ProtoOAPayloadType.ProtoOASymbolsForConversionReq, client);

    return taskCompletionSource.Task;
}
1
2
3
4
5
def sendProtoOASymbolsForConversionReq(accountId, firstAssetId, lastAssetId):
    request = ProtoOASymbolsForConversionReq()
    request.ctidTraderAccountId = accountId
    request.firstAssedId = firstAssetId
    request.lastAssetId = lastAssetId

Dodaj również poniższy warunek do wywołania zwrotnego onMessageReceived.

1
2
3
elif message.payloadType == ProtoOASymbolsForConversionRes().payloadType:
    ProtoOASymbolsForConversionRes = Protobuf.extract(message)
    conversionSymbols = ProtoOASymbolsForConversionRes.symbol

Uwaga

Zdecydowanie zalecamy pobranie wszystkich symboli łańcucha konwersji dla każdego aktywu przy pierwszym żądaniu i odbiorze danych aktywów z serwera. W przeciwnym razie za każdym razem, gdy trzeba będzie przeliczyć kursy, konieczne będzie wysłanie wiadomości ProtoOASymbolsForConversionReq, co w niektórych przypadkach może następować kilka razy na sekundę.

Uwaga

Ponieważ encje ProtoOALightSymbol nie zawierają pól reprezentujących ceny bid ani ask, zdecydowanie zalecamy utworzenie niestandardowych klas Symbol lub ich odpowiedników, aby łatwo programowo tworzyć nowe symbole i aktualizować ich właściwości po wyzwoleniu określonych zdarzeń. Uproszczone symbole trzeba będzie osobno przekonwertować na obiekty tej klasy.

Subskrybuj i obsługuj zdarzenia spot

Aby przejść przez cały łańcuch konwersji, musisz zasubskrybować ProtoOASpotEvent dla wszystkich symboli w łańcuchu. Za pomocą oficjalnych SDK można to zrobić w następujący sposób.

Przed wykonaniem poniższych operacji utwórz klasę Symbol lub jej odpowiednik, aby łatwo programowo tworzyć nowe symbole i aktualizować ich właściwości po wyzwoleniu określonych zdarzeń. W poniższych przykładach nasza klasa Symbol zawiera obiekty ProtoOASymbol i ProtoOALightSymbol jako właściwości.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public Task<ProtoOASubscribeSpotsRes> SubscribeToSpots(long accountId, bool isLive, params long[] symbolIds)
{
    var client = GetClient(isLive);

    var taskCompletionSource = new TaskCompletionSource<ProtoOASubscribeSpotsRes>();

    IDisposable disposable = null;

    disposable = client.OfType<ProtoOASubscribeSpotsRes>().Where(response => response.CtidTraderAccountId == accountId).Subscribe(response =>
    {
        taskCompletionSource.SetResult(response);

        disposable?.Dispose();
    });

    var requestMessage = new ProtoOASubscribeSpotsReq
    {
        CtidTraderAccountId = accountId,
    };

    requestMessage.SymbolId.AddRange(symbolIds);

    EnqueueMessage(requestMessage, ProtoOAPayloadType.ProtoOaSubscribeSpotsReq, client);

    return taskCompletionSource.Task;
}

Musimy również zasubskrybować i obsługiwać zdarzenia spot. W poniższym przykładzie uzyskujemy dostęp do kolekcji symbols, w której przechowywane są uproszczone symbole. Znajdujemy obiekt symbolu, którego identyfikator odpowiada polu symbolId właśnie odebranego zdarzenia ProtoOASpotEvent. Na koniec aktualizujemy właściwości bid i ask obiektu symbol za pomocą metody pomocniczej GetPriceFromRelative.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
client.OfType<ProtoOASpotEvent>().Subscribe(OnSpotEvent);

private void OnSpotEvent(ProtoOASpotEvent spotEvent) 
{
    var symbol = symbols.FirstOrDefault(iSymbol => iSymbol.Id == spotEvent.SymbolId);

    double bid;
    double ask;

    if (spotEvent.HasBid) bid = symbol.Data.GetPriceFromRelative((long)spotEvent.Bid);
    if (spotEvent.HasAsk) ask = symbol.Data.GetPriceFromRelative((long)spotEvent.Ask);

    if (bid != symbol.Bid) 
    {
        symbol.Bid = bid;
        RaisePropertyChanged(nameof(Bid));
    }

    if (ask != symbol.Ask) 
    {
        symbol.Ask = ask
        RaisePropertyChanged(nameof(Ask));
    }

}

Za pomocą poniższej funkcji możemy zasubskrybować ProtoOASpotEvent dla konkretnego symbolu. Zwróć uwagę, że funkcja subskrybuje tylko jeden symbol, więc trzeba ją wywołać dla każdego uproszczonego symbolu w kolekcji symboli konwersji.

1
2
3
4
5
6
7
def sendProtoOASubscribeSpotsReq(symbolId, subscribeToSpotTimestamp = False, clientMsgId = None):
    request = ProtoOASubscribeSpotsReq()
    request.ctidTraderAccountId = currentAccountId
    request.symbolId.append(int(symbolId))
    request.subscribeToSpotTimestamp = subscribeToSpotTimestamp if type(subscribeToSpotTimestamp) is bool else bool(subscribeToSpotTimestamp)
    deferred = client.send(request, clientMsgId = clientMsgId)
    deferred.addErrback(onError)

Aby obsługiwać zdarzenia spot, możemy dodać poniższe warunki do wywołania zwrotnego onMessageReceived.

1
2
3
elif message.paloadType == ProtoOASpotEvent().payloadType:
    ProtoOASpotEvent = Protobuf.extract(message)
    onSpotEvent(ProtoOASpotEvent)

Na koniec przedstawiamy nasze wywołanie zwrotne onSpotEvent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def onSpotEvent(ProtoOASpotEvent):
    iterableConversionSymbols = iter(conversionSymbols)
    symbol = next(s for s in iterableConversionSymbols if s.symbolId == ProtoOASpotEvent.symbolId)

    if symbol is None:
        return

    if ProtoOASpotEvent.hasBid == true:
        bid = symbol.data.getPipsFromRelative()
    if ProtoOASpotEvent.hasAsk == true:
        ask = symbol.data.getPipsFromRelative()

    if bid != symbol.bid:
        symbol.bid = bid
    if ask != symbol.ask
        symbol.ask = ask

W naszej klasie Symbol funkcja getPipsFromRelative jest zdefiniowana następująco:

1
2
3
4
    import math

    def getPipsFromRelative(self, relative):
    return round((relative / 100000.0) / symbol.pipPosition, symbol.digits - symbol.pipPosition)

Zwróć uwagę, że wartości właściwości pipPosition i digits trzeba pobrać osobno.

Przejdź przez łańcuch konwersji i zwróć końcowy kurs

Na tym etapie wszystkie symbole w łańcuchu konwersji powinny otrzymywać kursy spot, a Ty powinieneś mieć dostęp do dokładnych cen bid i ask dla każdego symbolu. Pozostaje jedynie przejść przez łańcuch konwersji i zwrócić końcowy kurs. Można to zrobić w następujący sposób:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private double GetConversionRate(List<Symbol> conversionSymbols) 
{
    double conversionRate = 1;

    var currentAsset = conversionSymbols.First().BaseAsset;

    foreach (var symbol in conversionSymbols)
    {
        var closePrice = symbol.Bid; //Replaced by symbol.Ask when calculating P&Ls for short positions

        if (symbol.BaseAsset == currentAsset)
        {
            conversionRate = conversionRate * closePrice;
            currentAsset = symbol.QuoteAsset;
        }
        else
        {
            conversionRate = conversionRate * 1 / closePrice;
            currentAsset = symbol.BaseAsset;
        }
    }

    return conversionRate;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def getConversionRate(conversionSymbols):
    conversionRate = 1
    currentAsset = conversionSymbols[0].baseAsset

    for symbol in conversionSymbols:
        closePrice = symbol.bid #Replaced by symbol.ask when calculating P&L for short positions

        if symbol.baseAsset == currentAsset:
            conversionRate = conversionRate * closePrice
            currentAsset = symbol.quoteAsset

        else:
            conversionRate = conversionRate * 1 / closePrice
            currentAsset = symbol.baseAsset