From 74683d108a26e2914be4d44fb87cd5ee3f837664 Mon Sep 17 00:00:00 2001 From: nyg Date: Thu, 14 Mar 2024 23:14:08 +0100 Subject: [PATCH 1/7] WIP --- .gitignore | 1 + .../andstuff/kraken/example/TotalRewards.java | 60 ++++++++---------- examples/src/main/resources/log4j2.xml | 2 +- .../dev/andstuff/kraken/api/KrakenAPI.java | 17 +++++ .../account/LedgerEntriesEndpoint.java | 16 +++++ .../endpoint/account/LedgerInfoEndpoint.java | 14 +++++ .../account/params/LedgerEntriesParams.java | 26 ++++++++ .../account/params/LedgerInfoParams.java | 63 +++++++++++++++++++ .../account/response/LedgerEntry.java | 46 ++++++++++++++ .../endpoint/account/response/LedgerInfo.java | 8 +++ .../endpoint/priv/GenericPostParams.java | 29 ++------- .../api/model/endpoint/priv/PostParams.java | 44 ++++++++++++- .../model/endpoint/priv/PrivateEndpoint.java | 4 -- 13 files changed, 263 insertions(+), 67 deletions(-) create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerEntriesEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerInfoEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerEntriesParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java diff --git a/.gitignore b/.gitignore index cce5593..b6338fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target *.iml .idea api-keys.properties +*.txt diff --git a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java index 2434fba..f637c41 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java @@ -9,9 +9,7 @@ import java.io.IOException; import java.io.PrintStream; import java.math.BigDecimal; -import java.time.Instant; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -19,10 +17,15 @@ import com.fasterxml.jackson.databind.JsonNode; import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; +import lombok.extern.slf4j.Slf4j; /** * TODO Group by year */ +@Slf4j public class TotalRewards { public static void main(String[] args) throws IOException, InterruptedException { @@ -30,38 +33,30 @@ public static void main(String[] args) throws IOException, InterruptedException Properties apiKeys = readFromFile("/api-keys.properties"); KrakenAPI api = new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret")); - Map params = new HashMap<>(); - params.put("type", "staking"); - params.put("without_count", "true"); - params.put("ofs", "0"); + LedgerInfoParams params = LedgerInfoParams.builder() + .assetType(LedgerInfoParams.Type.STAKING) + .withoutCount(true) + .build(); - Map rewards = new HashMap<>(); + Map rewards = new HashMap<>(); boolean hasNext = true; while (hasNext) { - JsonNode response = api.query(KrakenAPI.Private.LEDGERS, params); - params.put("ofs", String.valueOf(Integer.parseInt(params.get("ofs")) + 50)); - System.out.printf("Fetched %s rewards%n", params.get("ofs")); + LedgerInfo ledger = api.ledgerInfo(params); + params = params.withNextResultOffset(); - JsonNode ledgerEntries = response.findValue("result").findValue("ledger"); - Iterator> fields = ledgerEntries.fields(); - hasNext = ledgerEntries.size() == 50; - - while (fields.hasNext()) { - Map.Entry rewardEntry = fields.next(); - rewards.put(rewardEntry.getKey(), rewardEntry.getValue()); - } + int rewardCount = ledger.entries().size(); + log.info("Fetched {} rewards", rewardCount); + hasNext = rewardCount == 50; + rewards.putAll(ledger.entries()); Thread.sleep(2000); } - Map> groupedRewards = rewards.values() - .stream() - .collect(groupingBy(e -> { - String asset = e.findValue("asset").textValue(); - return asset.equals("XETH") ? "ETH" : asset.split("[0-9.]")[0]; - })); + Map> rewardsByAsset = rewards + .values().stream() + .collect(groupingBy(e -> e.asset().equals("XETH") ? "ETH" : e.asset().split("[0-9.]")[0])); String fileName = "rewards.txt"; FileOutputStream fileOutputStream = new FileOutputStream(fileName); @@ -69,15 +64,14 @@ public static void main(String[] args) throws IOException, InterruptedException System.setOut(printStream); BigDecimal totalRewardAmountUsd = BigDecimal.ZERO; - for (String asset : groupedRewards.keySet()) { - List assetRewards = groupedRewards.get(asset).stream() - .filter(e -> !asList("migration", "spottostaking").contains(e.findValue("subtype").textValue())) - .sorted(comparing(e -> e.get("time").asInt())) + for (String asset : rewardsByAsset.keySet()) { + List assetRewards = rewardsByAsset.get(asset).stream() + .filter(e -> !asList("migration", "spottostaking").contains(e.subType())) + .sorted(comparing(LedgerEntry::time)) .toList(); BigDecimal assetTotalRewardAmount = assetRewards.stream() - .map(e -> new BigDecimal(e.findValue("amount").textValue()) - .subtract(new BigDecimal(e.findValue("fee").textValue()))) + .map(LedgerEntry::netAmount) .reduce(BigDecimal::add) .orElse(BigDecimal.ONE.negate()); BigDecimal assetRate = fetchRate(asset, api); @@ -90,11 +84,7 @@ public static void main(String[] args) throws IOException, InterruptedException System.out.println("================================================================="); assetRewards.forEach(reward -> System.out.printf("%-10s %s %16s %16s %s%n", - reward.get("asset").textValue(), - Instant.ofEpochSecond(reward.get("time").asLong()), - new BigDecimal(reward.get("amount").textValue()), - new BigDecimal(reward.get("fee").textValue()), - reward.get("subtype").textValue())); + reward.asset(), reward.time(), reward.amount(), reward.fee(), reward.subType())); } System.out.println(); diff --git a/examples/src/main/resources/log4j2.xml b/examples/src/main/resources/log4j2.xml index d4aa8bc..fb676b4 100644 --- a/examples/src/main/resources/log4j2.xml +++ b/examples/src/main/resources/log4j2.xml @@ -1,7 +1,7 @@ - [%d{ISO8601}] [%-20.20c{1.}] [%-7.7t] [%-5.5p] %m%n + [%d{ISO8601}] %-5.5p [%-20.20c{1.}] [%-7.7t] %m%n diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java index 265eaf4..03303dc 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -5,6 +5,12 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.andstuff.kraken.api.model.endpoint.account.LedgerEntriesEndpoint; +import dev.andstuff.kraken.api.model.endpoint.account.LedgerInfoEndpoint; +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerEntriesParams; +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; import dev.andstuff.kraken.api.model.endpoint.market.AssetInfoEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.AssetPairEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.ServerTimeEndpoint; @@ -46,6 +52,7 @@ public SystemStatus systemStatus() { return restRequester.execute(new SystemStatusEndpoint()); } + // TODO maybe assets() ? + return type Assets that can return both map and list public Map assetInfo(List assets) { return restRequester.execute(new AssetInfoEndpoint(assets)); } @@ -62,6 +69,16 @@ public Map assetPairs(List pair, AssetPair.Info info) return restRequester.execute(new AssetPairEndpoint(pair, info)); } + /* Implemented private endpoints */ + + public LedgerInfo ledgerInfo(LedgerInfoParams params) { + return restRequester.execute(new LedgerInfoEndpoint(params)); + } + + public Map ledgerEntries(LedgerEntriesParams params) { + return restRequester.execute(new LedgerEntriesEndpoint(params)); + } + /* Query unimplemented endpoints */ public JsonNode query(Public endpoint) { diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerEntriesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerEntriesEndpoint.java new file mode 100644 index 0000000..3957488 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerEntriesEndpoint.java @@ -0,0 +1,16 @@ +package dev.andstuff.kraken.api.model.endpoint.account; + +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerEntriesParams; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import dev.andstuff.kraken.api.model.endpoint.priv.PrivateEndpoint; + +public class LedgerEntriesEndpoint extends PrivateEndpoint> { + + public LedgerEntriesEndpoint(LedgerEntriesParams params) { + super("QueryLedgers", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerInfoEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerInfoEndpoint.java new file mode 100644 index 0000000..ec8969f --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/LedgerInfoEndpoint.java @@ -0,0 +1,14 @@ +package dev.andstuff.kraken.api.model.endpoint.account; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; +import dev.andstuff.kraken.api.model.endpoint.priv.PrivateEndpoint; + +public class LedgerInfoEndpoint extends PrivateEndpoint { + + public LedgerInfoEndpoint(LedgerInfoParams params) { + super("Ledgers", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerEntriesParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerEntriesParams.java new file mode 100644 index 0000000..10ff3fc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerEntriesParams.java @@ -0,0 +1,26 @@ +package dev.andstuff.kraken.api.model.endpoint.account.params; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.model.endpoint.priv.PostParams; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder(toBuilder = true) +public class LedgerEntriesParams extends PostParams { + + @Builder.Default + private final List entryIds = List.of(); + private final boolean includeTrades; + + @Override + public Map params() { + HashMap params = new HashMap<>(); + putIfNonNull(params, "id", entryIds, v -> String.join(",", v)); + putIfNonNull(params, "trades", includeTrades); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java new file mode 100644 index 0000000..2b84bc6 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java @@ -0,0 +1,63 @@ +package dev.andstuff.kraken.api.model.endpoint.account.params; + +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.model.endpoint.priv.PostParams; +import lombok.Builder; +import lombok.Getter; +import lombok.With; + +@Getter +@Builder(toBuilder = true) +public class LedgerInfoParams extends PostParams { + + private final List assets; + private final String assetClass; + private final Type assetType; + private final Instant startDate; + private final Instant endDate; + private final String startId; // TODO handle + private final String endId; // TODO naming + private final boolean withoutCount; + + @With + @Builder.Default + private final int resultOffset = 0; + + @Override + protected Map params() { + HashMap params = new HashMap<>(); + putIfNonNull(params, "asset", assets, v -> String.join(",", v)); + putIfNonNull(params, "aclass", assetClass); + putIfNonNull(params, "type", assetType, e -> e.toString().toLowerCase()); + putIfNonNull(params, "start", startDate, d -> Long.toString(d.getEpochSecond())); + putIfNonNull(params, "end", endDate, d -> Long.toString(d.getEpochSecond())); + putIfNonNull(params, "without_count", withoutCount); + putIfNonNull(params, "ofs", resultOffset); + return params; + } + + public LedgerInfoParams withNextResultOffset() { + return this.withResultOffset(resultOffset + 50); + } + + public enum Type { + ALL, + TRADE, + DEPOSIT, + WITHDRAWAL, + TRANSFER, + MARGIN, + ADJUSTMENT, + ROLLOVER, + CREDIT, + SETTLED, + STAKING, + DIVIDEND, + SALE, + NFT_REBATE, + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java new file mode 100644 index 0000000..b36df07 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java @@ -0,0 +1,46 @@ +package dev.andstuff.kraken.api.model.endpoint.account.response; + +import java.math.BigDecimal; +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LedgerEntry(@JsonProperty("refid") String referenceId, + Instant time, + Type type, + @JsonProperty("subtype") String subType, + @JsonProperty("aclass") String assetClass, + String asset, + BigDecimal amount, + BigDecimal fee, + BigDecimal balance) { + + public BigDecimal netAmount() { + return amount.subtract(fee); + } + + public enum Type { + NONE, + TRADE, + DEPOSIT, + WITHDRAWAL, + TRANSFER, + MARGIN, + ADJUSTMENT, + ROLLOVER, + SPEND, + RECEIVE, + SETTLED, + CREDIT, + STAKING, + REWARD, + DIVIDEND, + SALE, + CONVERSION, + NFTTRADE, // TODO + NFTCREATORFEE, + NFTREBATE, + CUSTODYTRANSFER, + // TODO not implemented + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java new file mode 100644 index 0000000..f1314ea --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java @@ -0,0 +1,8 @@ +package dev.andstuff.kraken.api.model.endpoint.account.response; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LedgerInfo(@JsonProperty("ledger") Map entries, + int count) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/GenericPostParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/GenericPostParams.java index 9f15aa1..62096c2 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/GenericPostParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/GenericPostParams.java @@ -1,37 +1,18 @@ package dev.andstuff.kraken.api.model.endpoint.priv; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -public class GenericPostParams implements PostParams { +public class GenericPostParams extends PostParams { - private final Map params; + private final Map params = new HashMap<>(); public GenericPostParams(Map params) { - this.params = new HashMap<>(params); + this.params.putAll(params); } @Override - public String initNonce() { - String nonce = Long.toString(System.currentTimeMillis()); - params.put("nonce", nonce); - return nonce; - } - - @Override - public String encoded() { - // TODO handle nested props - return params.keySet().stream() - .reduce( - new StringBuilder(), - (postData, key) -> postData.append(key) - .append("=") - .append(URLEncoder.encode(params.get(key), StandardCharsets.UTF_8)) - .append("&"), - StringBuilder::append) - .toString() - .replaceFirst("&$", ""); + protected Map params() { + return params; } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PostParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PostParams.java index 623fcd7..02f3c82 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PostParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PostParams.java @@ -1,8 +1,46 @@ package dev.andstuff.kraken.api.model.endpoint.priv; -public interface PostParams { // TODO maybe create an abstract class +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.Function; - String initNonce(); +public abstract class PostParams { - String encoded(); + protected abstract Map params(); + + private String nonce; + + public String initNonce() { + nonce = Long.toString(System.currentTimeMillis()); + return nonce; + } + + // TODO handle nested props + public String encoded() { + + Map params = params(); + params.put("nonce", nonce); + + return params.keySet().stream() + .reduce( + new StringBuilder(), + (postData, key) -> postData.append(key) + .append("=") + .append(URLEncoder.encode(params.get(key), StandardCharsets.UTF_8)) + .append("&"), + StringBuilder::append) + .toString() + .replaceFirst("&$", ""); + } + + protected static void putIfNonNull(Map map, String key, T value) { + putIfNonNull(map, key, value, Object::toString); + } + + protected static void putIfNonNull(Map map, String key, T value, Function apply) { + if (value != null) { + map.put(key, apply.apply(value)); + } + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PrivateEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PrivateEndpoint.java index d63964a..cff9a55 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PrivateEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/priv/PrivateEndpoint.java @@ -15,10 +15,6 @@ public class PrivateEndpoint extends Endpoint { private final PostParams postParams; - public PrivateEndpoint(String path, TypeReference responseType) { - this(path, null, responseType); - } - public PrivateEndpoint(String path, PostParams postParams, TypeReference responseType) { super("POST", path, responseType); this.postParams = postParams; From e48cccd70db58d667e841e188e677910af2522ae Mon Sep 17 00:00:00 2001 From: nyg Date: Sun, 17 Mar 2024 00:41:44 +0100 Subject: [PATCH 2/7] WIP --- .../andstuff/kraken/example/TotalRewards.java | 32 ++++++++++----- .../dev/andstuff/kraken/api/KrakenAPI.java | 12 ++++-- .../model/endpoint/market/TickerEndpoint.java | 17 ++++++++ .../endpoint/market/params/TickerParams.java | 17 ++++++++ .../endpoint/market/response/Ticker.java | 41 +++++++++++++++++++ 5 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/TickerEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/TickerParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/Ticker.java diff --git a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java index f637c41..f81da92 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java @@ -14,16 +14,18 @@ import java.util.Map; import java.util.Properties; -import com.fasterxml.jackson.databind.JsonNode; - import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.model.KrakenException; import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; import lombok.extern.slf4j.Slf4j; /** - * TODO Group by year + * TODO + * - Group by year + * - One csv file with staking rewards + * - Another with summary (asset, years, total) */ @Slf4j public class TotalRewards { @@ -56,7 +58,7 @@ public static void main(String[] args) throws IOException, InterruptedException Map> rewardsByAsset = rewards .values().stream() - .collect(groupingBy(e -> e.asset().equals("XETH") ? "ETH" : e.asset().split("[0-9.]")[0])); + .collect(groupingBy(TotalRewards::extractAsset)); String fileName = "rewards.txt"; FileOutputStream fileOutputStream = new FileOutputStream(fileName); @@ -93,15 +95,23 @@ public static void main(String[] args) throws IOException, InterruptedException private static BigDecimal fetchRate(String asset, KrakenAPI api) { try { - Map tickerParams = new HashMap<>(); - tickerParams.put("pair", asset + "USD"); - - JsonNode tickerResponse = api.query(KrakenAPI.Public.TICKER, tickerParams).findValue("result"); - return new BigDecimal(tickerResponse.findValue(tickerResponse.fieldNames().next()).findValue("c").get(0).textValue()); + String referenceAsset = "USD"; + return asset.equals(referenceAsset) + ? BigDecimal.ONE + : api.ticker(List.of(asset + referenceAsset)) + .values().stream() + .findAny().orElseThrow() + .lastTrade().price(); } - catch (Exception e) { - System.err.printf("Couldn't fetch rate for %s%n", asset); + catch (KrakenException e) { + log.error("Couldn't fetch rate for {}", asset); return BigDecimal.ONE.negate(); } } + + private static String extractAsset(LedgerEntry ledgerEntry) { + String rawAsset = ledgerEntry.asset(); + boolean originalAsset = rawAsset.matches("^([XZ])([A-Z]{3})$"); + return originalAsset ? rawAsset.substring(1, 4) : rawAsset.split("[0-9.]")[0]; + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java index 03303dc..61790b2 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -15,10 +15,12 @@ import dev.andstuff.kraken.api.model.endpoint.market.AssetPairEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.ServerTimeEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.SystemStatusEndpoint; +import dev.andstuff.kraken.api.model.endpoint.market.TickerEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair; import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime; import dev.andstuff.kraken.api.model.endpoint.market.response.SystemStatus; +import dev.andstuff.kraken.api.model.endpoint.market.response.Ticker; import dev.andstuff.kraken.api.model.endpoint.priv.JsonPrivateEndpoint; import dev.andstuff.kraken.api.model.endpoint.pub.JsonPublicEndpoint; import dev.andstuff.kraken.api.rest.DefaultKrakenRestRequester; @@ -52,7 +54,7 @@ public SystemStatus systemStatus() { return restRequester.execute(new SystemStatusEndpoint()); } - // TODO maybe assets() ? + return type Assets that can return both map and list + // TODO maybe return type Assets that can return both map and list public Map assetInfo(List assets) { return restRequester.execute(new AssetInfoEndpoint(assets)); } @@ -61,14 +63,18 @@ public Map assetInfo(List assets, String assetClass) return restRequester.execute(new AssetInfoEndpoint(assets, assetClass)); } - public Map assetPairs(List pair) { - return restRequester.execute(new AssetPairEndpoint(pair)); + public Map assetPairs(List pairs) { + return restRequester.execute(new AssetPairEndpoint(pairs)); } public Map assetPairs(List pair, AssetPair.Info info) { return restRequester.execute(new AssetPairEndpoint(pair, info)); } + public Map ticker(List pairs) { + return restRequester.execute(new TickerEndpoint(pairs)); + } + /* Implemented private endpoints */ public LedgerInfo ledgerInfo(LedgerInfoParams params) { diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/TickerEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/TickerEndpoint.java new file mode 100644 index 0000000..b47776c --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/TickerEndpoint.java @@ -0,0 +1,17 @@ +package dev.andstuff.kraken.api.model.endpoint.market; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.model.endpoint.market.params.TickerParams; +import dev.andstuff.kraken.api.model.endpoint.market.response.Ticker; +import dev.andstuff.kraken.api.model.endpoint.pub.PublicEndpoint; + +public class TickerEndpoint extends PublicEndpoint> { + + public TickerEndpoint(List pairs) { + super("Ticker", new TickerParams(pairs), new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/TickerParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/TickerParams.java new file mode 100644 index 0000000..55809fc --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/TickerParams.java @@ -0,0 +1,17 @@ +package dev.andstuff.kraken.api.model.endpoint.market.params; + +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.model.endpoint.pub.QueryParams; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TickerParams implements QueryParams { + + private final List pairs; + + public Map toMap() { + return Map.of("pair", String.join(",", pairs)); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/Ticker.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/Ticker.java new file mode 100644 index 0000000..36d5c83 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/Ticker.java @@ -0,0 +1,41 @@ +package dev.andstuff.kraken.api.model.endpoint.market.response; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +public record Ticker(@JsonProperty("a") Ask ask, + @JsonProperty("b") Bid bid, + @JsonProperty("c") LastTrade lastTrade, + @JsonProperty("v") Volume volume, + @JsonProperty("p") VWAP volumeWeightedAveragePrice, + @JsonProperty("t") TradeCount tradeCount, + @JsonProperty("l") Low low, + @JsonProperty("h") High high, + @JsonProperty("o") BigDecimal openingPrice) { + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record Ask(BigDecimal price, BigDecimal wholeLotVolume, BigDecimal lotVolume) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record Bid(BigDecimal price, BigDecimal wholeLotVolume, BigDecimal lotVolume) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record LastTrade(BigDecimal price, BigDecimal volume) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record Volume(BigDecimal today, BigDecimal last24Hours) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record VWAP(BigDecimal today, BigDecimal last24Hours) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record TradeCount(int today, int last24Hours) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record Low(BigDecimal today, BigDecimal last24Hours) {} + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record High(BigDecimal today, BigDecimal last24Hours) {} +} From 6953ecb9cbaf4fda2a09891e1ce136c821703005 Mon Sep 17 00:00:00 2001 From: nyg Date: Sun, 24 Mar 2024 22:03:03 +0100 Subject: [PATCH 3/7] wip --- .gitignore | 1 + examples/pom.xml | 10 ++ .../dev/andstuff/kraken/example/Examples.java | 2 +- .../andstuff/kraken/example/TotalRewards.java | 117 ------------------ .../HeaderAndPositionMappingStrategy.java | 22 ++++ .../{ => helper}/PropertiesHelper.java | 4 +- .../kraken/example/reward/AssetRates.java | 38 ++++++ .../example/reward/RewardAggregate.java | 60 +++++++++ .../example/reward/StakingRewardsSummary.java | 89 +++++++++++++ .../example/reward/csv/CsvLedgerEntries.java | 44 +++++++ .../example/reward/csv/CsvLedgerEntry.java | 33 +++++ .../reward/csv/CsvYearlyAssetRewards.java | 73 +++++++++++ .../account/response/LedgerEntry.java | 16 ++- .../endpoint/account/response/LedgerInfo.java | 14 ++- 14 files changed, 401 insertions(+), 122 deletions(-) delete mode 100644 examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/helper/HeaderAndPositionMappingStrategy.java rename examples/src/main/java/dev/andstuff/kraken/example/{ => helper}/PropertiesHelper.java (82%) create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntries.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntry.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java diff --git a/.gitignore b/.gitignore index b6338fb..07a44b2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target .idea api-keys.properties *.txt +*.csv diff --git a/examples/pom.xml b/examples/pom.xml index 3d91ebd..4d19678 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -13,6 +13,7 @@ 2.23.0 + 5.9 @@ -21,6 +22,8 @@ kraken-api ${project.version} + + org.apache.logging.log4j log4j-core @@ -33,6 +36,13 @@ ${log4j.version} runtime + + + + com.opencsv + opencsv + ${opencsv.version} + diff --git a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java b/examples/src/main/java/dev/andstuff/kraken/example/Examples.java index 0d6f34f..5cd9855 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/Examples.java @@ -1,6 +1,6 @@ package dev.andstuff.kraken.example; -import static dev.andstuff.kraken.example.PropertiesHelper.readFromFile; +import static dev.andstuff.kraken.example.helper.PropertiesHelper.readFromFile; import java.util.List; import java.util.Map; diff --git a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java deleted file mode 100644 index f81da92..0000000 --- a/examples/src/main/java/dev/andstuff/kraken/example/TotalRewards.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.andstuff.kraken.example; - -import static dev.andstuff.kraken.example.PropertiesHelper.readFromFile; -import static java.util.Arrays.asList; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.groupingBy; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import dev.andstuff.kraken.api.KrakenAPI; -import dev.andstuff.kraken.api.model.KrakenException; -import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; -import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; -import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; -import lombok.extern.slf4j.Slf4j; - -/** - * TODO - * - Group by year - * - One csv file with staking rewards - * - Another with summary (asset, years, total) - */ -@Slf4j -public class TotalRewards { - - public static void main(String[] args) throws IOException, InterruptedException { - - Properties apiKeys = readFromFile("/api-keys.properties"); - KrakenAPI api = new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret")); - - LedgerInfoParams params = LedgerInfoParams.builder() - .assetType(LedgerInfoParams.Type.STAKING) - .withoutCount(true) - .build(); - - Map rewards = new HashMap<>(); - - boolean hasNext = true; - while (hasNext) { - - LedgerInfo ledger = api.ledgerInfo(params); - params = params.withNextResultOffset(); - - int rewardCount = ledger.entries().size(); - log.info("Fetched {} rewards", rewardCount); - hasNext = rewardCount == 50; - - rewards.putAll(ledger.entries()); - Thread.sleep(2000); - } - - Map> rewardsByAsset = rewards - .values().stream() - .collect(groupingBy(TotalRewards::extractAsset)); - - String fileName = "rewards.txt"; - FileOutputStream fileOutputStream = new FileOutputStream(fileName); - PrintStream printStream = new PrintStream(fileOutputStream); - System.setOut(printStream); - - BigDecimal totalRewardAmountUsd = BigDecimal.ZERO; - for (String asset : rewardsByAsset.keySet()) { - List assetRewards = rewardsByAsset.get(asset).stream() - .filter(e -> !asList("migration", "spottostaking").contains(e.subType())) - .sorted(comparing(LedgerEntry::time)) - .toList(); - - BigDecimal assetTotalRewardAmount = assetRewards.stream() - .map(LedgerEntry::netAmount) - .reduce(BigDecimal::add) - .orElse(BigDecimal.ONE.negate()); - BigDecimal assetRate = fetchRate(asset, api); - BigDecimal assetTotalRewardAmountUsd = assetTotalRewardAmount.multiply(assetRate); - totalRewardAmountUsd = totalRewardAmountUsd.add(assetTotalRewardAmountUsd); - - System.out.println(); - System.out.printf("Asset: %s, reward count: %s, total rewards: %s, USD: %s%n", - asset, assetRewards.size(), assetTotalRewardAmount, assetTotalRewardAmountUsd); - System.out.println("================================================================="); - - assetRewards.forEach(reward -> System.out.printf("%-10s %s %16s %16s %s%n", - reward.asset(), reward.time(), reward.amount(), reward.fee(), reward.subType())); - } - - System.out.println(); - System.out.printf("Total USD: %s%n", totalRewardAmountUsd); - } - - private static BigDecimal fetchRate(String asset, KrakenAPI api) { - try { - String referenceAsset = "USD"; - return asset.equals(referenceAsset) - ? BigDecimal.ONE - : api.ticker(List.of(asset + referenceAsset)) - .values().stream() - .findAny().orElseThrow() - .lastTrade().price(); - } - catch (KrakenException e) { - log.error("Couldn't fetch rate for {}", asset); - return BigDecimal.ONE.negate(); - } - } - - private static String extractAsset(LedgerEntry ledgerEntry) { - String rawAsset = ledgerEntry.asset(); - boolean originalAsset = rawAsset.matches("^([XZ])([A-Z]{3})$"); - return originalAsset ? rawAsset.substring(1, 4) : rawAsset.split("[0-9.]")[0]; - } -} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/helper/HeaderAndPositionMappingStrategy.java b/examples/src/main/java/dev/andstuff/kraken/example/helper/HeaderAndPositionMappingStrategy.java new file mode 100644 index 0000000..75bb81b --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/helper/HeaderAndPositionMappingStrategy.java @@ -0,0 +1,22 @@ +package dev.andstuff.kraken.example.helper; + +import com.opencsv.bean.ColumnPositionMappingStrategy; +import com.opencsv.bean.CsvBindByName; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; + +public class HeaderAndPositionMappingStrategy extends ColumnPositionMappingStrategy { + + @Override + public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException { + super.generateHeader(bean); + + int fieldCount = getFieldMap().values().size(); + String[] header = new String[fieldCount]; + + for (int i = 0; i < fieldCount; i++) { + header[i] = findField(i).getField().getDeclaredAnnotation(CsvBindByName.class).column(); + } + + return header; + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java b/examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java similarity index 82% rename from examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java rename to examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java index fb44f12..2affed3 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java @@ -1,4 +1,4 @@ -package dev.andstuff.kraken.example; +package dev.andstuff.kraken.example.helper; import java.io.IOException; import java.io.InputStream; @@ -12,7 +12,7 @@ public final class PropertiesHelper { public static Properties readFromFile(String path) { try { - InputStream stream = Examples.class.getResourceAsStream(path); + InputStream stream = PropertiesHelper.class.getResourceAsStream(path); Properties properties = new Properties(); properties.load(stream); return properties; diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java new file mode 100644 index 0000000..d611eda --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java @@ -0,0 +1,38 @@ +package dev.andstuff.kraken.example.reward; + +import static java.util.stream.Collectors.toMap; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Map; +import java.util.Optional; + +import dev.andstuff.kraken.api.model.endpoint.market.response.Ticker; +import lombok.Getter; + +@Getter +public class AssetRates { + + public static final String REFERENCE_ASSET = "USD"; + private static final int REFERENCE_ASSET_DECIMALS = 2; + + private final Map rates; + + public AssetRates(Map tickers) { + rates = tickers.entrySet().stream() + .collect(toMap(Map.Entry::getKey, this::extractRate)); + rates.put(REFERENCE_ASSET + REFERENCE_ASSET, BigDecimal.ONE); + } + + public BigDecimal evaluate(BigDecimal amount, String asset) { + String normalPair = asset + REFERENCE_ASSET; + String commodityPair = "X%sZ%s".formatted(asset, REFERENCE_ASSET); + BigDecimal rate = Optional.ofNullable(rates.get(normalPair)) + .orElseGet(() -> rates.getOrDefault(commodityPair, BigDecimal.ZERO)); + return rate.multiply(amount).setScale(REFERENCE_ASSET_DECIMALS, RoundingMode.HALF_UP); + } + + private BigDecimal extractRate(Map.Entry entry) { + return entry.getValue().lastTrade().price(); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java new file mode 100644 index 0000000..5f7f42b --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java @@ -0,0 +1,60 @@ +package dev.andstuff.kraken.example.reward; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +import java.math.BigDecimal; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import lombok.Getter; + +@Getter +public class RewardAggregate { // YearlyStakingRewards + + private final Set assets; + private final Set years; + private final Set assetRewards; + + public RewardAggregate(List rewards) { + + assets = rewards.stream().map(LedgerEntry::underlyingAsset).collect(toSet()); + years = rewards.stream().map(RewardAggregate::extractYear).collect(toSet()); + + assetRewards = rewards.stream() + .collect(groupingBy(LedgerEntry::underlyingAsset)) + .entrySet().stream() + .map(entry -> new AssetRewards(entry.getKey(), entry.getValue())) + .collect(toSet()); + } + + @Getter + public static class AssetRewards { + + private final String asset; + private final BigDecimal totalReward; + private final Map yearlyRewards; + + public AssetRewards(String asset, List rewards) { + this.asset = asset; + + this.totalReward = rewards.stream() + .map(LedgerEntry::netAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + this.yearlyRewards = rewards.stream() + .collect(toMap( + RewardAggregate::extractYear, + LedgerEntry::netAmount, + BigDecimal::add)); + } + } + + private static int extractYear(LedgerEntry ledgerEntry) { + return ledgerEntry.time().atZone(ZoneId.of("UTC")).getYear(); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java new file mode 100644 index 0000000..9331693 --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java @@ -0,0 +1,89 @@ +package dev.andstuff.kraken.example.reward; + +import static dev.andstuff.kraken.example.helper.PropertiesHelper.readFromFile; +import static java.util.Arrays.asList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.model.KrakenException; +import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; +import dev.andstuff.kraken.example.reward.csv.CsvLedgerEntries; +import dev.andstuff.kraken.example.reward.csv.CsvYearlyAssetRewards; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class StakingRewardsSummary { + + private final KrakenAPI api; + + public static void main(String[] args) { + Properties apiKeys = readFromFile("/api-keys.properties"); + new StakingRewardsSummary(new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret"))).generate(); + } + + public void generate() { + List rewards = fetchAllStakingRewards(); + RewardAggregate rewardAggregate = new RewardAggregate(rewards); + AssetRates rates = fetchRatesFor(rewardAggregate.getAssets()); + + new CsvLedgerEntries(rewards).writeToFile("rewards.csv"); + new CsvYearlyAssetRewards(rewardAggregate, rates).writeToFile("rewards-summary.csv"); + } + + private List fetchAllStakingRewards() { + + List rewards = new ArrayList<>(); + LedgerInfoParams params = LedgerInfoParams.builder() + .assetType(LedgerInfoParams.Type.STAKING) + .withoutCount(true) + .build(); + + boolean hasNext = true; + while (hasNext) { + + LedgerInfo ledgerInfo = api.ledgerInfo(params); + List ledgerEntries = ledgerInfo.asList(); + log.info("Fetched {} rewards", ledgerEntries.size()); + + params = params.withNextResultOffset(); + hasNext = ledgerInfo.hasNext(); + + rewards.addAll(ledgerEntries.stream().filter(StakingRewardsSummary::isStakingReward).toList()); + + try { + Thread.sleep(2000); + } + catch (InterruptedException e) { + log.warn("Thread has been interrupted"); + Thread.currentThread().interrupt(); + } + } + + return rewards; + } + + private AssetRates fetchRatesFor(Set assets) { + try { + List pairs = assets.stream() + .map(asset -> asset + AssetRates.REFERENCE_ASSET) + .filter(pair -> !pair.equals("USD" + AssetRates.REFERENCE_ASSET)) + .toList(); + return new AssetRates(api.ticker(pairs)); + } + catch (KrakenException e) { + throw new RuntimeException("Couldn't fetch rates", e); + } + } + + private static boolean isStakingReward(LedgerEntry entry) { + return !asList("migration", "spottostaking").contains(entry.subType()); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntries.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntries.java new file mode 100644 index 0000000..20589de --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntries.java @@ -0,0 +1,44 @@ +package dev.andstuff.kraken.example.reward.csv; + +import static java.util.Comparator.comparing; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.List; + +import com.opencsv.bean.MappingStrategy; +import com.opencsv.bean.StatefulBeanToCsvBuilder; +import com.opencsv.exceptions.CsvDataTypeMismatchException; +import com.opencsv.exceptions.CsvRequiredFieldEmptyException; + +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import dev.andstuff.kraken.example.helper.HeaderAndPositionMappingStrategy; + +public class CsvLedgerEntries { + + private final List ledgerEntries; + + public CsvLedgerEntries(List ledgerEntries) { + this.ledgerEntries = ledgerEntries.stream() + .map(CsvLedgerEntry::new) + .sorted(comparing(CsvLedgerEntry::time)) + .toList(); + } + + public void writeToFile(String fileName) { + + try (Writer writer = new FileWriter(fileName)) { + MappingStrategy mappingStrategy = new HeaderAndPositionMappingStrategy<>(); + mappingStrategy.setType(CsvLedgerEntry.class); + + new StatefulBeanToCsvBuilder(writer) + .withMappingStrategy(mappingStrategy) + .build() + .write(ledgerEntries); + } + catch (CsvRequiredFieldEmptyException | CsvDataTypeMismatchException | IOException e) { + throw new RuntimeException("Couldn't write ledger entries to file", e); + } + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntry.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntry.java new file mode 100644 index 0000000..db4a5d1 --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvLedgerEntry.java @@ -0,0 +1,33 @@ +package dev.andstuff.kraken.example.reward.csv; + +import java.math.BigDecimal; +import java.time.Instant; + +import com.opencsv.bean.CsvBindByName; +import com.opencsv.bean.CsvBindByPosition; + +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; + +public record CsvLedgerEntry(@CsvBindByPosition(position = 7) @CsvBindByName(column = "ledger_entry_id") String ledgerEntryId, + @CsvBindByPosition(position = 8) @CsvBindByName(column = "reference_id") String referenceId, + @CsvBindByPosition(position = 0) @CsvBindByName(column = "date") Instant time, + @CsvBindByPosition(position = 3) @CsvBindByName(column = "type") String type, + @CsvBindByPosition(position = 4) @CsvBindByName(column = "sub_type") String subType, + @CsvBindByPosition(position = 1) @CsvBindByName(column = "asset") String asset, + @CsvBindByPosition(position = 2) @CsvBindByName(column = "staking_asset") String stakingAsset, + @CsvBindByPosition(position = 5) @CsvBindByName(column = "amount") BigDecimal amount, + @CsvBindByPosition(position = 6) @CsvBindByName(column = "fee") BigDecimal fee) { + + public CsvLedgerEntry(LedgerEntry ledgerEntry) { + this( + ledgerEntry.id(), + ledgerEntry.referenceId(), + ledgerEntry.time(), + ledgerEntry.type().name().toLowerCase(), + ledgerEntry.subType(), + ledgerEntry.underlyingAsset(), + ledgerEntry.asset(), + ledgerEntry.amount(), + ledgerEntry.fee()); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java new file mode 100644 index 0000000..2d2ca11 --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java @@ -0,0 +1,73 @@ +package dev.andstuff.kraken.example.reward.csv; + +import static java.util.Comparator.comparing; +import static java.util.Comparator.reverseOrder; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import java.io.FileWriter; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import com.opencsv.CSVWriter; + +import dev.andstuff.kraken.example.reward.AssetRates; +import dev.andstuff.kraken.example.reward.RewardAggregate; + +public class CsvYearlyAssetRewards { + + private final List headers; + private final Map> assetRewardRows; + + public CsvYearlyAssetRewards(RewardAggregate aggregate, AssetRates rates) { + + // build header row - adding columns for fiat valuation + Set years = aggregate.getYears(); + headers = years.stream().flatMap(e -> Stream.of(e.toString(), "")).collect(toList()); + headers.addFirst("Asset"); + headers.addAll(List.of("Total", "")); + + // build asset reward rows with rates + Set assetRewards = aggregate.getAssetRewards(); + assetRewardRows = assetRewards.stream() + .collect(toMap( + RewardAggregate.AssetRewards::getAsset, + assetReward -> { + Map yearlyRewards = assetReward.getYearlyRewards(); + List yearlyRewardsWithRates = years.stream() + .flatMap(year -> { + BigDecimal reward = yearlyRewards.getOrDefault(year, BigDecimal.ZERO); + BigDecimal fiatValue = rates.evaluate(reward, assetReward.getAsset()); + return Stream.of(reward, fiatValue); + }) + .collect(toList()); + BigDecimal totalReward = assetReward.getTotalReward(); + yearlyRewardsWithRates.addAll(List.of(totalReward, rates.evaluate(totalReward, assetReward.getAsset()))); + return yearlyRewardsWithRates; + })); + } + + public void writeToFile(String fileName) { + try (CSVWriter writer = new CSVWriter(new FileWriter(fileName))) { + writer.writeNext(headers.toArray(new String[0])); // Asset, y1, y2, y3, ..., total + + List array = assetRewardRows.entrySet().stream() + .map(entry -> { + List collect = entry.getValue().stream().map(BigDecimal::toPlainString).collect(toList()); + collect.addFirst(entry.getKey()); + return collect.toArray(new String[0]); + }) + .sorted(comparing(e -> new BigDecimal(e[e.length - 1]), reverseOrder())) + .toList(); + + writer.writeAll(array); // ETH, xxx, xxx, xxx + } + catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java index b36df07..717d7eb 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java @@ -5,7 +5,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record LedgerEntry(@JsonProperty("refid") String referenceId, +import lombok.With; + +public record LedgerEntry(@With String id, // TODO + @JsonProperty("refid") String referenceId, Instant time, Type type, @JsonProperty("subtype") String subType, @@ -19,6 +22,17 @@ public BigDecimal netAmount() { return amount.subtract(fee); } + /** + * Attempts to extract the underlying asset, e.g. DOT.28S returns DOT, XXBT returns XBT, ZUSD returns USD. + * + * @return the underlying asset + */ + public String underlyingAsset() { + return asset.matches("^([XZ])([A-Z]{3})$") + ? asset.substring(1, 4) + : asset.split("[0-9.]")[0]; + } + public enum Type { NONE, TRADE, diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java index f1314ea..7f4d795 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java @@ -1,8 +1,20 @@ package dev.andstuff.kraken.api.model.endpoint.account.response; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; public record LedgerInfo(@JsonProperty("ledger") Map entries, - int count) {} + int count) { + + public boolean hasNext() { + return entries.size() == 50; + } + + public List asList() { + return entries.entrySet().stream() + .map(entry -> entry.getValue().withId(entry.getKey())) + .toList(); + } +} From 7d36636f6e76285bc7108d8935cd75b7770f992b Mon Sep 17 00:00:00 2001 From: nyg Date: Wed, 27 Mar 2024 22:44:02 +0100 Subject: [PATCH 4/7] wip --- README.md | 6 +- .../{Examples.java => SimpleExamples.java} | 10 +-- ...java => StakingRewardsSummaryExample.java} | 54 +++++++------ ...tiesHelper.java => CredentialsHelper.java} | 9 ++- .../kraken/example/reward/AssetRates.java | 18 +++-- .../kraken/example/reward/AssetRewards.java | 32 ++++++++ .../example/reward/RewardAggregate.java | 60 -------------- .../kraken/example/reward/StakingRewards.java | 32 ++++++++ .../reward/csv/CsvYearlyAssetRewards.java | 78 +++++++++++-------- .../dev/andstuff/kraken/api/KrakenAPI.java | 5 ++ .../account/params/LedgerInfoParams.java | 4 +- .../account/response/LedgerEntry.java | 26 +++++-- .../endpoint/account/response/LedgerInfo.java | 16 +++- .../api/rest/DefaultKrakenRestRequester.java | 4 + 14 files changed, 201 insertions(+), 153 deletions(-) rename examples/src/main/java/dev/andstuff/kraken/example/{Examples.java => SimpleExamples.java} (87%) rename examples/src/main/java/dev/andstuff/kraken/example/{reward/StakingRewardsSummary.java => StakingRewardsSummaryExample.java} (52%) rename examples/src/main/java/dev/andstuff/kraken/example/helper/{PropertiesHelper.java => CredentialsHelper.java} (59%) create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRewards.java delete mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewards.java diff --git a/README.md b/README.md index d4b030f..14e2403 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,6 @@ See `DefaultKrakenRestRequester` for the default implementation. ### Custom nonce generator (not yet implemented) - - ## Examples The `examples` Maven module contains some examples that might be worth checking (e.g. total staking rewards summary). The examples can be run directly from your IDE, or from the command line. @@ -99,10 +97,12 @@ For private endpoints, you need to rename `api-keys.properties.example` (located mvn clean install # run example classes -mvn -q -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.Examples +mvn -q -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.SimpleExamples mvn -q -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.TotalRewards ``` [1]: https://docs.kraken.com/rest/ + [2]: https://github.com/FasterXML/jackson + [3]: https://github.com/nyg/kraken-api-java/blob/v1.0.0/examples/src/main/java/dev/andstuff/kraken/example/Examples.java diff --git a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java b/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java similarity index 87% rename from examples/src/main/java/dev/andstuff/kraken/example/Examples.java rename to examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java index 5cd9855..3c65b41 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/Examples.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java @@ -1,14 +1,14 @@ package dev.andstuff.kraken.example; -import static dev.andstuff.kraken.example.helper.PropertiesHelper.readFromFile; +import static dev.andstuff.kraken.example.helper.CredentialsHelper.readFromFile; import java.util.List; import java.util.Map; -import java.util.Properties; import com.fasterxml.jackson.databind.JsonNode; import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.model.KrakenCredentials; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair; import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime; @@ -16,7 +16,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class Examples { +public class SimpleExamples { public static void main(String[] args) { @@ -50,8 +50,8 @@ public static void main(String[] args) { /* Private endpoint example */ - Properties apiKeys = readFromFile("/api-keys.properties"); - KrakenAPI api = new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret")); + KrakenCredentials credentials = readFromFile("/api-keys.properties"); + KrakenAPI api = new KrakenAPI(credentials); JsonNode balance = api.query(KrakenAPI.Private.BALANCE); log.info("{}", balance); diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java similarity index 52% rename from examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java rename to examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java index 9331693..0a5745f 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewardsSummary.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java @@ -1,44 +1,54 @@ -package dev.andstuff.kraken.example.reward; +package dev.andstuff.kraken.example; -import static dev.andstuff.kraken.example.helper.PropertiesHelper.readFromFile; -import static java.util.Arrays.asList; +import static dev.andstuff.kraken.example.helper.CredentialsHelper.readFromFile; +import static java.util.function.Predicate.not; import java.util.ArrayList; import java.util.List; -import java.util.Properties; import java.util.Set; import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.model.KrakenCredentials; import dev.andstuff.kraken.api.model.KrakenException; import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerInfoParams; import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerInfo; +import dev.andstuff.kraken.example.reward.AssetRates; +import dev.andstuff.kraken.example.reward.StakingRewards; import dev.andstuff.kraken.example.reward.csv.CsvLedgerEntries; import dev.andstuff.kraken.example.reward.csv.CsvYearlyAssetRewards; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +/** + * Generates a CSV file containing all the ledger entries corresponding to + * staking rewards, and another CSV file containing the summary of staking + * rewards earned each year for each asset. + */ @Slf4j @RequiredArgsConstructor -public class StakingRewardsSummary { +public class StakingRewardsSummaryExample { + + private static final int SLEEP_BETWEEN_API_CALLS = 2000; private final KrakenAPI api; public static void main(String[] args) { - Properties apiKeys = readFromFile("/api-keys.properties"); - new StakingRewardsSummary(new KrakenAPI(apiKeys.getProperty("key"), apiKeys.getProperty("secret"))).generate(); + KrakenCredentials credentials = readFromFile("/api-keys.properties"); + new StakingRewardsSummaryExample(new KrakenAPI(credentials)) + .generate("rewards.csv", "rewards-summary.csv"); } - public void generate() { - List rewards = fetchAllStakingRewards(); - RewardAggregate rewardAggregate = new RewardAggregate(rewards); - AssetRates rates = fetchRatesFor(rewardAggregate.getAssets()); + public void generate(String rewardsFileName, String rewardSummaryFileName) { + List rewards = fetchStakingRewards(); + StakingRewards stakingRewards = new StakingRewards(rewards); + AssetRates rates = fetchRatesFor(stakingRewards.getAssets()); - new CsvLedgerEntries(rewards).writeToFile("rewards.csv"); - new CsvYearlyAssetRewards(rewardAggregate, rates).writeToFile("rewards-summary.csv"); + new CsvLedgerEntries(rewards).writeToFile(rewardsFileName); + new CsvYearlyAssetRewards(stakingRewards, rates).writeToFile(rewardSummaryFileName); } - private List fetchAllStakingRewards() { + private List fetchStakingRewards() { List rewards = new ArrayList<>(); LedgerInfoParams params = LedgerInfoParams.builder() @@ -48,21 +58,19 @@ private List fetchAllStakingRewards() { boolean hasNext = true; while (hasNext) { - LedgerInfo ledgerInfo = api.ledgerInfo(params); - List ledgerEntries = ledgerInfo.asList(); - log.info("Fetched {} rewards", ledgerEntries.size()); + log.info("Fetched {} rewards", ledgerInfo.size()); params = params.withNextResultOffset(); hasNext = ledgerInfo.hasNext(); - rewards.addAll(ledgerEntries.stream().filter(StakingRewardsSummary::isStakingReward).toList()); + rewards.addAll(ledgerInfo.stakingRewards()); try { - Thread.sleep(2000); + Thread.sleep(SLEEP_BETWEEN_API_CALLS); } catch (InterruptedException e) { - log.warn("Thread has been interrupted"); + log.warn("Thread was interrupted"); Thread.currentThread().interrupt(); } } @@ -74,7 +82,7 @@ private AssetRates fetchRatesFor(Set assets) { try { List pairs = assets.stream() .map(asset -> asset + AssetRates.REFERENCE_ASSET) - .filter(pair -> !pair.equals("USD" + AssetRates.REFERENCE_ASSET)) + .filter(not(AssetRates.REFERENCE_PAIR::equals)) .toList(); return new AssetRates(api.ticker(pairs)); } @@ -82,8 +90,4 @@ private AssetRates fetchRatesFor(Set assets) { throw new RuntimeException("Couldn't fetch rates", e); } } - - private static boolean isStakingReward(LedgerEntry entry) { - return !asList("migration", "spottostaking").contains(entry.subType()); - } } diff --git a/examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java b/examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java similarity index 59% rename from examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java rename to examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java index 2affed3..b184822 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/helper/PropertiesHelper.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java @@ -4,18 +4,19 @@ import java.io.InputStream; import java.util.Properties; +import dev.andstuff.kraken.api.model.KrakenCredentials; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class PropertiesHelper { +public final class CredentialsHelper { - public static Properties readFromFile(String path) { + public static KrakenCredentials readFromFile(String path) { try { - InputStream stream = PropertiesHelper.class.getResourceAsStream(path); + InputStream stream = CredentialsHelper.class.getResourceAsStream(path); Properties properties = new Properties(); properties.load(stream); - return properties; + return new KrakenCredentials(properties.getProperty("key"), properties.getProperty("secret")); } catch (IOException e) { throw new RuntimeException(String.format("Could not read properties from file: %s", path)); diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java index d611eda..3f14506 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java @@ -14,22 +14,24 @@ public class AssetRates { public static final String REFERENCE_ASSET = "USD"; + public static final String REFERENCE_PAIR = REFERENCE_ASSET + REFERENCE_ASSET; + private static final int REFERENCE_ASSET_DECIMALS = 2; private final Map rates; public AssetRates(Map tickers) { - rates = tickers.entrySet().stream() - .collect(toMap(Map.Entry::getKey, this::extractRate)); - rates.put(REFERENCE_ASSET + REFERENCE_ASSET, BigDecimal.ONE); + rates = tickers.entrySet().stream().collect(toMap(Map.Entry::getKey, this::extractRate)); + rates.put(REFERENCE_PAIR, BigDecimal.ONE); } public BigDecimal evaluate(BigDecimal amount, String asset) { - String normalPair = asset + REFERENCE_ASSET; - String commodityPair = "X%sZ%s".formatted(asset, REFERENCE_ASSET); - BigDecimal rate = Optional.ofNullable(rates.get(normalPair)) - .orElseGet(() -> rates.getOrDefault(commodityPair, BigDecimal.ZERO)); - return rate.multiply(amount).setScale(REFERENCE_ASSET_DECIMALS, RoundingMode.HALF_UP); + String newFormatPair = asset + REFERENCE_ASSET; + String oldFormatPair = "X%sZ%s".formatted(asset, REFERENCE_ASSET); + return Optional.ofNullable(rates.get(newFormatPair)) + .orElseGet(() -> rates.getOrDefault(oldFormatPair, BigDecimal.ZERO)) + .multiply(amount) + .setScale(REFERENCE_ASSET_DECIMALS, RoundingMode.HALF_UP); } private BigDecimal extractRate(Map.Entry entry) { diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRewards.java new file mode 100644 index 0000000..7580e5b --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRewards.java @@ -0,0 +1,32 @@ +package dev.andstuff.kraken.example.reward; + +import static java.util.stream.Collectors.toMap; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import lombok.Getter; + +/** + * Aggregates asset rewards by year. + */ +@Getter +public class AssetRewards { + + private final String asset; + private final BigDecimal totalReward; + private final Map yearlyRewards; + + public AssetRewards(String asset, List rewards) { + this.asset = asset; + + this.totalReward = rewards.stream() + .map(LedgerEntry::netAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + this.yearlyRewards = rewards.stream() + .collect(toMap(LedgerEntry::year, LedgerEntry::netAmount, BigDecimal::add)); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java deleted file mode 100644 index 5f7f42b..0000000 --- a/examples/src/main/java/dev/andstuff/kraken/example/reward/RewardAggregate.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.andstuff.kraken.example.reward; - -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toMap; -import static java.util.stream.Collectors.toSet; - -import java.math.BigDecimal; -import java.time.ZoneId; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; -import lombok.Getter; - -@Getter -public class RewardAggregate { // YearlyStakingRewards - - private final Set assets; - private final Set years; - private final Set assetRewards; - - public RewardAggregate(List rewards) { - - assets = rewards.stream().map(LedgerEntry::underlyingAsset).collect(toSet()); - years = rewards.stream().map(RewardAggregate::extractYear).collect(toSet()); - - assetRewards = rewards.stream() - .collect(groupingBy(LedgerEntry::underlyingAsset)) - .entrySet().stream() - .map(entry -> new AssetRewards(entry.getKey(), entry.getValue())) - .collect(toSet()); - } - - @Getter - public static class AssetRewards { - - private final String asset; - private final BigDecimal totalReward; - private final Map yearlyRewards; - - public AssetRewards(String asset, List rewards) { - this.asset = asset; - - this.totalReward = rewards.stream() - .map(LedgerEntry::netAmount) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - this.yearlyRewards = rewards.stream() - .collect(toMap( - RewardAggregate::extractYear, - LedgerEntry::netAmount, - BigDecimal::add)); - } - } - - private static int extractYear(LedgerEntry ledgerEntry) { - return ledgerEntry.time().atZone(ZoneId.of("UTC")).getYear(); - } -} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewards.java new file mode 100644 index 0000000..bccebca --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/StakingRewards.java @@ -0,0 +1,32 @@ +package dev.andstuff.kraken.example.reward; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toSet; + +import java.util.List; +import java.util.Set; + +import dev.andstuff.kraken.api.model.endpoint.account.response.LedgerEntry; +import lombok.Getter; + +/** + * Aggregates staking rewards by asset. + */ +@Getter +public class StakingRewards { + + private final Set assets; + private final Set years; + private final Set assetRewards; + + public StakingRewards(List rewards) { + assets = rewards.stream().map(LedgerEntry::underlyingAsset).collect(toSet()); + years = rewards.stream().map(LedgerEntry::year).collect(toSet()); + + assetRewards = rewards.stream() + .collect(groupingBy(LedgerEntry::underlyingAsset)) + .entrySet().stream() + .map(entry -> new AssetRewards(entry.getKey(), entry.getValue())) + .collect(toSet()); + } +} diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java index 2d2ca11..2debdb6 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java @@ -16,29 +16,50 @@ import com.opencsv.CSVWriter; import dev.andstuff.kraken.example.reward.AssetRates; -import dev.andstuff.kraken.example.reward.RewardAggregate; +import dev.andstuff.kraken.example.reward.AssetRewards; +import dev.andstuff.kraken.example.reward.StakingRewards; public class CsvYearlyAssetRewards { - private final List headers; - private final Map> assetRewardRows; + private final String[] headers; + private final List assetRewardRows; - public CsvYearlyAssetRewards(RewardAggregate aggregate, AssetRates rates) { + public CsvYearlyAssetRewards(StakingRewards rewards, AssetRates rates) { + this.headers = buildHeaderRow(rewards.getYears()); + this.assetRewardRows = buildRewardRows(rewards, rates); + } + + public void writeToFile(String fileName) { + try (CSVWriter writer = new CSVWriter(new FileWriter(fileName))) { + writer.writeNext(headers); + writer.writeAll(assetRewardRows); + } catch (IOException e) { + throw new RuntimeException("Couldn't write reward summary to file", e); + } + } - // build header row - adding columns for fiat valuation - Set years = aggregate.getYears(); - headers = years.stream().flatMap(e -> Stream.of(e.toString(), "")).collect(toList()); + /** + * Build header row, adding columns for fiat valuation. + * + * @return an array of String: Asset, y1, _, y2, _, …, total, _ + */ + private static String[] buildHeaderRow(Set years) { + List headers = years.stream().flatMap(year -> Stream.of(year.toString(), "")).collect(toList()); headers.addFirst("Asset"); headers.addAll(List.of("Total", "")); + return headers.toArray(new String[0]); + } - // build asset reward rows with rates - Set assetRewards = aggregate.getAssetRewards(); - assetRewardRows = assetRewards.stream() + /** + * Build reward rows with fiat valuation. + */ + private static List buildRewardRows(StakingRewards rewards, AssetRates rates) { + return rewards.getAssetRewards().stream() .collect(toMap( - RewardAggregate.AssetRewards::getAsset, + AssetRewards::getAsset, assetReward -> { Map yearlyRewards = assetReward.getYearlyRewards(); - List yearlyRewardsWithRates = years.stream() + List yearlyRewardsWithRates = rewards.getYears().stream() .flatMap(year -> { BigDecimal reward = yearlyRewards.getOrDefault(year, BigDecimal.ZERO); BigDecimal fiatValue = rates.evaluate(reward, assetReward.getAsset()); @@ -46,28 +67,17 @@ public CsvYearlyAssetRewards(RewardAggregate aggregate, AssetRates rates) { }) .collect(toList()); BigDecimal totalReward = assetReward.getTotalReward(); - yearlyRewardsWithRates.addAll(List.of(totalReward, rates.evaluate(totalReward, assetReward.getAsset()))); + yearlyRewardsWithRates.addAll( + List.of(totalReward, rates.evaluate(totalReward, assetReward.getAsset()))); return yearlyRewardsWithRates; - })); - } - - public void writeToFile(String fileName) { - try (CSVWriter writer = new CSVWriter(new FileWriter(fileName))) { - writer.writeNext(headers.toArray(new String[0])); // Asset, y1, y2, y3, ..., total - - List array = assetRewardRows.entrySet().stream() - .map(entry -> { - List collect = entry.getValue().stream().map(BigDecimal::toPlainString).collect(toList()); - collect.addFirst(entry.getKey()); - return collect.toArray(new String[0]); - }) - .sorted(comparing(e -> new BigDecimal(e[e.length - 1]), reverseOrder())) - .toList(); - - writer.writeAll(array); // ETH, xxx, xxx, xxx - } - catch (IOException e) { - throw new RuntimeException(e); - } + })) + .entrySet().stream() + .map(entry -> { + List cells = entry.getValue().stream().map(BigDecimal::toPlainString).collect(toList()); + cells.addFirst(entry.getKey()); + return cells.toArray(new String[0]); + }) + .sorted(comparing(e -> new BigDecimal(e[e.length - 1]), reverseOrder())) + .toList(); } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java index 61790b2..881f50d 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import dev.andstuff.kraken.api.model.KrakenCredentials; import dev.andstuff.kraken.api.model.endpoint.account.LedgerEntriesEndpoint; import dev.andstuff.kraken.api.model.endpoint.account.LedgerInfoEndpoint; import dev.andstuff.kraken.api.model.endpoint.account.params.LedgerEntriesParams; @@ -36,6 +37,10 @@ public KrakenAPI() { this(new DefaultKrakenRestRequester()); } + public KrakenAPI(KrakenCredentials credentials) { + this(new DefaultKrakenRestRequester(credentials)); + } + public KrakenAPI(String key, String secret) { this(new DefaultKrakenRestRequester(key, secret)); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java index 2b84bc6..05edeac 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java @@ -19,8 +19,8 @@ public class LedgerInfoParams extends PostParams { private final Type assetType; private final Instant startDate; private final Instant endDate; - private final String startId; // TODO handle - private final String endId; // TODO naming + private final String startId; // FIXME handle + private final String endId; // FIXME naming private final boolean withoutCount; @With diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java index 717d7eb..fce9f90 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java @@ -2,12 +2,13 @@ import java.math.BigDecimal; import java.time.Instant; +import java.time.ZoneId; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.With; -public record LedgerEntry(@With String id, // TODO +public record LedgerEntry(@With String id, // FIXME @JsonProperty("refid") String referenceId, Instant time, Type type, @@ -18,12 +19,9 @@ public record LedgerEntry(@With String id, // TODO BigDecimal fee, BigDecimal balance) { - public BigDecimal netAmount() { - return amount.subtract(fee); - } - /** - * Attempts to extract the underlying asset, e.g. DOT.28S returns DOT, XXBT returns XBT, ZUSD returns USD. + * Attempts to extract the underlying asset, e.g. DOT.28S returns DOT, XXBT + * returns XBT, ZUSD returns USD. * * @return the underlying asset */ @@ -33,6 +31,18 @@ public String underlyingAsset() { : asset.split("[0-9.]")[0]; } + public BigDecimal netAmount() { + return amount.subtract(fee); + } + + public boolean isStakingReward() { + return type == Type.STAKING && (subType == null || subType.isEmpty()); + } + + public int year() { + return time.atZone(ZoneId.of("UTC")).getYear(); + } + public enum Type { NONE, TRADE, @@ -51,10 +61,10 @@ public enum Type { DIVIDEND, SALE, CONVERSION, - NFTTRADE, // TODO + NFTTRADE, // FIXME NFTCREATORFEE, NFTREBATE, CUSTODYTRANSFER, - // TODO not implemented + // FIXME not implemented } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java index 7f4d795..a0a93e8 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java @@ -8,13 +8,21 @@ public record LedgerInfo(@JsonProperty("ledger") Map entries, int count) { - public boolean hasNext() { - return entries.size() == 50; - } - public List asList() { return entries.entrySet().stream() .map(entry -> entry.getValue().withId(entry.getKey())) .toList(); } + + public List stakingRewards() { + return asList().stream().filter(LedgerEntry::isStakingReward).toList(); + } + + public boolean hasNext() { + return entries.size() == 50; + } + + public int size() { + return entries.size(); + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java index f652fb3..f10b075 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java +++ b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java @@ -45,6 +45,10 @@ public DefaultKrakenRestRequester() { this.credentials = null; } + public DefaultKrakenRestRequester(KrakenCredentials credentials) { + this.credentials = credentials; + } + public DefaultKrakenRestRequester(String key, String secret) { this.credentials = new KrakenCredentials(key, secret); } From 71c012927321cfe67c2c69835b26c1391b1e365a Mon Sep 17 00:00:00 2001 From: nyg Date: Wed, 27 Mar 2024 23:09:36 +0100 Subject: [PATCH 5/7] wip --- .../kraken/example/SimpleExamples.java | 3 +- .../dev/andstuff/kraken/api/KrakenAPI.java | 4 +-- .../kraken/api/model/KrakenResponse.java | 2 +- .../account/params/LedgerInfoParams.java | 30 +++++++++++++++---- .../account/response/LedgerEntry.java | 9 ++++-- .../endpoint/market/AssetPairEndpoint.java | 4 +-- .../market/params/AssetPairParams.java | 15 ++++++++-- .../endpoint/market/response/AssetInfo.java | 11 +++++++ .../endpoint/market/response/AssetPair.java | 21 ++++--------- .../endpoint/market/response/AssetStatus.java | 8 ----- .../market/response/SystemStatus.java | 7 ++++- .../api/rest/DefaultKrakenRestRequester.java | 1 + 12 files changed, 73 insertions(+), 42 deletions(-) delete mode 100644 library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetStatus.java diff --git a/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java b/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java index 3c65b41..dd2863f 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/SimpleExamples.java @@ -9,6 +9,7 @@ import dev.andstuff.kraken.api.KrakenAPI; import dev.andstuff.kraken.api.model.KrakenCredentials; +import dev.andstuff.kraken.api.model.endpoint.market.params.AssetPairParams; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair; import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime; @@ -39,7 +40,7 @@ public static void main(String[] args) { Map pairs1 = publicAPI.assetPairs(List.of("ETH/BTC", "ETH/USD")); log.info("{}", pairs1); - Map pairs2 = publicAPI.assetPairs(List.of("DOT/USD", "ADA/USD"), AssetPair.Info.MARGIN); + Map pairs2 = publicAPI.assetPairs(List.of("DOT/USD", "ADA/USD"), AssetPairParams.Info.MARGIN); log.info("{}", pairs2); JsonNode ticker = publicAPI.query(KrakenAPI.Public.TICKER, Map.of("pair", "XBTEUR")); diff --git a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java index 881f50d..5bcbec0 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -17,6 +17,7 @@ import dev.andstuff.kraken.api.model.endpoint.market.ServerTimeEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.SystemStatusEndpoint; import dev.andstuff.kraken.api.model.endpoint.market.TickerEndpoint; +import dev.andstuff.kraken.api.model.endpoint.market.params.AssetPairParams; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetInfo; import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair; import dev.andstuff.kraken.api.model.endpoint.market.response.ServerTime; @@ -59,7 +60,6 @@ public SystemStatus systemStatus() { return restRequester.execute(new SystemStatusEndpoint()); } - // TODO maybe return type Assets that can return both map and list public Map assetInfo(List assets) { return restRequester.execute(new AssetInfoEndpoint(assets)); } @@ -72,7 +72,7 @@ public Map assetPairs(List pairs) { return restRequester.execute(new AssetPairEndpoint(pairs)); } - public Map assetPairs(List pair, AssetPair.Info info) { + public Map assetPairs(List pair, AssetPairParams.Info info) { return restRequester.execute(new AssetPairEndpoint(pair, info)); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java index 7373f93..1308ba6 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/KrakenResponse.java @@ -9,7 +9,7 @@ public record KrakenResponse(List error, Optional result) { public Optional result() { - // FIXME looks like an issue with jackson which returns Optional.of(NullNode.instance) instead of Optional.empty + // TODO looks like an issue with jackson which returns Optional.of(NullNode.instance) instead of Optional.empty return result.map(res -> res.equals(NullNode.getInstance()) ? null : res); } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java index 05edeac..944bf7e 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; + import dev.andstuff.kraken.api.model.endpoint.priv.PostParams; import lombok.Builder; import lombok.Getter; @@ -17,10 +19,10 @@ public class LedgerInfoParams extends PostParams { private final List assets; private final String assetClass; private final Type assetType; - private final Instant startDate; - private final Instant endDate; - private final String startId; // FIXME handle - private final String endId; // FIXME naming + private final Instant fromDate; + private final Instant toDate; + private final String fromLedgerId; + private final String toLedgerId; private final boolean withoutCount; @With @@ -33,8 +35,21 @@ protected Map params() { putIfNonNull(params, "asset", assets, v -> String.join(",", v)); putIfNonNull(params, "aclass", assetClass); putIfNonNull(params, "type", assetType, e -> e.toString().toLowerCase()); - putIfNonNull(params, "start", startDate, d -> Long.toString(d.getEpochSecond())); - putIfNonNull(params, "end", endDate, d -> Long.toString(d.getEpochSecond())); + + if (fromDate != null) { + putIfNonNull(params, "start", fromDate, d -> Long.toString(d.getEpochSecond())); + } + else { + putIfNonNull(params, "start", fromLedgerId); + } + + if (toDate != null) { + putIfNonNull(params, "end", toDate, d -> Long.toString(d.getEpochSecond())); + } + else { + putIfNonNull(params, "end", toLedgerId); + } + putIfNonNull(params, "without_count", withoutCount); putIfNonNull(params, "ofs", resultOffset); return params; @@ -59,5 +74,8 @@ public enum Type { DIVIDEND, SALE, NFT_REBATE, + + @JsonEnumDefaultValue + UNKNOWN } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java index fce9f90..124516d 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java @@ -4,11 +4,12 @@ import java.time.Instant; import java.time.ZoneId; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.With; -public record LedgerEntry(@With String id, // FIXME +public record LedgerEntry(@With String id, // TODO see if jackson can set this value @JsonProperty("refid") String referenceId, Instant time, Type type, @@ -61,10 +62,12 @@ public enum Type { DIVIDEND, SALE, CONVERSION, - NFTTRADE, // FIXME + NFTTRADE, // TODO add underscore NFTCREATORFEE, NFTREBATE, CUSTODYTRANSFER, - // FIXME not implemented + + @JsonEnumDefaultValue + UNKNOWN } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetPairEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetPairEndpoint.java index 697180c..f892578 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetPairEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/AssetPairEndpoint.java @@ -12,10 +12,10 @@ public class AssetPairEndpoint extends PublicEndpoint> { public AssetPairEndpoint(List pairs) { - this(pairs, AssetPair.Info.ALL); + this(pairs, AssetPairParams.Info.ALL); } - public AssetPairEndpoint(List pairs, AssetPair.Info info) { + public AssetPairEndpoint(List pairs, AssetPairParams.Info info) { super("AssetPairs", new AssetPairParams(pairs, info), new TypeReference<>() {}); } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/AssetPairParams.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/AssetPairParams.java index c419c7a..8ffd6fc 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/AssetPairParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/params/AssetPairParams.java @@ -3,19 +3,30 @@ import java.util.List; import java.util.Map; -import dev.andstuff.kraken.api.model.endpoint.market.response.AssetPair; import dev.andstuff.kraken.api.model.endpoint.pub.QueryParams; +import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class AssetPairParams implements QueryParams { private final List pairs; - private final AssetPair.Info info; + private final Info info; public Map toMap() { return Map.of( "pair", String.join(",", pairs), "info", info.getValue()); } + + @Getter + @RequiredArgsConstructor + public enum Info { + ALL("info"), + LEVERAGE("leverage"), + FEES("fees"), + MARGIN("margin"); + + private final String value; + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetInfo.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetInfo.java index f2985f1..113f793 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetInfo.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetInfo.java @@ -2,6 +2,7 @@ import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; public record AssetInfo(@JsonProperty("aclass") String assetClass, @@ -10,4 +11,14 @@ public record AssetInfo(@JsonProperty("aclass") String assetClass, @JsonProperty("display_decimals") int displayedDecimals, @JsonProperty("collateral_value") BigDecimal collateralValue, AssetStatus status) { + + public enum AssetStatus { + ENABLED, + DEPOSIT_ONLY, + WITHDRAWAL_ONLY, + FUNDING_TEMPORARILY_DISABLED, + + @JsonEnumDefaultValue + UNKNOWN + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetPair.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetPair.java index 86144f1..d57e805 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetPair.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetPair.java @@ -3,12 +3,10 @@ import java.math.BigDecimal; import java.util.List; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - public record AssetPair(@JsonProperty("altname") String alternateName, @JsonProperty("wsname") String webSocketName, @JsonProperty("aclass_base") String baseAssetClass, @@ -36,23 +34,14 @@ public record AssetPair(@JsonProperty("altname") String alternateName, @JsonFormat(shape = JsonFormat.Shape.ARRAY) public record FeeSchedule(BigDecimal volume, BigDecimal percentage) {} - @Getter - @RequiredArgsConstructor - public enum Info { - - ALL("info"), - LEVERAGE("leverage"), - FEES("fees"), - MARGIN("margin"); - - private final String value; - } - public enum Status { ONLINE, CANCEL_ONLY, POST_ONLY, LIMIT_ONLY, - REDUCE_ONLY + REDUCE_ONLY, + + @JsonEnumDefaultValue + UNKNOWN } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetStatus.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetStatus.java deleted file mode 100644 index b382f5a..0000000 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/AssetStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.andstuff.kraken.api.model.endpoint.market.response; - -public enum AssetStatus { - ENABLED, - DEPOSIT_ONLY, - WITHDRAWAL_ONLY, - FUNDING_TEMPORARILY_DISABLED -} diff --git a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/SystemStatus.java b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/SystemStatus.java index 6978001..b4ec29e 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/SystemStatus.java +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/market/response/SystemStatus.java @@ -2,6 +2,8 @@ import java.time.Instant; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; + public record SystemStatus(Description status, Instant timestamp) { @@ -9,6 +11,9 @@ enum Description { ONLINE, MAINTENANCE, CANCEL_ONLY, - POST_ONLY + POST_ONLY, + + @JsonEnumDefaultValue + UNKNOWN } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java index f10b075..58ae65f 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java +++ b/library/src/main/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequester.java @@ -34,6 +34,7 @@ public class DefaultKrakenRestRequester implements KrakenRestRequester { static { OBJECT_MAPPER = JsonMapper.builder() .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .addModules(new JavaTimeModule(), new Jdk8Module()) .build(); From 0f945b3988c1bd5b0a2180282ab0a98fc8be25de Mon Sep 17 00:00:00 2001 From: nyg Date: Wed, 27 Mar 2024 23:33:56 +0100 Subject: [PATCH 6/7] wip --- .../example/StakingRewardsSummaryExample.java | 4 +- ...rds.java => CsvStakingRewardsSummary.java} | 38 ++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) rename examples/src/main/java/dev/andstuff/kraken/example/reward/csv/{CsvYearlyAssetRewards.java => CsvStakingRewardsSummary.java} (68%) diff --git a/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java index 0a5745f..ce8ba01 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java @@ -16,7 +16,7 @@ import dev.andstuff.kraken.example.reward.AssetRates; import dev.andstuff.kraken.example.reward.StakingRewards; import dev.andstuff.kraken.example.reward.csv.CsvLedgerEntries; -import dev.andstuff.kraken.example.reward.csv.CsvYearlyAssetRewards; +import dev.andstuff.kraken.example.reward.csv.CsvStakingRewardsSummary; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -45,7 +45,7 @@ public void generate(String rewardsFileName, String rewardSummaryFileName) { AssetRates rates = fetchRatesFor(stakingRewards.getAssets()); new CsvLedgerEntries(rewards).writeToFile(rewardsFileName); - new CsvYearlyAssetRewards(stakingRewards, rates).writeToFile(rewardSummaryFileName); + new CsvStakingRewardsSummary(stakingRewards, rates).writeToFile(rewardSummaryFileName); } private List fetchStakingRewards() { diff --git a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvStakingRewardsSummary.java similarity index 68% rename from examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java rename to examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvStakingRewardsSummary.java index 2debdb6..ca8066d 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvYearlyAssetRewards.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvStakingRewardsSummary.java @@ -19,21 +19,25 @@ import dev.andstuff.kraken.example.reward.AssetRewards; import dev.andstuff.kraken.example.reward.StakingRewards; -public class CsvYearlyAssetRewards { +public class CsvStakingRewardsSummary { - private final String[] headers; + private final String[] headerRow; private final List assetRewardRows; + private final String[] footerRow; - public CsvYearlyAssetRewards(StakingRewards rewards, AssetRates rates) { - this.headers = buildHeaderRow(rewards.getYears()); + public CsvStakingRewardsSummary(StakingRewards rewards, AssetRates rates) { + this.headerRow = buildHeaderRow(rewards.getYears()); this.assetRewardRows = buildRewardRows(rewards, rates); + this.footerRow = buildFooterRow(rewards, rates); } public void writeToFile(String fileName) { try (CSVWriter writer = new CSVWriter(new FileWriter(fileName))) { - writer.writeNext(headers); + writer.writeNext(headerRow); writer.writeAll(assetRewardRows); - } catch (IOException e) { + writer.writeNext(footerRow); + } + catch (IOException e) { throw new RuntimeException("Couldn't write reward summary to file", e); } } @@ -44,10 +48,12 @@ public void writeToFile(String fileName) { * @return an array of String: Asset, y1, _, y2, _, …, total, _ */ private static String[] buildHeaderRow(Set years) { - List headers = years.stream().flatMap(year -> Stream.of(year.toString(), "")).collect(toList()); - headers.addFirst("Asset"); - headers.addAll(List.of("Total", "")); - return headers.toArray(new String[0]); + List headerCells = years.stream() + .flatMap(year -> Stream.of(year.toString(), AssetRates.REFERENCE_ASSET)) + .collect(toList()); + headerCells.addFirst("Asset"); + headerCells.addAll(List.of("Total", AssetRates.REFERENCE_ASSET)); + return headerCells.toArray(new String[0]); } /** @@ -80,4 +86,16 @@ private static List buildRewardRows(StakingRewards rewards, AssetRates .sorted(comparing(e -> new BigDecimal(e[e.length - 1]), reverseOrder())) .toList(); } + + private String[] buildFooterRow(StakingRewards rewards, AssetRates rates) { + + BigDecimal totalFiatAmount = rewards.getAssetRewards().stream() + .map(reward -> rates.evaluate(reward.getTotalReward(), reward.getAsset())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + List footerCells = rewards.getYears().stream().flatMap(year -> Stream.of("", "")).collect(toList()); + footerCells.addFirst("Total"); + footerCells.addAll(List.of("", totalFiatAmount.toPlainString())); + return footerCells.toArray(new String[0]); + } } From aaa82b25ae14fcc21f6d436a08f926b6e16f99c9 Mon Sep 17 00:00:00 2001 From: nyg Date: Wed, 27 Mar 2024 23:41:39 +0100 Subject: [PATCH 7/7] wip --- .../andstuff/kraken/example/StakingRewardsSummaryExample.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java index ce8ba01..ea48cd9 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java @@ -59,12 +59,11 @@ private List fetchStakingRewards() { boolean hasNext = true; while (hasNext) { LedgerInfo ledgerInfo = api.ledgerInfo(params); - log.info("Fetched {} rewards", ledgerInfo.size()); - params = params.withNextResultOffset(); hasNext = ledgerInfo.hasNext(); rewards.addAll(ledgerInfo.stakingRewards()); + log.info("Fetched {} staking rewards", rewards.size()); try { Thread.sleep(SLEEP_BETWEEN_API_CALLS);