콘텐츠로 이동

심볼 환율 변환

심볼 환율 변환은 애플리케이션에 손익 계산과 관련된 기능을 포함하려는 경우 필수적인 작업입니다.

사용 사례

수수료, 스왑 및 배당금을 계산할 때 심볼 환율 변환을 수행해야 합니다. 수수료, 스왑 및 배당금은 일반적으로 USD로 표시됩니다. 그러나 중개인과 선호도에 따라 계좌 입금 통화가 USD가 아닌 다른 자산일 수 있습니다. 수수료, 스왑 및 배당금을 정확히 정의하려면 USD와 계좌 입금 통화 간의 환율을 알아야 합니다.

변환 체인

각 개별 심볼의 가격을 기본 자산과 인용 자산 간의 환율로 생각하세요. 주어진 자산을 다른 자산으로 직접 연결하는 심볼이 없는 경우 환율을 얻는 작업이 복잡해집니다. 다행히 cTrader 백엔드는 한 자산에서 다른 자산으로의 최단 변환 경로 역할을 하는 변환 체인을 자동으로 생성할 수 있습니다.

예를 들어, EUR을 NZD로 변환하려고 하는데 거래 서버에 EURNZD 심볼이 없는 경우, cTrader는 변환 과정에서 중간 단계 역할을 하는 체인을 제안할 것입니다. 심볼 가용성에 따라 이러한 체인은 EURUSD-USDCAD-CADNZD, EURCFH-CFHUSD-USDNZD 또는 EURCAD-CADUSD-USDCFH-CFHNZD와 같이 보일 수 있습니다.

두 자산 간에 변환하려면 앱이 다음 작업을 수행해야 합니다:

  • 변환하려는 자산의 ID에 접근합니다.
  • 하나 이상의 라이트 심볼을 포함하는 전환 체인을 획득하세요.
  • 스팟 이벤트를 구독하고 처리하세요.
  • 전환 체인을 따라 최종 전환율을 반환하세요.

아래에서 이러한 각 작업에 대해 자세히 설명합니다.

자산 ID 접근

자산 ID는 라이트 심볼 엔티티의 속성으로 직접 접근할 수 있습니다(ProtoOALightSymbol 모델 메시지로 표현됨). 또한, 계정 입금 통화의 ID는 관련된 ProtoOaTrader 엔티티에서 직접 획득할 수 있습니다.

아래 예제는 하나의 심볼에 대한 자산 ID만을 획득합니다. 여러 심볼에 대해 동일한 작업을 수행하려면 루프를 추가하거나 다른 적절한 솔루션(예: map() 메서드 또는 동등한 방법)을 사용해야 합니다.

JSON 작업

JSON 작업 시, 아래 코드와 이 튜토리얼의 다른 코드 스니펫을 재사용할 수 있습니다. 그러나 Proto... 클래스의 이름을 사용자 정의 클래스의 이름으로 대체하고, 선호하는 TCP 및 WebSocket 클라이언트를 고려하여 필요한 수정을 해야 합니다.

symbolProtoOALightSymbol 타입의 변수입니다. JSON을 사용하는 경우, ProtoOALightSymbol 메시지를 나타내는 사용자 정의 타입일 수 있습니다.

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

아래 예제에서는 ProtoOATrader 타입의 account 객체의 입금 통화 ID를 획득합니다.

1
int depositAssetId = account.DepositAssetId;

symbolProtoOALightSymbol 타입의 메시지를 나타내는 변수입니다.

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

아래 예제에서는 ProtoOATrader 모델 메시지를 나타내는 account 객체의 입금 통화 ID를 획득합니다.

1
depositAssetId = account.depositAssetId

전환 체인 획득

유효한 전환 체인을 획득하려면, 앱이 ProtoOASymbolsForConversionReq 메시지를 전송해야 하며, firstAssetIdlastAssetId 필드를 변환하려는 자산의 ID로 설정해야 합니다. ProtoOASymbolsForConversionRes 타입의 응답을 받으면, 반복된 symbol 필드를 컬렉션 내에 저장하세요.

공식 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

또한 onMessageReceived 콜백에 다음 조건을 추가하세요.

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

참고

서버에서 자산 데이터를 처음 요청하고 받을 때 모든 전환 체인 심볼을 획득하는 것을 강력히 권장합니다. 그렇지 않으면, 전환율이 필요할 때마다 ProtoOASymbolsForConversionReq 메시지를 전송해야 하며, 경우에 따라 초당 여러 번 전송해야 할 수도 있습니다.

참고

ProtoOALightSymbol 엔티티는 매도 또는 매수 가격을 나타내는 필드를 포함하지 않으므로, 사용자 정의 Symbol 클래스 또는 동등한 것을 생성하여 프로그래밍 방식으로 새로운 심볼을 쉽게 생성하고 특정 이벤트가 트리거될 때 속성을 업데이트할 수 있도록 하는 것을 강력히 권장합니다. 이 클래스의 객체로 라이트 심볼을 별도로 변환해야 합니다.

스팟 이벤트 구독 및 처리

전환 체인을 처음부터 끝까지 따라가려면 체인에 포함된 모든 심볼에 대해 ProtoOASpotEvent를 구독해야 합니다. 이것은 공식 SDK를 통해 다음과 같이 수행됩니다.

아래 나열된 작업을 수행하기 전에, Symbol 클래스 또는 동등한 것을 생성하여 프로그래밍 방식으로 새로운 심볼을 쉽게 생성하고 특정 이벤트가 트리거될 때 속성을 업데이트할 수 있도록 하세요. 아래 예제에서 우리의 Symbol 클래스는 ProtoOASymbolProtoOALightSymbol 객체를 속성으로 포함합니다.

 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;
}

또한 스팟 이벤트를 구독하고 처리해야 합니다. 아래 예제에서는 라이트 심볼을 저장하는 symbols 컬렉션에 접근합니다. 방금 받은 ProtoOASpotEventsymbolId 필드와 일치하는 심볼 객체를 찾습니다. 마지막으로, GetPriceFromRelative 헬퍼 메서드를 사용하여 symbolbidask 속성을 업데이트합니다.

 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));
    }

}

특정 심볼에 대해 ProtoOASpotEvent를 구독하기 위해 다음 함수를 사용할 수 있습니다. 이 함수는 하나의 심볼만 구독하므로, 전환 심볼 컬렉션의 모든 라이트 심볼에 대해 이 함수를 호출해야 합니다.

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)

스팟 이벤트를 처리하기 위해 onMessageReceived 콜백에 다음 조건을 추가할 수 있습니다.

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

마지막으로, 여기 우리의 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

우리의 Symbol 클래스에서 getPipsFromRelative 함수는 다음과 같이 정의됩니다:

1
2
3
4
    import math

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

pipPositiondigits 속성의 값은 별도로 획득해야 합니다.

전환 체인을 따라 최종 전환율 반환

이 시점에서 전환 체인의 모든 심볼은 스팟율을 받아야 하며, 각 심볼에 대한 정확한 매도 및 매수 가격에 접근할 수 있어야 합니다. 남은 것은 전환 체인을 따라 이동하여 최종율을 반환하는 것입니다. 이것은 다음과 같이 수행할 수 있습니다:

 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