All Downloads are FREE. Search and download functionalities are using the official Maven repository.

estonlabs.cxtl.exchanges.b2c2.v1.lib.B2C2Cex Maven / Gradle / Ivy

package estonlabs.cxtl.exchanges.b2c2.v1.lib;

import estonlabs.cxtl.common.auth.Credentials;
import estonlabs.cxtl.common.exception.CxtlApiException;
import estonlabs.cxtl.common.exception.CxtlEventException;
import estonlabs.cxtl.common.exception.ErrorCode;
import estonlabs.cxtl.common.http.Event;
import estonlabs.cxtl.common.http.HeaderBuilder;
import estonlabs.cxtl.common.http.JsonRestClient;
import estonlabs.cxtl.common.http.MetricsLogger;
import estonlabs.cxtl.exchanges.a.specification.domain.*;
import estonlabs.cxtl.exchanges.a.specification.lib.Cex;
import estonlabs.cxtl.exchanges.a.specification.lib.ExchangeDataInterface;
import estonlabs.cxtl.exchanges.b2c2.v1.domain.*;
import lombok.NonNull;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.Proxy;
import java.util.*;
import java.util.stream.Collectors;

public class B2C2Cex implements Cex, ExchangeDataInterface {

    private final JsonRestClient client;
    private final MetricsLogger metricsLogger;

    public B2C2Cex(JsonRestClient restClient, MetricsLogger metricsLogger) {
        this.client = restClient;
        this.metricsLogger = metricsLogger;
    }

    private HeaderBuilder authorized(Credentials credentials){
        return (builder, message) -> builder.addHeader("Authorization", "Token " + credentials.getApiKey());
    }

    @Override
    public Proxy getProxy() {
        return client.getProxy();
    }

    @Override
    public Mono> getOlhcv(Object request) {
        return Mono.empty();
    }

    @Override
    public Mono> getLatestPublicTrades(AssetClass assetClass, String symbol) {
        return Mono.empty();
    }

    private Mono>> getInstrumentsByType(Credentials credentials){
        return handleResponse(client.getMany(authorized(credentials),"/instruments", Instrument.class)).map(
                instruments -> instruments.stream().collect(Collectors.groupingBy(Instrument::getType))
        );
    }

    @Override
    public Mono>> getTickers(Credentials credentials) {
        return getInstrumentsByType(credentials);
    }

    public Mono>> getInstruments(List credentials) {
        // Convert the list of credentials to a Flux of Monos
        Flux>> > monos = Flux.fromIterable(credentials)
                .map(this::getInstrumentsByType);

        // Zip the Monos together and merge the results
        return Flux.concat(monos)
                .reduce((map1, map2) -> {
                    map2.forEach((key, value) ->
                            map1.merge(key, value, (list1, list2) -> {
                                list1.addAll(list2);
                                return list1;
                            })
                    );
                    return map1;
                });
    }

    @Override
    public Mono>> getTickers() {
        return Mono.error(new IllegalStateException("B2C2 requires credentials to get tickers"));
    }

    @Override
    public Exchange getExchange() {
        return Exchange.B2C2;
    }

    @Override
    public AssetClass[] getSupportedAssetClasses() {
        return new AssetClass[]{AssetClass.SPOT, AssetClass.CFD};
    }

    @Override
    public Mono placeOrder(Credentials credentials, OrderRequest order) {
        return handleErrorResponse(client.postAsJson(authorized(credentials), "/order/", order, OrderResponse.class));
    }

    //only FOK and MKT orders are supported, hence no partial fills, no cancels
    @Override
    public Mono cancelOrder(Credentials credentials, CancelRequest request) {
        return Mono.error(new CxtlApiException("B2C2 doesn't support order cancellation", new UnsupportedOperationException()));
    }

    @Override
    public Mono> getOrders(Credentials credentials, OrderQueryRequest orderQueryRequest) {
        return handleResponse(client.getMany(authorized(credentials), "/order", orderQueryRequest, OrderResponse.class));
    }

    public Mono getBalances(Credentials credentials) {
        return handleResponse(client.get(authorized(credentials), "/balance",  UserAssets.class));
    }

