diff --git a/.gitignore b/.gitignore
index cce5593..07a44b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ target
*.iml
.idea
api-keys.properties
+*.txt
+*.csv
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/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/SimpleExamples.java
similarity index 82%
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 0d6f34f..dd2863f 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,15 @@
package dev.andstuff.kraken.example;
-import static dev.andstuff.kraken.example.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.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;
@@ -16,7 +17,7 @@
import lombok.extern.slf4j.Slf4j;
@Slf4j
-public class Examples {
+public class SimpleExamples {
public static void main(String[] args) {
@@ -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"));
@@ -50,8 +51,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/StakingRewardsSummaryExample.java b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java
new file mode 100644
index 0000000..ea48cd9
--- /dev/null
+++ b/examples/src/main/java/dev/andstuff/kraken/example/StakingRewardsSummaryExample.java
@@ -0,0 +1,92 @@
+package dev.andstuff.kraken.example;
+
+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.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.CsvStakingRewardsSummary;
+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 StakingRewardsSummaryExample {
+
+ private static final int SLEEP_BETWEEN_API_CALLS = 2000;
+
+ private final KrakenAPI api;
+
+ public static void main(String[] args) {
+ KrakenCredentials credentials = readFromFile("/api-keys.properties");
+ new StakingRewardsSummaryExample(new KrakenAPI(credentials))
+ .generate("rewards.csv", "rewards-summary.csv");
+ }
+
+ public void generate(String rewardsFileName, String rewardSummaryFileName) {
+ List rewards = fetchStakingRewards();
+ StakingRewards stakingRewards = new StakingRewards(rewards);
+ AssetRates rates = fetchRatesFor(stakingRewards.getAssets());
+
+ new CsvLedgerEntries(rewards).writeToFile(rewardsFileName);
+ new CsvStakingRewardsSummary(stakingRewards, rates).writeToFile(rewardSummaryFileName);
+ }
+
+ private List fetchStakingRewards() {
+
+ 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);
+ params = params.withNextResultOffset();
+ hasNext = ledgerInfo.hasNext();
+
+ rewards.addAll(ledgerInfo.stakingRewards());
+ log.info("Fetched {} staking rewards", rewards.size());
+
+ try {
+ Thread.sleep(SLEEP_BETWEEN_API_CALLS);
+ }
+ catch (InterruptedException e) {
+ log.warn("Thread was interrupted");
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ return rewards;
+ }
+
+ private AssetRates fetchRatesFor(Set assets) {
+ try {
+ List pairs = assets.stream()
+ .map(asset -> asset + AssetRates.REFERENCE_ASSET)
+ .filter(not(AssetRates.REFERENCE_PAIR::equals))
+ .toList();
+ return new AssetRates(api.ticker(pairs));
+ }
+ catch (KrakenException e) {
+ throw new RuntimeException("Couldn't fetch rates", e);
+ }
+ }
+}
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 2434fba..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.time.Instant;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
-import com.fasterxml.jackson.databind.JsonNode;
-
-import dev.andstuff.kraken.api.KrakenAPI;
-
-/**
- * TODO Group by year
- */
-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"));
-
- Map params = new HashMap<>();
- params.put("type", "staking");
- params.put("without_count", "true");
- params.put("ofs", "0");
-
- 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"));
-
- 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());
- }
-
- 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];
- }));
-
- String fileName = "rewards.txt";
- FileOutputStream fileOutputStream = new FileOutputStream(fileName);
- PrintStream printStream = new PrintStream(fileOutputStream);
- 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()))
- .toList();
-
- BigDecimal assetTotalRewardAmount = assetRewards.stream()
- .map(e -> new BigDecimal(e.findValue("amount").textValue())
- .subtract(new BigDecimal(e.findValue("fee").textValue())))
- .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.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()));
- }
-
- System.out.println();
- System.out.printf("Total USD: %s%n", totalRewardAmountUsd);
- }
-
- 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());
- }
- catch (Exception e) {
- System.err.printf("Couldn't fetch rate for %s%n", asset);
- return BigDecimal.ONE.negate();
- }
- }
-}
diff --git a/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java b/examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java
similarity index 54%
rename from examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java
rename to examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java
index fb44f12..b184822 100644
--- a/examples/src/main/java/dev/andstuff/kraken/example/PropertiesHelper.java
+++ b/examples/src/main/java/dev/andstuff/kraken/example/helper/CredentialsHelper.java
@@ -1,21 +1,22 @@
-package dev.andstuff.kraken.example;
+package dev.andstuff.kraken.example.helper;
import java.io.IOException;
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 = Examples.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/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/reward/AssetRates.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java
new file mode 100644
index 0000000..3f14506
--- /dev/null
+++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/AssetRates.java
@@ -0,0 +1,40 @@
+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";
+ 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_PAIR, BigDecimal.ONE);
+ }
+
+ public BigDecimal evaluate(BigDecimal amount, String asset) {
+ 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) {
+ return entry.getValue().lastTrade().price();
+ }
+}
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/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/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/CsvStakingRewardsSummary.java b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvStakingRewardsSummary.java
new file mode 100644
index 0000000..ca8066d
--- /dev/null
+++ b/examples/src/main/java/dev/andstuff/kraken/example/reward/csv/CsvStakingRewardsSummary.java
@@ -0,0 +1,101 @@
+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.AssetRewards;
+import dev.andstuff.kraken.example.reward.StakingRewards;
+
+public class CsvStakingRewardsSummary {
+
+ private final String[] headerRow;
+ private final List assetRewardRows;
+ private final String[] footerRow;
+
+ 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(headerRow);
+ writer.writeAll(assetRewardRows);
+ writer.writeNext(footerRow);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Couldn't write reward summary to file", e);
+ }
+ }
+
+ /**
+ * Build header row, adding columns for fiat valuation.
+ *
+ * @return an array of String: Asset, y1, _, y2, _, …, total, _
+ */
+ private static String[] buildHeaderRow(Set years) {
+ 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]);
+ }
+
+ /**
+ * Build reward rows with fiat valuation.
+ */
+ private static List buildRewardRows(StakingRewards rewards, AssetRates rates) {
+ return rewards.getAssetRewards().stream()
+ .collect(toMap(
+ AssetRewards::getAsset,
+ assetReward -> {
+ Map yearlyRewards = assetReward.getYearlyRewards();
+ List yearlyRewardsWithRates = rewards.getYears().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;
+ }))
+ .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();
+ }
+
+ 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]);
+ }
+}
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..5bcbec0 100644
--- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
+++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
@@ -5,14 +5,24 @@
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;
+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;
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;
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;
@@ -28,6 +38,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));
}
@@ -54,14 +68,28 @@ 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) {
+ public Map assetPairs(List pair, AssetPairParams.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) {
+ 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/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/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