Skip to content

Feature/ohlc download #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@
import com.gazbert.bxbot.exchanges.trading.api.impl.BalanceInfoImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderBookImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.MarketOrderImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.OhlcFrameImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.OhlcImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.OpenOrderImpl;
import com.gazbert.bxbot.exchanges.trading.api.impl.TickerImpl;
import com.gazbert.bxbot.trading.api.BalanceInfo;
import com.gazbert.bxbot.trading.api.ExchangeNetworkException;
import com.gazbert.bxbot.trading.api.MarketOrder;
import com.gazbert.bxbot.trading.api.MarketOrderBook;
import com.gazbert.bxbot.trading.api.Ohlc;
import com.gazbert.bxbot.trading.api.OhlcFrame;
import com.gazbert.bxbot.trading.api.OhlcInterval;
import com.gazbert.bxbot.trading.api.OpenOrder;
import com.gazbert.bxbot.trading.api.OrderType;
import com.gazbert.bxbot.trading.api.Ticker;
Expand All @@ -49,12 +54,14 @@
import com.google.common.base.MoreObjects;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.RoundingMode;
Expand All @@ -68,6 +75,9 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
Expand Down Expand Up @@ -157,6 +167,8 @@ public final class KrakenExchangeAdapter extends AbstractExchangeAdapter
"Failed to get Balance from exchange. Details: ";
private static final String FAILED_TO_GET_TICKER =
"Failed to get Ticker from exchange. Details: ";
private static final String FAILED_TO_GET_OHLC =
"Failed to get OHLC Data from exchange. Details: ";

private static final String FAILED_TO_GET_OPEN_ORDERS =
"Failed to get Open Orders from exchange. Details: ";
Expand Down Expand Up @@ -599,6 +611,92 @@ public Ticker getTicker(String marketId) throws TradingApiException, ExchangeNet
}
}

@Override
public Ohlc getOhlc(String marketId, OhlcInterval interval)
throws TradingApiException, ExchangeNetworkException {
return getOhlc(marketId, interval, null);
}

@Override
public Ohlc getOhlc(String marketId, OhlcInterval interval, Integer resumeID)
throws TradingApiException, ExchangeNetworkException {
ExchangeHttpResponse response;
String intervalInMinutes = calculateMinuteParamFrom(interval);

try {
final Map<String, String> params = createRequestParamMap();
params.put("pair", marketId);
params.put("interval", intervalInMinutes);
if (resumeID != null) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objects.nonNull(resumeID) ?

params.put("since", String.valueOf(resumeID));
}

response = sendPublicRequestToExchange("OHLC", params);
LOG.debug(() -> "OHLC response: " + response);

if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {

final Type resultType = new TypeToken<KrakenResponse<KrakenOhlcResult>>() {}.getType();
final KrakenResponse<KrakenOhlcResult> krakenResponse =
gson.fromJson(response.getPayload(), resultType);

final List errors = krakenResponse.error;
if (errors == null || errors.isEmpty()) {
Copy link

@Bisconcini Bisconcini May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import static java.util.Objects.isNull;

( isNull(errors) || errors.isEmpty() ) more readable I think.


// Assume we'll always get something here if errors array is empty; else blow fast wih NPE
return krakenResponse.result.parseOhlcResult();

} else {
if (isExchangeUndergoingMaintenance(response) && keepAliveDuringMaintenance) {
LOG.warn(() -> UNDER_MAINTENANCE_WARNING_MESSAGE);
throw new ExchangeNetworkException(UNDER_MAINTENANCE_WARNING_MESSAGE);
}

final String errorMsg = FAILED_TO_GET_OHLC + response;
LOG.error(errorMsg);
throw new TradingApiException(errorMsg);
}

} else {
final String errorMsg = FAILED_TO_GET_OHLC + response;
LOG.error(errorMsg);
throw new TradingApiException(errorMsg);
}

} catch (ExchangeNetworkException | TradingApiException e) {
throw e;

} catch (Exception e) {
LOG.error(UNEXPECTED_ERROR_MSG, e);
throw new TradingApiException(UNEXPECTED_ERROR_MSG, e);
}
}