    public Mono> getPositions(Credentials credentials) {
        return handleResponse(client.getMany(authorized(credentials), "/cfd/open_positions",  OpenPositions.class));
    }

    public Mono getMarginRequirement(String currency, Credentials credentials) {
        return handleResponse(client.get(authorized(credentials), "/margin_requirements?currency=" + currency, MarginRequirement.class));
    }

    @Override
    public Mono getOrder(Credentials credentials, OrderQueryRequest orderQueryRequest) {
        String path = "/order";
        Mono> result;
        if (orderQueryRequest.getOrderId() != null){
            result = handleResponse(client.getMany(authorized(credentials), path + "/" + orderQueryRequest.getOrderId(),OrderResponse.class));
        } else if (orderQueryRequest.getClientOrderId() != null){
            result = handleResponse(client.getMany(authorized(credentials), path + "/" + orderQueryRequest.getClientOrderId(), OrderResponse.class));
        } else {
            result = handleResponse(client.getMany(authorized(credentials), path, orderQueryRequest, OrderResponse.class));
        }
        return result.map(l -> l == null || l.isEmpty() ? null : l.get(0));
    }


    public Mono rfq(Credentials credentials, RFQRequest rfqRequest) {
        return handleErrorResponse(client.postAsJson(authorized(credentials), "/request_for_quote/", rfqRequest, RFQResponse.class));
    }

    @NonNull
    private  Mono onError(Throwable t) {
        if(t instanceof CxtlApiException e){
            return Mono.error(e);
        }
        if(t instanceof CxtlEventException e){
            metricsLogger.finishedError(e.getEvent());
            ErrorResponse response = client.getCodec().quietFromJson(e.getEvent().getResponseJson(), ErrorResponse.class);
            if (response != null){
                CxtlApiException concatMsg = checkForErrors(response);
                if (concatMsg != null) return   Mono.error(concatMsg);
            }
        }
        return Mono.error(new CxtlApiException(t.getMessage(), "UNKNOWN", ErrorCode.UNKNOWN_ERROR));
    }

    @Nullable
    private static CxtlApiException checkForErrors(ErrorResponse response) {
        if (response.getErrors() != null && !response.getErrors().isEmpty()){
            var concatMsg = response.getErrors().stream().map(e -> e.getMessage()+"("+e.getField()+")").collect(Collectors.joining());
            var firstMessage = response.getErrors().get(0);
            return new CxtlApiException(concatMsg, Integer.toString(firstMessage.getCode()), errorCode(firstMessage.getCode(), firstMessage.getField()));
        }
        return null;
    }

    private static ErrorCode errorCode(int code, String field) {
        if (code == 1100 && Objects.equals(field, "quantity")){
            return ErrorCode.INVALID_QTY;
        }
        return switch (code) {
            case 1000, 1100 -> ErrorCode.UNKNOWN_ERROR;
            case 1011, 1502, 1012 -> ErrorCode.INSUFFICIENT_BALANCE;
            case 1006, 1010, 1019, 1026, 1501, 1015 -> ErrorCode.INVALID_QTY;
            case 1005, 1009, 1600 -> ErrorCode.BAD_PX;
            case 1001, 1003, 1004, 1007, 1002, 1022, 1025, 1601 -> ErrorCode.INVALID_SYMBOL;
            case 1021, 1030, 1031 -> ErrorCode.UNKNOWN_ORDER;
            case 1101 -> ErrorCode.INVALID_ARGUMENT;
            default -> ErrorCode.UNKNOWN_ERROR;
        };
    }

    private  Mono handleErrorResponse(Mono> response){
        return response.handle((e, sink) -> {
            CxtlApiException concatMsg = checkForErrors(e.getResponse());
            if(concatMsg != null) {
                metricsLogger.finishedError(e);
                sink.error(concatMsg);
                return;
            }
            metricsLogger.finishedSuccess(e);
            sink.next(e.getResponse());
        }).onErrorResume(this::onError);
    }

    private  Mono handleResponse(Mono> response){
        return response.map( e ->{
            metricsLogger.finishedSuccess(e);
            return e.getResponse();
        }).onErrorResume(this::onError);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy