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> { + + 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..944bf7e --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/params/LedgerInfoParams.java @@ -0,0 +1,81 @@ +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 com.fasterxml.jackson.annotation.JsonEnumDefaultValue; + +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 fromDate; + private final Instant toDate; + private final String fromLedgerId; + private final String toLedgerId; + 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()); + + 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; + } + + 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, + + @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 new file mode 100644 index 0000000..124516d --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerEntry.java @@ -0,0 +1,73 @@ +package dev.andstuff.kraken.api.model.endpoint.account.response; + +import java.math.BigDecimal; +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, // TODO see if jackson can set this value + @JsonProperty("refid") String referenceId, + Instant time, + Type type, + @JsonProperty("subtype") String subType, + @JsonProperty("aclass") String assetClass, + String asset, + BigDecimal amount, + BigDecimal fee, + BigDecimal balance) { + + /** + * 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 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, + DEPOSIT, + WITHDRAWAL, + TRANSFER, + MARGIN, + ADJUSTMENT, + ROLLOVER, + SPEND, + RECEIVE, + SETTLED, + CREDIT, + STAKING, + REWARD, + DIVIDEND, + SALE, + CONVERSION, + NFTTRADE, // TODO add underscore + NFTCREATORFEE, + NFTREBATE, + CUSTODYTRANSFER, + + @JsonEnumDefaultValue + UNKNOWN + } +} 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..a0a93e8 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/model/endpoint/account/response/LedgerInfo.java @@ -0,0 +1,28 @@ +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) { + + 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/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/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/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/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/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/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) {} +} 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; 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..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(); @@ -45,6 +46,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); }