private String calculateMinuteParamFrom(OhlcInterval interval) {
switch (interval) {
case OneMinute:
return "1";
case FiveMinutes:
return "5";
case FifteenMinutes:
return "15";
case HalfHour:
return "30";
case OneHour:
return "60";
case FourHours:
return "240";
case OneDay:
return "1440";
case OneWeek:
return "10080";
case FifteenDays:
return "21600";
default:
return null;
}
}

// --------------------------------------------------------------------------
// GSON classes for JSON responses.
// See https://www.kraken.com/en-gb/help/api
Expand Down Expand Up @@ -812,6 +910,46 @@ private static class KrakenMarketOrder extends ArrayList<BigDecimal> {
private static final long serialVersionUID = -4959711260742077759L;
}

private static class KrakenOhlcResult extends HashMap<String, Object> implements Serializable {
private static final long serialVersionUID = 5632663477504483978L;

Ohlc parseOhlcResult() {
Gson gson = new Gson();
List<OhlcFrame> frames = new ArrayList<>();
Integer last = null;
if (this.entrySet().size() != 2) {
throw new IllegalStateException(
"Unknown OHLC result from the API. Maybe the api changed?"
+ "Known are currently only 'last' and the required frames in '<pair>'");
}
for (Entry<String, Object> entry : this.entrySet()) {
if (entry.getKey().equalsIgnoreCase("last")) {
JsonElement resumeId = gson.toJsonTree(entry).getAsJsonObject().get("value");
last = resumeId.getAsInt();
} else {
JsonElement jsonElement = gson.toJsonTree(entry);
for (JsonElement frame : jsonElement.getAsJsonObject().get("value").getAsJsonArray()) {
if (frame.isJsonArray()) {
JsonArray frameAsArray = frame.getAsJsonArray();
Instant timeInstant = Instant.ofEpochSecond(frameAsArray.get(0).getAsInt());
ZonedDateTime time = ZonedDateTime.ofInstant(timeInstant, ZoneId.systemDefault());
BigDecimal open = frameAsArray.get(1).getAsBigDecimal();
BigDecimal high = frameAsArray.get(2).getAsBigDecimal();
BigDecimal low = frameAsArray.get(3).getAsBigDecimal();
BigDecimal close = frameAsArray.get(4).getAsBigDecimal();
BigDecimal vwap = frameAsArray.get(5).getAsBigDecimal();
BigDecimal volume = frameAsArray.get(6).getAsBigDecimal();
Integer count = frameAsArray.get(7).getAsInt();

frames.add(new OhlcFrameImpl(time, open, high, low, close, vwap, volume, count));
}
}
}
}
return new OhlcImpl(last, frames);
}
}

/**
* Custom GSON Deserializer for Ticker API call result.
*
Expand Down Expand Up @@ -1091,8 +1229,8 @@ private void loadPairPrecisionConfig() {

if (response.getStatusCode() == HttpURLConnection.HTTP_OK) {
Type type = new TypeToken<KrakenResponse<KrakenAssetPairsConfig>>() {}.getType();
KrakenResponse<KrakenAssetPairsConfig> krakenResponse = gson.fromJson(
response.getPayload(), type);
KrakenResponse<KrakenAssetPairsConfig> krakenResponse =
gson.fromJson(response.getPayload(), type);

if (krakenResponse.error != null && !krakenResponse.error.isEmpty()) {
LOG.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.gazbert.bxbot.exchanges.trading.api.impl;

import com.gazbert.bxbot.trading.api.OhlcFrame;
import com.google.common.base.MoreObjects;
import java.math.BigDecimal;
import java.time.ZonedDateTime;

public class OhlcFrameImpl implements OhlcFrame {
private final ZonedDateTime time;
private final BigDecimal open;
private final BigDecimal high;
private final BigDecimal low;
private final BigDecimal close;
private final BigDecimal vwap;
private final BigDecimal volume;
private final Integer count;

/**
* Class representing a frame of the OHLC download.
*
* @param time the starttime of the corresponding ohlc frame
* @param open the open price of the corresponding ohl frame
* @param high the high price of the corresponding ohl frame
* @param low the low price of the corresponding ohl frame
* @param close the close price of the corresponding ohl frame
* @param vwap the volume-weighted avaerage price of the corresponding ohl frame
* @param volume the volume traded in the corresponding ohl frame
* @param count the trades cound of the corresponding ohl frame
*/
public OhlcFrameImpl(
ZonedDateTime time,
BigDecimal open,
BigDecimal high,
BigDecimal low,
BigDecimal close,
BigDecimal vwap,
BigDecimal volume,
Integer count) {
this.time = time;
this.open = open;
this.high = high;
this.low = low;
this.close = close;
this.vwap = vwap;
this.volume = volume;
this.count = count;
}

@Override
public ZonedDateTime getTime() {
return time;
}

@Override
public BigDecimal getOpen() {
return open;
}

@Override
public BigDecimal getHigh() {
return high;
}

@Override
public BigDecimal getLow() {
return low;
}

@Override
public BigDecimal getClose() {
return close;
}

@Override
public BigDecimal getVwap() {
return vwap;
}

@Override
public BigDecimal getVolume() {
return volume;
}

@Override
public Integer getCount() {
return count;
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("time", time)
.add("open", open)
.add("high", high)
.add("low", low)
.add("close", close)
.add("vwap", vwap)
.add("volume", volume)
.add("count", count)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.gazbert.bxbot.exchanges.trading.api.impl;

import com.gazbert.bxbot.trading.api.Ohlc;
import com.gazbert.bxbot.trading.api.OhlcFrame;
import java.util.List;

public class OhlcImpl implements Ohlc {
private final Integer resumeID;
private final List<OhlcFrame> frames;

/**
* This class represents the OHLC result. It contains all frames and an ID from which subsequent
* calls can resume fetching OHLC data
*
* @param resumeID the ID which can be used to resume OHLC download by ignoring all order results
* @param frames the OHLC frames in the requested packaging interval
*/
public OhlcImpl(Integer resumeID, List<OhlcFrame> frames) {
this.resumeID = resumeID;
this.frames = frames;
}

@Override
public Integer getResumeID() {
return resumeID;
}

@Override
public List<OhlcFrame> getFrames() {
return frames;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
Expand All @@ -44,6 +45,9 @@
import com.gazbert.bxbot.trading.api.BalanceInfo;
import com.gazbert.bxbot.trading.api.ExchangeNetworkException;
import com.gazbert.bxbot.trading.api.MarketOrderBook;
import com.gazbert.bxbot.trading.api.Ohlc;
import com.gazbert.bxbot.trading.api.OhlcFrame;
import com.gazbert.bxbot.trading.api.OhlcInterval;
import com.gazbert.bxbot.trading.api.OpenOrder;
import com.gazbert.bxbot.trading.api.OrderType;
import com.gazbert.bxbot.trading.api.Ticker;
Expand Down Expand Up @@ -1091,6 +1095,35 @@ public void testGettingExchangeSellingFeeIsAsExpected() throws Exception {
PowerMock.verifyAll();
}

@Test
public void testGetOhlcWorksAsExpected() throws Exception {
PowerMock.replayAll();

final ExchangeAdapter exchangeAdapter = new KrakenExchangeAdapter();
exchangeAdapter.init(exchangeConfig);

final Ohlc firstOhlc =
exchangeAdapter.getOhlc(MARKET_ID, OhlcInterval.FiveMinutes);
assertFalse(firstOhlc.getFrames().isEmpty());

for (OhlcFrame frame : firstOhlc.getFrames()) {
assertTrue(frame.getHigh().compareTo(frame.getClose()) >= 0);
assertTrue(frame.getLow().compareTo(frame.getClose()) <= 0);
assertTrue(frame.getOpen().compareTo(frame.getHigh()) <= 0);
assertTrue(frame.getOpen().compareTo(frame.getLow()) >= 0);
assertTrue(frame.toString().contains("open"));
assertTrue(frame.toString().contains("high"));
assertTrue(frame.toString().contains("low"));
assertTrue(frame.toString().contains("close"));
}

final Ohlc onlyNewestUpdates =
exchangeAdapter.getOhlc(MARKET_ID, OhlcInterval.FiveMinutes, firstOhlc.getResumeID());
assertEquals(1, onlyNewestUpdates.getFrames().size());

PowerMock.verifyAll();
}

@Test
public void testGettingExchangeBuyingFeeIsAsExpected() throws Exception {
PowerMock.replayAll();
Expand Down
Loading