From c2f77c16bf1708c4362e8c183b56c2d0ea775eff Mon Sep 17 00:00:00 2001 From: nyg Date: Mon, 7 Sep 2026 21:24:47 +0200 Subject: [PATCH 1/4] feat: complete typed Market Data coverage Refs #82 --- AGENTS.md | 2 +- README.md | 58 ++- .../kraken/example/MarketDataExample.java | 35 ++ library/pom.xml | 9 + .../dev/andstuff/kraken/api/KrakenAPI.java | 165 ++++++++ .../market/GroupedOrderBookEndpoint.java | 31 ++ .../market/Level3OrderBookEndpoint.java | 31 ++ .../market/MaintenanceScheduleEndpoint.java | 19 + .../api/endpoint/market/OhlcEndpoint.java | 31 ++ .../endpoint/market/OrderBookEndpoint.java | 33 ++ .../market/RecentSpreadsEndpoint.java | 31 ++ .../endpoint/market/RecentTradesEndpoint.java | 31 ++ .../market/params/GroupedOrderBookParams.java | 47 +++ .../market/params/Level3OrderBookParams.java | 39 ++ .../endpoint/market/params/OhlcParams.java | 59 +++ .../market/params/OrderBookParams.java | 53 +++ .../market/params/RecentSpreadsParams.java | 53 +++ .../market/params/RecentTradesParams.java | 59 +++ .../market/response/GroupedOrderBook.java | 25 ++ .../market/response/Level3OrderBook.java | 27 ++ .../market/response/MaintenanceSchedule.java | 75 ++++ .../endpoint/market/response/OhlcData.java | 61 +++ .../endpoint/market/response/OrderBook.java | 27 ++ .../market/response/RecentSpreads.java | 55 +++ .../market/response/RecentTrades.java | 79 ++++ .../api/endpoint/market/MarketDataTest.java | 382 ++++++++++++++++++ library/src/test/resources/market/README.md | 7 + library/src/test/resources/market/depth.json | 10 + .../test/resources/market/grouped-book.json | 9 + library/src/test/resources/market/level3.json | 14 + .../market/maintenance-schedule.json | 18 + library/src/test/resources/market/ohlc.json | 10 + library/src/test/resources/market/spread.json | 10 + library/src/test/resources/market/trades.json | 10 + pom.xml | 7 + 35 files changed, 1603 insertions(+), 9 deletions(-) create mode 100644 examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java create mode 100644 library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java create mode 100644 library/src/test/resources/market/README.md create mode 100644 library/src/test/resources/market/depth.json create mode 100644 library/src/test/resources/market/grouped-book.json create mode 100644 library/src/test/resources/market/level3.json create mode 100644 library/src/test/resources/market/maintenance-schedule.json create mode 100644 library/src/test/resources/market/ohlc.json create mode 100644 library/src/test/resources/market/spread.json create mode 100644 library/src/test/resources/market/trades.json diff --git a/AGENTS.md b/AGENTS.md index 4963f39..1ebe91f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ mvn -pl library package # build only the library module mvn -pl examples package # build only the examples module ``` -There are no tests in this project. CI runs `mvn clean package` on PRs targeting `master`. +The library's JUnit 5 tests use JSON fixtures and a fake `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`. Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `25`). diff --git a/README.md b/README.md index 1127e88..5b9a269 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,51 @@ JsonNode trades = api.queryPublic("Trades", Map.of("pair", "XBTUSD", "count", "1 // {"XXBTZUSD":[["68515.60000","0.00029628",1.7100231295628998E9,"s","m","",68007835]], … ``` +### Market Data + +All 12 Market Data endpoints in Kraken's current Spot REST specification have typed methods. In addition to `serverTime`, `systemStatus`, `assetInfo`, `assetPairs`, and `ticker`: + +| Endpoint | Typed method | Response | +|---|---|---| +| `OHLC` | `ohlc(pair)` / `ohlc(params)` | `OhlcData`: candles by pair and a `last` cursor | +| `Depth` | `orderBook(pair)` / `orderBook(params)` | `Map` | +| `GroupedBook` | `groupedOrderBook(pair)` / `groupedOrderBook(params)` | `GroupedOrderBook` | +| `Trades` | `recentTrades(pair)` / `recentTrades(params)` | `RecentTrades`: trades by pair and a `last` cursor | +| `Spread` | `recentSpreads(pair)` / `recentSpreads(params)` | `RecentSpreads`: spreads by pair and a `last` cursor | +| `Level3` (private) | `level3OrderBook(pair)` / `level3OrderBook(params)` | `Level3OrderBook` | +| `MaintenanceSchedule` | `maintenanceSchedule()` | `MaintenanceSchedule` | + +Use parameter builders to set optional fields; omitted fields retain Kraken's defaults: + +```java +OhlcData candles = api.ohlc(OhlcParams.builder() + .pair("BTC/USD").interval(60).assetVersion(1).build()); +List hourly = candles.candles().get("BTC/USD"); + +RecentTrades trades = api.recentTrades(RecentTradesParams.builder() + .pair("BTC/USD").count(10).build()); +RecentTrades nextBatch = api.recentTrades(RecentTradesParams.builder() + .pair("BTC/USD").since(trades.last()).count(10).build()); +``` + +`OHLC`, `Depth`, `Trades`, and `Spread` accept `assetVersion(1)` for display pair keys such as `BTC/USD`; without it, Kraken returns internal keys such as `XXBTZUSD`. Their `assetClass("tokenized_asset")` option supports xStocks. Response maps preserve the keys Kraken returns. + +OHLC includes a final candle that is still forming and retains at most 720 entries. Reuse its `last()` cursor as `since` to poll for committed updates. Trade cursors are opaque strings: pass `last()` unchanged. Prices and quantities use `BigDecimal`; trade times retain fractional Unix seconds, while Level3 timestamps are Unix nanoseconds. + +Level3 requires credentials with **Orders and trades – Query open orders & trades** permission: + +```java +KrakenAPI authenticated = new KrakenAPI("my key", "my secret"); +Level3OrderBook book = authenticated.level3OrderBook(Level3OrderBookParams.builder() + .pair("YFI/EUR").depth(10).build()); +``` + +Run the public examples without credentials (after `mvn clean install`): + +```sh +mvn -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.MarketDataExample +``` + ### Private endpoints Private endpoints can be queried in the same way as the public ones, but an API key and secret must be provided to the `KrakenAPI` instance: @@ -105,22 +150,19 @@ JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of( ### Custom endpoints -An endpoint the library doesn't implement can also be given a proper type, instead of falling back to `JsonNode`. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`: +You can also define typed endpoints outside the library. The following example demonstrates the same mechanism used by the built-in order book endpoint. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`: ```java -public class TradesEndpoint extends PublicEndpoint>> { +public class MyOrderBookEndpoint extends PublicEndpoint> { - public TradesEndpoint(String pair) { - super("Trades", () -> Map.of("pair", pair), new TypeReference<>() {}); + public MyOrderBookEndpoint(String pair) { + super("Depth", () -> Map.of("pair", pair), new TypeReference<>() {}); } } -record Trade(BigDecimal price, BigDecimal volume) {} - KrakenAPI api = new KrakenAPI(); -Map> trades = api.query(new TradesEndpoint("XBTUSD")); -// {XXBTZUSD=[Trade[price=68515.60000, volume=0.00029628]], … +Map books = api.query(new MyOrderBookEndpoint("XBTUSD")); ``` The endpoint is run through the same `KrakenRestRequester` as the built-in ones, and a `PrivateEndpoint` is signed with the credentials and nonce generator the `KrakenAPI` instance was built with. Querying one on an instance built without credentials throws an `IllegalStateException`. diff --git a/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java b/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java new file mode 100644 index 0000000..7c9ac75 --- /dev/null +++ b/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java @@ -0,0 +1,35 @@ +package dev.andstuff.kraken.example; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; +import dev.andstuff.kraken.api.endpoint.market.params.OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.RecentTradesParams; +import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MarketDataExample { + + static void main() { + KrakenAPI api = new KrakenAPI(); + String pair = "BTC/USD"; + + OhlcData candles = api.ohlc(OhlcParams.builder().pair(pair).interval(60).assetVersion(1).build()); + log.info("Hourly candles: {}", candles.candles().get(pair)); + // The final candle is still forming. Reuse candles.last() as since for committed updates. + + log.info("L2 order book: {}", api.orderBook(OrderBookParams.builder().pair(pair).count(10).build())); + log.info("Grouped book: {}", api.groupedOrderBook(GroupedOrderBookParams.builder() + .pair(pair).depth(10).grouping(1000).build())); + + RecentTrades trades = api.recentTrades(RecentTradesParams.builder().pair(pair).count(2).build()); + log.info("Recent trades: {}", trades.trades()); + log.info("Next trade cursor: {}", trades.last()); + // Pass trades.last() unchanged as since when requesting the next batch. + + log.info("Recent spreads: {}", api.recentSpreads(pair)); + log.info("Upcoming maintenance: {}", api.maintenanceSchedule()); + } +} diff --git a/library/pom.xml b/library/pom.xml index f44f4cd..226d85c 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -12,4 +12,13 @@ kraken-api Java Kraken API Client + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + 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 67cf598..58395c4 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -39,12 +39,32 @@ import dev.andstuff.kraken.api.endpoint.earn.response.EarnStrategies; import dev.andstuff.kraken.api.endpoint.market.AssetInfoEndpoint; import dev.andstuff.kraken.api.endpoint.market.AssetPairEndpoint; +import dev.andstuff.kraken.api.endpoint.market.GroupedOrderBookEndpoint; +import dev.andstuff.kraken.api.endpoint.market.Level3OrderBookEndpoint; +import dev.andstuff.kraken.api.endpoint.market.MaintenanceScheduleEndpoint; +import dev.andstuff.kraken.api.endpoint.market.OhlcEndpoint; +import dev.andstuff.kraken.api.endpoint.market.OrderBookEndpoint; +import dev.andstuff.kraken.api.endpoint.market.RecentSpreadsEndpoint; +import dev.andstuff.kraken.api.endpoint.market.RecentTradesEndpoint; import dev.andstuff.kraken.api.endpoint.market.ServerTimeEndpoint; import dev.andstuff.kraken.api.endpoint.market.SystemStatusEndpoint; import dev.andstuff.kraken.api.endpoint.market.TickerEndpoint; import dev.andstuff.kraken.api.endpoint.market.params.AssetPairParams; +import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.Level3OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; +import dev.andstuff.kraken.api.endpoint.market.params.OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.RecentSpreadsParams; +import dev.andstuff.kraken.api.endpoint.market.params.RecentTradesParams; import dev.andstuff.kraken.api.endpoint.market.response.AssetInfo; import dev.andstuff.kraken.api.endpoint.market.response.AssetPairs; +import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; +import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; +import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; import dev.andstuff.kraken.api.endpoint.market.response.ServerTime; import dev.andstuff.kraken.api.endpoint.market.response.SystemStatus; import dev.andstuff.kraken.api.endpoint.market.response.Ticker; @@ -242,6 +262,126 @@ public Map ticker(List pairs) { return query(new TickerEndpoint(pairs)); } + /** + * Queries the {@code OHLC} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the candles by returned pair name and the cursor for committed updates + * @throws KrakenException if Kraken returns an error + */ + public OhlcData ohlc(String pair) { + return query(new OhlcEndpoint(pair)); + } + + /** + * Queries the {@code OHLC} endpoint. + * + * @param params the request parameters + * @return the candles by returned pair name and the cursor for committed updates + * @throws KrakenException if Kraken returns an error + */ + public OhlcData ohlc(OhlcParams params) { + return query(new OhlcEndpoint(params)); + } + + /** + * Queries the {@code Depth} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the L2 order books by returned pair name + * @throws KrakenException if Kraken returns an error + */ + public Map orderBook(String pair) { + return query(new OrderBookEndpoint(pair)); + } + + /** + * Queries the {@code Depth} endpoint. + * + * @param params the request parameters + * @return the L2 order books by returned pair name + * @throws KrakenException if Kraken returns an error + */ + public Map orderBook(OrderBookParams params) { + return query(new OrderBookEndpoint(params)); + } + + /** + * Queries the {@code Trades} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the trades by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ + public RecentTrades recentTrades(String pair) { + return query(new RecentTradesEndpoint(pair)); + } + + /** + * Queries the {@code Trades} endpoint. + * + * @param params the request parameters + * @return the trades by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ + public RecentTrades recentTrades(RecentTradesParams params) { + return query(new RecentTradesEndpoint(params)); + } + + /** + * Queries the {@code Spread} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the spreads by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ + public RecentSpreads recentSpreads(String pair) { + return query(new RecentSpreadsEndpoint(pair)); + } + + /** + * Queries the {@code Spread} endpoint. + * + * @param params the request parameters + * @return the spreads by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ + public RecentSpreads recentSpreads(RecentSpreadsParams params) { + return query(new RecentSpreadsEndpoint(params)); + } + + /** + * Queries the {@code GroupedBook} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the grouped bids and asks, pair and grouping value + * @throws KrakenException if Kraken returns an error + */ + public GroupedOrderBook groupedOrderBook(String pair) { + return query(new GroupedOrderBookEndpoint(pair)); + } + + /** + * Queries the {@code GroupedBook} endpoint. + * + * @param params the request parameters + * @return the grouped bids and asks, pair and grouping value + * @throws KrakenException if Kraken returns an error + */ + public GroupedOrderBook groupedOrderBook(GroupedOrderBookParams params) { + return query(new GroupedOrderBookEndpoint(params)); + } + + /** + * Queries the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. + * + * @return the maintenance schedule + * @throws KrakenException if Kraken returns an error + */ + public MaintenanceSchedule maintenanceSchedule() { + return query(new MaintenanceScheduleEndpoint()); + } + /** * Queries the {@code PreTrade} endpoint, returning the aggregated order book of a currency pair, with at most ten price levels on each side. * @@ -277,6 +417,30 @@ public PostTrade postTrade(PostTradeParams params) { /* Implemented private endpoints */ + /** + * Queries the {@code Level3} endpoint using Kraken's default options. Requires the Orders and trades - Query open orders & trades API key permission. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the individual bid and ask orders with IDs and nanosecond timestamps + * @throws KrakenException if Kraken returns an error + * @throws IllegalStateException if credentials are missing + */ + public Level3OrderBook level3OrderBook(String pair) { + return query(new Level3OrderBookEndpoint(pair)); + } + + /** + * Queries the {@code Level3} endpoint. Requires the Orders and trades - Query open orders & trades API key permission. + * + * @param params the request parameters + * @return the individual bid and ask orders with IDs and nanosecond timestamps + * @throws KrakenException if Kraken returns an error + * @throws IllegalStateException if credentials are missing + */ + public Level3OrderBook level3OrderBook(Level3OrderBookParams params) { + return query(new Level3OrderBookEndpoint(params)); + } + /** * Queries the private {@code Ledgers} endpoint, returning at most 50 ledger entries per call. * @@ -597,6 +761,7 @@ public enum Public { ASSET_PAIRS("AssetPairs"), DEPTH("Depth"), GROUPED_BOOK("GroupedBook"), + MAINTENANCE_SCHEDULE("MaintenanceSchedule"), OHLC("OHLC"), POST_TRADE("PostTrade"), PRE_TRADE("PreTrade"), diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java new file mode 100644 index 0000000..14f4bbb --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code GroupedBook} endpoint, returning typed market data. + */ +public class GroupedOrderBookEndpoint extends PublicEndpoint { + + /** + * Creates the {@code GroupedBook} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public GroupedOrderBookEndpoint(String pair) { + this(GroupedOrderBookParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code GroupedBook} endpoint. + * + * @param params the request parameters + */ + public GroupedOrderBookEndpoint(GroupedOrderBookParams params) { + super("GroupedBook", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java new file mode 100644 index 0000000..735846b --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.Level3OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; + +/** + * The private {@code Level3} endpoint, requiring the Orders and trades - Query open orders & trades API key permission. + */ +public class Level3OrderBookEndpoint extends PrivateEndpoint { + + /** + * Creates the {@code Level3} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public Level3OrderBookEndpoint(String pair) { + this(Level3OrderBookParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code Level3} endpoint. + * + * @param params the request parameters + */ + public Level3OrderBookEndpoint(Level3OrderBookParams params) { + super("Level3", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java new file mode 100644 index 0000000..8ad4c43 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java @@ -0,0 +1,19 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code MaintenanceSchedule} endpoint, returning typed market data. + */ +public class MaintenanceScheduleEndpoint extends PublicEndpoint { + + /** + * Creates the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. + */ + public MaintenanceScheduleEndpoint() { + super("MaintenanceSchedule", new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java new file mode 100644 index 0000000..bafe8c2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; +import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code OHLC} endpoint, returning typed market data. + */ +public class OhlcEndpoint extends PublicEndpoint { + + /** + * Creates the {@code OHLC} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public OhlcEndpoint(String pair) { + this(OhlcParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code OHLC} endpoint. + * + * @param params the request parameters + */ + public OhlcEndpoint(OhlcParams params) { + super("OHLC", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java new file mode 100644 index 0000000..8dbe25b --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java @@ -0,0 +1,33 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code Depth} endpoint, returning typed market data. + */ +public class OrderBookEndpoint extends PublicEndpoint> { + + /** + * Creates the {@code Depth} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public OrderBookEndpoint(String pair) { + this(OrderBookParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code Depth} endpoint. + * + * @param params the request parameters + */ + public OrderBookEndpoint(OrderBookParams params) { + super("Depth", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java new file mode 100644 index 0000000..fe51109 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.RecentSpreadsParams; +import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code Spread} endpoint, returning typed market data. + */ +public class RecentSpreadsEndpoint extends PublicEndpoint { + + /** + * Creates the {@code Spread} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public RecentSpreadsEndpoint(String pair) { + this(RecentSpreadsParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code Spread} endpoint. + * + * @param params the request parameters + */ + public RecentSpreadsEndpoint(RecentSpreadsParams params) { + super("Spread", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java new file mode 100644 index 0000000..5aa4fa1 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java @@ -0,0 +1,31 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import com.fasterxml.jackson.core.type.TypeReference; + +import dev.andstuff.kraken.api.endpoint.market.params.RecentTradesParams; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; + +/** + * The public {@code Trades} endpoint, returning typed market data. + */ +public class RecentTradesEndpoint extends PublicEndpoint { + + /** + * Creates the {@code Trades} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ + public RecentTradesEndpoint(String pair) { + this(RecentTradesParams.builder().pair(pair).build()); + } + + /** + * Creates the {@code Trades} endpoint. + * + * @param params the request parameters + */ + public RecentTradesEndpoint(RecentTradesParams params) { + super("Trades", params, new TypeReference<>() {}); + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java new file mode 100644 index 0000000..dd84e48 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java @@ -0,0 +1,47 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import static dev.andstuff.kraken.api.endpoint.pub.QueryParams.putIfNonNull; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.pub.QueryParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code GroupedBook} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class GroupedOrderBookParams implements QueryParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Price levels per side: 10, 25, 100, 250 or 1000; defaults to 10. + */ + private final Integer depth; + + /** + * Ticks per price level: 1, 5, 10, 25, 50, 100, 250, 500 or 1000; defaults to 1. + */ + private final Integer grouping; + + /** + * {@inheritDoc} + */ + @Override + public Map toMap() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "depth", depth, String::valueOf); + putIfNonNull(params, "grouping", grouping, String::valueOf); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java new file mode 100644 index 0000000..569aa92 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java @@ -0,0 +1,39 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.priv.PostParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code Level3} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class Level3OrderBookParams extends PostParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Price levels per side: 0 (full book), 10, 25, 100, 250 or 1000; defaults to 100. + */ + private final Integer depth; + + /** + * {@inheritDoc} + */ + @Override + protected Map params() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "depth", depth, String::valueOf); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java new file mode 100644 index 0000000..602ea66 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java @@ -0,0 +1,59 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import static dev.andstuff.kraken.api.endpoint.pub.QueryParams.putIfNonNull; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.pub.QueryParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code OHLC} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class OhlcParams implements QueryParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Candle interval in minutes: 1, 5, 15, 30, 60, 240, 1440, 10080 or 21600; defaults to 1. + */ + private final Integer interval; + + /** + * Unix timestamp in seconds, or the last cursor from the previous OHLC response. + */ + private final Long since; + + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ + private final Integer assetVersion; + + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ + private final String assetClass; + + /** + * {@inheritDoc} + */ + @Override + public Map toMap() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "interval", interval, String::valueOf); + putIfNonNull(params, "since", since, String::valueOf); + putIfNonNull(params, "assetVersion", assetVersion, String::valueOf); + putIfNonNull(params, "asset_class", assetClass, v -> v); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java new file mode 100644 index 0000000..54903e2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java @@ -0,0 +1,53 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import static dev.andstuff.kraken.api.endpoint.pub.QueryParams.putIfNonNull; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.pub.QueryParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code Depth} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class OrderBookParams implements QueryParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Maximum entries per side, from 1 to 500; defaults to 100. + */ + private final Integer count; + + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ + private final Integer assetVersion; + + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ + private final String assetClass; + + /** + * {@inheritDoc} + */ + @Override + public Map toMap() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "count", count, String::valueOf); + putIfNonNull(params, "assetVersion", assetVersion, String::valueOf); + putIfNonNull(params, "asset_class", assetClass, v -> v); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java new file mode 100644 index 0000000..7fd5a64 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java @@ -0,0 +1,53 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import static dev.andstuff.kraken.api.endpoint.pub.QueryParams.putIfNonNull; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.pub.QueryParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code Spread} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class RecentSpreadsParams implements QueryParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Unix timestamp in seconds, or the last cursor from the previous Spread response. + */ + private final Long since; + + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ + private final Integer assetVersion; + + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ + private final String assetClass; + + /** + * {@inheritDoc} + */ + @Override + public Map toMap() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "since", since, String::valueOf); + putIfNonNull(params, "assetVersion", assetVersion, String::valueOf); + putIfNonNull(params, "asset_class", assetClass, v -> v); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java new file mode 100644 index 0000000..4d65a89 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java @@ -0,0 +1,59 @@ +package dev.andstuff.kraken.api.endpoint.market.params; + +import static dev.andstuff.kraken.api.endpoint.pub.QueryParams.putIfNonNull; + +import java.util.HashMap; +import java.util.Map; + +import dev.andstuff.kraken.api.endpoint.pub.QueryParams; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +/** + * The parameters of the {@code Trades} endpoint; unset options use Kraken's defaults. + */ +@Getter +@Builder(toBuilder = true) +public class RecentTradesParams implements QueryParams { + + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ + @NonNull + private final String pair; + + /** + * Timestamp or opaque last cursor from the previous Trades response, preserved verbatim. + */ + private final String since; + + /** + * Maximum number of trades, from 1 to 1000; defaults to 1000. + */ + private final Integer count; + + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ + private final Integer assetVersion; + + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ + private final String assetClass; + + /** + * {@inheritDoc} + */ + @Override + public Map toMap() { + Map params = new HashMap<>(); + params.put("pair", pair); + putIfNonNull(params, "since", since, v -> v); + putIfNonNull(params, "count", count, String::valueOf); + putIfNonNull(params, "assetVersion", assetVersion, String::valueOf); + putIfNonNull(params, "asset_class", assetClass, v -> v); + return params; + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java new file mode 100644 index 0000000..9ae4099 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java @@ -0,0 +1,25 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@code GroupedBook} response, with asks rounded up and bids down to grouped price levels. + * + * @param pair asset pair + * @param grouping ticks per price level used by Kraken + * @param bids aggregated bid levels + * @param asks aggregated ask levels + */ +public record GroupedOrderBook(String pair, int grouping, List bids, List asks) { + + /** + * A grouped price level returned by the {@code GroupedBook} endpoint. + * + * @param price grouped price + * @param quantity aggregated quantity at this price + */ + public record Level(BigDecimal price, @JsonProperty("qty") BigDecimal quantity) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java new file mode 100644 index 0000000..8477b81 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java @@ -0,0 +1,27 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@code Level3} response, containing individual orders in the book. + * + * @param pair asset pair + * @param bids bid orders in the order returned by Kraken + * @param asks ask orders in the order returned by Kraken + */ +public record Level3OrderBook(String pair, List bids, List asks) { + + /** + * An individual order returned by the {@code Level3} endpoint. + * + * @param price order price + * @param quantity order quantity + * @param orderId order identifier + * @param timestamp Unix timestamp in nanoseconds + */ + public record Order(BigDecimal price, @JsonProperty("qty") BigDecimal quantity, + @JsonProperty("order_id") String orderId, long timestamp) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java new file mode 100644 index 0000000..1c89fee --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java @@ -0,0 +1,75 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.time.Instant; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * The {@code MaintenanceSchedule} response, covering the next seven days. + * + * @param events scheduled events ordered by expected start time, empty when none are scheduled + */ +public record MaintenanceSchedule(List events) { + + /** + * A scheduled maintenance event returned by the {@code MaintenanceSchedule} endpoint. + * + * @param eventId stable event identifier + * @param title event title + * @param expectedStart expected start time + * @param expectedEnd expected end time + * @param timeToStart seconds until the start, evaluated when Kraken served the response + * @param phase approach phase + * @param affectedServices affected Kraken services + * @param orderSubmission guidance about submitting orders + * @param recommendedAction suggested client action + * @param cancelBefore cancellation deadline, absent when not scheduled + * @param sourceUrl link to the event on Kraken's status page + */ + public record Event(@JsonProperty("event_id") long eventId, + String title, + @JsonProperty("expected_start_utc") Instant expectedStart, + @JsonProperty("expected_end_utc") Instant expectedEnd, + @JsonProperty("time_to_start_s") long timeToStart, + Phase phase, + @JsonProperty("affected_services") List affectedServices, + @JsonProperty("order_submission") OrderSubmission orderSubmission, + @JsonProperty("recommended_action") RecommendedAction recommendedAction, + @JsonProperty("cancel_before_utc") Instant cancelBefore, + @JsonProperty("source_url") String sourceUrl) {} + + /** + * The approach phase in a {@code MaintenanceSchedule} event. + */ + public enum Phase { + ANNOUNCED, REMINDER_24H, APPROACHING_30M, IMMINENT_5M, FINAL_WARNING_30S, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * A Kraken service affected by a {@code MaintenanceSchedule} event. + */ + public enum Service { + SPOT_WS, SPOT_REST, SPOT_FIX, SPOT_TRADING, + FUTURES_WS, FUTURES_REST, FUTURES_FIX, FUTURES_TRADING, ALL, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * Order submission guidance in a {@code MaintenanceSchedule} event. + */ + public enum OrderSubmission { + ALLOWED, DISCOURAGED, BLOCKED, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * Suggested client action in a {@code MaintenanceSchedule} event. + */ + public enum RecommendedAction { + CONTINUE, REDUCE_ACTIVITY, CANCEL_OPEN_ORDERS, STOP_NEW_ORDERS, + @JsonEnumDefaultValue UNKNOWN + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java new file mode 100644 index 0000000..02f3b71 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java @@ -0,0 +1,61 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +/** + * The {@code OHLC} response containing OHLC candles, including the final, uncommitted candle. At most 720 entries are available. + * + * @param candles entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for committed candles + */ +@JsonDeserialize(builder = OhlcData.ResponseBuilder.class) +public record OhlcData(Map> candles, Long last) { + + /** + * A positional OHLC candle returned by the {@code OHLC} endpoint. + * + * @param time candle start, in Unix seconds + * @param open opening price + * @param high highest price + * @param low lowest price + * @param close closing price + * @param vwap volume weighted average price + * @param volume traded volume + * @param count number of trades + */ + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + @JsonPropertyOrder({"time", "open", "high", "low", "close", "vwap", "volume", "count"}) + public record Candle(long time, BigDecimal open, BigDecimal high, BigDecimal low, + BigDecimal close, BigDecimal vwap, BigDecimal volume, long count) {} + + @JsonPOJOBuilder(withPrefix = "") + static class ResponseBuilder { + + private final Map> entries = new LinkedHashMap<>(); + private Long last; + + @JsonProperty("last") + void last(Long value) { + last = value; + } + + @JsonAnySetter + void pair(String name, List values) { + entries.put(name, values); + } + + OhlcData build() { + return new OhlcData(entries, last); + } + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java new file mode 100644 index 0000000..f1614c2 --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java @@ -0,0 +1,27 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * A pair's aggregated L2 order book from the {@code Depth} endpoint. + * + * @param asks ask levels in the order returned by Kraken + * @param bids bid levels in the order returned by Kraken + */ +public record OrderBook(List asks, List bids) { + + /** + * An aggregated price level returned by the {@code Depth} endpoint. + * + * @param price price of this level + * @param volume aggregated volume at this price + * @param time Unix timestamp in seconds + */ + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + @JsonPropertyOrder({"price", "volume", "time"}) + public record Level(BigDecimal price, BigDecimal volume, long time) {} +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java new file mode 100644 index 0000000..4d960ef --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java @@ -0,0 +1,55 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +/** + * The {@code Spread} response containing recent top-of-book spreads; Kraken retains approximately 200 entries. + * + * @param spreads entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for new data + */ +@JsonDeserialize(builder = RecentSpreads.ResponseBuilder.class) +public record RecentSpreads(Map> spreads, Long last) { + + /** + * A positional top-of-book spread returned by the {@code Spread} endpoint. + * + * @param time Unix timestamp in seconds + * @param bid best bid price + * @param ask best ask price + */ + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + @JsonPropertyOrder({"time", "bid", "ask"}) + public record Spread(long time, BigDecimal bid, BigDecimal ask) {} + + @JsonPOJOBuilder(withPrefix = "") + static class ResponseBuilder { + + private final Map> entries = new LinkedHashMap<>(); + private Long last; + + @JsonProperty("last") + void last(Long value) { + last = value; + } + + @JsonAnySetter + void pair(String name, List values) { + entries.put(name, values); + } + + RecentSpreads build() { + return new RecentSpreads(entries, last); + } + } +} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java new file mode 100644 index 0000000..8a3205c --- /dev/null +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java @@ -0,0 +1,79 @@ +package dev.andstuff.kraken.api.endpoint.market.response; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +/** + * The {@code Trades} response containing recent trades; by default Kraken returns the last 1000 trades. + * + * @param trades entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for new data + */ +@JsonDeserialize(builder = RecentTrades.ResponseBuilder.class) +public record RecentTrades(Map> trades, String last) { + + /** + * A positional trade returned by the {@code Trades} endpoint. + * + * @param price trade price + * @param volume traded volume + * @param time Unix timestamp in seconds, retaining fractional precision + * @param side buy or sell + * @param orderType market or limit + * @param miscellaneous additional trade information + * @param tradeId trade identifier + */ + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + @JsonPropertyOrder({"price", "volume", "time", "side", "orderType", "miscellaneous", "tradeId"}) + public record Trade(BigDecimal price, BigDecimal volume, BigDecimal time, Side side, + OrderType orderType, String miscellaneous, long tradeId) {} + + /** + * The buy/sell code in a {@code Trades} row. + */ + public enum Side { + @JsonProperty("b") BUY, + @JsonProperty("s") SELL, + @JsonEnumDefaultValue UNKNOWN + } + + /** + * The market/limit code in a {@code Trades} row. + */ + public enum OrderType { + @JsonProperty("m") MARKET, + @JsonProperty("l") LIMIT, + @JsonEnumDefaultValue UNKNOWN + } + + @JsonPOJOBuilder(withPrefix = "") + static class ResponseBuilder { + + private final Map> entries = new LinkedHashMap<>(); + private String last; + + @JsonProperty("last") + void last(String value) { + last = value; + } + + @JsonAnySetter + void pair(String name, List values) { + entries.put(name, values); + } + + RecentTrades build() { + return new RecentTrades(entries, last); + } + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java new file mode 100644 index 0000000..25baac5 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java @@ -0,0 +1,382 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.Endpoint; +import dev.andstuff.kraken.api.endpoint.KrakenException; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.Level3OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; +import dev.andstuff.kraken.api.endpoint.market.params.OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.params.RecentSpreadsParams; +import dev.andstuff.kraken.api.endpoint.market.params.RecentTradesParams; +import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; +import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; +import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; +import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; +import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; +import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +import dev.andstuff.kraken.api.rest.KrakenCredentials; +import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; +import dev.andstuff.kraken.api.rest.KrakenRestRequester; + +class MarketDataTest { + + // Match DefaultKrakenRestRequester: bind directly from JSON, not an intermediate tree. + private static final ObjectMapper 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(); + + private static final String PAIR = "BTC/USD"; + + @ParameterizedTest + @MethodSource("publicRequests") + void encodesEveryDocumentedPublicParameter(PublicEndpoint endpoint, String path, Map expected) { + assertEquals("GET", endpoint.getHttpMethod()); + assertEquals("https", endpoint.buildURL().getProtocol()); + assertEquals("api.kraken.com", endpoint.buildURL().getHost()); + assertEquals("/0/public/" + path, endpoint.buildURL().getPath()); + assertEquals(expected, decode(endpoint.buildURL().getQuery())); + assertTrue(Arrays.stream(KrakenAPI.Public.values()).anyMatch(value -> value.getPath().equals(path))); + } + + static Stream publicRequests() { + return Stream.of( + Arguments.of(new OhlcEndpoint(OhlcParams.builder().pair(PAIR).interval(60).since(1688671200L) + .assetVersion(1).assetClass("tokenized_asset").build()), "OHLC", + Map.of("pair", PAIR, "interval", "60", "since", "1688671200", "assetVersion", "1", "asset_class", "tokenized_asset")), + Arguments.of(new OrderBookEndpoint(OrderBookParams.builder().pair(PAIR).count(500) + .assetVersion(1).assetClass("tokenized_asset").build()), "Depth", + Map.of("pair", PAIR, "count", "500", "assetVersion", "1", "asset_class", "tokenized_asset")), + Arguments.of(new RecentTradesEndpoint(RecentTradesParams.builder().pair(PAIR).since("1688671969993150842").count(2) + .assetVersion(1).assetClass("tokenized_asset").build()), "Trades", + Map.of("pair", PAIR, "since", "1688671969993150842", "count", "2", "assetVersion", "1", "asset_class", "tokenized_asset")), + Arguments.of(new RecentSpreadsEndpoint(RecentSpreadsParams.builder().pair(PAIR).since(1688672106L) + .assetVersion(1).assetClass("tokenized_asset").build()), "Spread", + Map.of("pair", PAIR, "since", "1688672106", "assetVersion", "1", "asset_class", "tokenized_asset")), + Arguments.of(new GroupedOrderBookEndpoint(GroupedOrderBookParams.builder().pair(PAIR).depth(25).grouping(1000).build()), "GroupedBook", + Map.of("pair", PAIR, "depth", "25", "grouping", "1000")), + Arguments.of(new OhlcEndpoint(PAIR), "OHLC", Map.of("pair", PAIR)), + Arguments.of(new OrderBookEndpoint(PAIR), "Depth", Map.of("pair", PAIR)), + Arguments.of(new RecentTradesEndpoint(PAIR), "Trades", Map.of("pair", PAIR)), + Arguments.of(new RecentSpreadsEndpoint(PAIR), "Spread", Map.of("pair", PAIR)), + Arguments.of(new GroupedOrderBookEndpoint(PAIR), "GroupedBook", Map.of("pair", PAIR)), + Arguments.of(new MaintenanceScheduleEndpoint(), "MaintenanceSchedule", Map.of())); + } + + @Test + void encodesLevel3WithNonceAndFullBookDepth() { + var endpoint = new Level3OrderBookEndpoint(Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build()); + assertEquals("POST", endpoint.getHttpMethod()); + assertEquals("https://api.kraken.com/0/private/Level3", endpoint.buildURL().toString()); + assertEquals(KrakenAPI.Private.LEVEL3.getPath(), endpoint.getPath()); + assertEquals(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "123"), decode(endpoint.encodedParamsWith("123"))); + assertEquals("124", decode(endpoint.encodedParamsWith("124")).get("nonce")); + assertEquals(Map.of("pair", PAIR, "nonce", "123"), decode(new Level3OrderBookEndpoint(PAIR).encodedParamsWith("123"))); + } + + @Test + void requiresThePairForEachParameterizedEndpoint() { + assertThrows(NullPointerException.class, () -> OhlcParams.builder().build()); + assertThrows(NullPointerException.class, () -> OrderBookParams.builder().build()); + assertThrows(NullPointerException.class, () -> RecentTradesParams.builder().build()); + assertThrows(NullPointerException.class, () -> RecentSpreadsParams.builder().build()); + assertThrows(NullPointerException.class, () -> GroupedOrderBookParams.builder().build()); + assertThrows(NullPointerException.class, () -> Level3OrderBookParams.builder().build()); + } + + @Test + void readsAllCandlePositionsAndReusesTheCommittedCursor() { + OhlcData data = read(new OhlcEndpoint(PAIR), fixture("ohlc")); + assertEquals(new OhlcData.Candle(1688671200, decimal("30306.1"), decimal("30306.2"), decimal("30305.7"), + decimal("30305.7"), decimal("30306.1"), decimal("3.39243896"), 23), data.candles().get("XXBTZUSD").getFirst()); + assertEquals(2, data.candles().get("XXBTZUSD").size()); + assertEquals(1688671260L, data.candles().get("XXBTZUSD").getLast().time()); + assertEquals(1688672160L, data.last()); + assertEquals("1688672160", OhlcParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); + } + + @Test + void readsDepthLevelsAndIgnoresUnknownBookProperties() { + Map books = read(new OrderBookEndpoint(PAIR), fixture("depth")); + OrderBook book = books.get("XXBTZUSD"); + assertEquals(new OrderBook.Level(decimal("30384.10000"), decimal("2.059"), 1688671659), book.asks().getFirst()); + assertEquals(new OrderBook.Level(decimal("30297.00000"), decimal("1.115"), 1688671636), book.bids().getFirst()); + assertEquals(2, book.asks().size()); + assertEquals(2, book.bids().size()); + } + + @Test + void readsTradeCodesAndPreservesDecimalTimeAndOpaqueCursor() { + RecentTrades data = read(new RecentTradesEndpoint(PAIR), fixture("trades")); + assertEquals(new RecentTrades.Trade(decimal("30243.40000"), decimal("0.34507674"), decimal("1688669597.8277369"), + RecentTrades.Side.BUY, RecentTrades.OrderType.MARKET, "", 61044952), data.trades().get("XXBTZUSD").getFirst()); + assertEquals(RecentTrades.Side.SELL, data.trades().get("XXBTZUSD").getLast().side()); + assertEquals(RecentTrades.OrderType.LIMIT, data.trades().get("XXBTZUSD").getLast().orderType()); + assertEquals("1688671969993150842", data.last()); + assertEquals(data.last(), RecentTradesParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); + } + + @Test + void preservesSubMicrosecondTradePrecisionAndLargeIdentifiers() { + String json = fixture("trades").replace("1688669597.8277369", "1688669597.123456789") + .replace("61044952", "61044952000000001"); + var trade = read(new RecentTradesEndpoint(PAIR), json).trades().get("XXBTZUSD").getFirst(); + assertEquals(decimal("1688669597.123456789"), trade.time()); + assertEquals(61044952000000001L, trade.tradeId()); + } + + @Test + void toleratesFutureTradeCodes() { + String json = fixture("trades").replace("\"b\"", "\"future-side\"").replace("\"m\"", "\"future-type\""); + var trade = read(new RecentTradesEndpoint(PAIR), json).trades().get("XXBTZUSD").getFirst(); + assertEquals(RecentTrades.Side.UNKNOWN, trade.side()); + assertEquals(RecentTrades.OrderType.UNKNOWN, trade.orderType()); + } + + @Test + void readsSpreadsAndReusesTheCursor() { + RecentSpreads data = read(new RecentSpreadsEndpoint(PAIR), fixture("spread")); + assertEquals(new RecentSpreads.Spread(1688671834, decimal("30292.10000"), decimal("30297.50000")), + data.spreads().get("XXBTZUSD").getFirst()); + assertEquals(1688672106L, data.last()); + assertEquals("1688672106", RecentSpreadsParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); + } + + @Test + void acceptsDisplayPairKeysForAllLegacyMarketResponses() { + assertTrue(read(new OhlcEndpoint(PAIR), fixture("ohlc").replace("XXBTZUSD", PAIR)).candles().containsKey(PAIR)); + assertTrue(read(new OrderBookEndpoint(PAIR), fixture("depth").replace("XXBTZUSD", PAIR)).containsKey(PAIR)); + assertTrue(read(new RecentTradesEndpoint(PAIR), fixture("trades").replace("XXBTZUSD", PAIR)).trades().containsKey(PAIR)); + assertTrue(read(new RecentSpreadsEndpoint(PAIR), fixture("spread").replace("XXBTZUSD", PAIR)).spreads().containsKey(PAIR)); + } + + @Test + void separatesCursorFromMultiplePairKeysRegardlessOfPropertyOrder() { + String json = """ + {"error":[],"result":{"last":123,"BTC/USD":[],"ETH/USD":[]}} + """; + assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), read(new OhlcEndpoint(PAIR), json).candles()); + assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), read(new RecentSpreadsEndpoint(PAIR), json).spreads()); + var trades = read(new RecentTradesEndpoint(PAIR), json.replace(":123", ":\"001234567890123456789\"")); + assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), trades.trades()); + assertEquals("001234567890123456789", trades.last()); + } + + @Test + void readsGroupedQuantitiesAndGrouping() { + GroupedOrderBook book = read(new GroupedOrderBookEndpoint(PAIR), fixture("grouped-book")); + assertEquals(PAIR, book.pair()); + assertEquals(1000, book.grouping()); + assertEquals(new GroupedOrderBook.Level(decimal("90400.00000"), decimal("19.83057746")), book.bids().getFirst()); + assertEquals(new GroupedOrderBook.Level(decimal("90500.00000"), decimal("38.96185061")), book.asks().getFirst()); + } + + @Test + void readsIndividualLevel3OrdersAndNanosecondTimestamps() { + Level3OrderBook book = read(new Level3OrderBookEndpoint("YFI/EUR"), fixture("level3")); + assertEquals("YFI/EUR", book.pair()); + assertEquals(new Level3OrderBook.Order(decimal("3062.00000"), decimal("0.29665800"), "O5KJU4-IEQTM-NDMS6W", 1765622008594292000L), + book.bids().getFirst()); + assertEquals(2, book.bids().size()); + assertNotEquals(book.bids().getFirst().orderId(), book.bids().getLast().orderId()); + assertEquals(decimal("0.00278335"), book.asks().getFirst().quantity()); + assertEquals(1765622021013826600L, book.asks().getLast().timestamp()); + } + + @Test + void readsEveryMaintenanceEventField() { + var event = read(new MaintenanceScheduleEndpoint(), fixture("maintenance-schedule")).events().getFirst(); + assertEquals(new MaintenanceSchedule.Event(21, "Scheduled Maintenance - Website", + Instant.parse("2026-05-11T09:00:00Z"), Instant.parse("2026-05-11T10:00:00Z"), 1740, + MaintenanceSchedule.Phase.APPROACHING_30M, List.of(MaintenanceSchedule.Service.SPOT_TRADING), + MaintenanceSchedule.OrderSubmission.ALLOWED, MaintenanceSchedule.RecommendedAction.REDUCE_ACTIVITY, + Instant.parse("2026-05-11T08:55:00Z"), "https://status.kraken.com/incidents/b7k2r9wqmn41"), event); + } + + @Test + void toleratesFutureMaintenanceValuesAndAnAbsentCancellationDeadline() { + String json = fixture("maintenance-schedule").replace("approaching_30m", "future-phase") + .replace("spot_trading", "future-service").replace("allowed", "future-guidance") + .replace("reduce_activity", "future-action") + .replace("\"cancel_before_utc\": \"2026-05-11T08:55:00Z\",", "\"future_field\": true,"); + var event = read(new MaintenanceScheduleEndpoint(), json).events().getFirst(); + assertEquals(MaintenanceSchedule.Phase.UNKNOWN, event.phase()); + assertEquals(List.of(MaintenanceSchedule.Service.UNKNOWN), event.affectedServices()); + assertEquals(MaintenanceSchedule.OrderSubmission.UNKNOWN, event.orderSubmission()); + assertEquals(MaintenanceSchedule.RecommendedAction.UNKNOWN, event.recommendedAction()); + assertNull(event.cancelBefore()); + } + + @Test + void handlesEmptyBooksSchedulesAndTimeSeries() { + assertTrue(read(new MaintenanceScheduleEndpoint(), "{\"error\":[],\"result\":{\"events\":[]}}").events().isEmpty()); + assertTrue(read(new OrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"BTC/USD\":{\"asks\":[],\"bids\":[]}}}").get(PAIR).bids().isEmpty()); + assertTrue(read(new GroupedOrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"pair\":\"BTC/USD\",\"grouping\":1,\"asks\":[],\"bids\":[]}}").asks().isEmpty()); + assertTrue(read(new Level3OrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"pair\":\"BTC/USD\",\"asks\":[],\"bids\":[]}}").bids().isEmpty()); + String json = "{\"error\":[],\"result\":{\"BTC/USD\":[],\"last\":123}}"; + assertTrue(read(new OhlcEndpoint(PAIR), json).candles().get(PAIR).isEmpty()); + assertTrue(read(new RecentTradesEndpoint(PAIR), json).trades().get(PAIR).isEmpty()); + assertTrue(read(new RecentSpreadsEndpoint(PAIR), json).spreads().get(PAIR).isEmpty()); + } + + @ParameterizedTest + @MethodSource("publicFacadeCalls") + void facadeRoutesEveryPublicOverloadThroughTheConfiguredRequester(String path, String fixture, Class type, Function call) { + var requester = new FixtureRequester(fixture(fixture)); + Object result = call.apply(new KrakenAPI(null, requester)); + assertInstanceOf(type, result); + assertEquals(path, requester.endpoint.getPath()); + assertInstanceOf(PublicEndpoint.class, requester.endpoint); + } + + static Stream publicFacadeCalls() { + return Stream.of( + facade("OHLC", "ohlc", OhlcData.class, api -> api.ohlc(PAIR)), + facade("OHLC", "ohlc", OhlcData.class, api -> api.ohlc(OhlcParams.builder().pair(PAIR).build())), + facade("Depth", "depth", Map.class, api -> api.orderBook(PAIR)), + facade("Depth", "depth", Map.class, api -> api.orderBook(OrderBookParams.builder().pair(PAIR).build())), + facade("Trades", "trades", RecentTrades.class, api -> api.recentTrades(PAIR)), + facade("Trades", "trades", RecentTrades.class, api -> api.recentTrades(RecentTradesParams.builder().pair(PAIR).build())), + facade("Spread", "spread", RecentSpreads.class, api -> api.recentSpreads(PAIR)), + facade("Spread", "spread", RecentSpreads.class, api -> api.recentSpreads(RecentSpreadsParams.builder().pair(PAIR).build())), + facade("GroupedBook", "grouped-book", GroupedOrderBook.class, api -> api.groupedOrderBook(PAIR)), + facade("GroupedBook", "grouped-book", GroupedOrderBook.class, api -> api.groupedOrderBook(GroupedOrderBookParams.builder().pair(PAIR).build())), + facade("MaintenanceSchedule", "maintenance-schedule", MaintenanceSchedule.class, KrakenAPI::maintenanceSchedule)); + } + + @Test + void level3FacadePassesConfiguredCredentialsAndNonceGenerator() { + var requester = new FixtureRequester(fixture("level3")); + var credentials = new KrakenCredentials("test-key", "c2VjcmV0"); + KrakenNonceGenerator nonce = () -> "987654321"; + KrakenAPI api = new KrakenAPI(credentials, nonce, requester); + assertEquals("YFI/EUR", api.level3OrderBook("YFI/EUR").pair()); + assertSame(credentials, requester.credentials); + assertSame(nonce, requester.nonceGenerator); + assertEquals(Map.of("pair", "YFI/EUR", "nonce", "987654321"), decode(requester.postBody)); + api.level3OrderBook(Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build()); + assertEquals(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "987654321"), decode(requester.postBody)); + assertEquals("Level3", requester.endpoint.getPath()); + } + + @Test + void rejectsLevel3WithoutCredentialsBeforeCallingTheRequester() { + var requester = new FixtureRequester(fixture("level3")); + KrakenAPI api = new KrakenAPI(null, requester); + assertTrue(assertThrows(IllegalStateException.class, () -> api.level3OrderBook(PAIR)).getMessage().contains("Level3")); + assertThrows(IllegalStateException.class, () -> api.level3OrderBook(Level3OrderBookParams.builder().pair(PAIR).build())); + assertNull(requester.endpoint); + } + + @Test + void preservesKrakenErrors() { + var requester = new FixtureRequester("{\"error\":[\"EQuery:Unknown asset pair\"]}"); + var api = new KrakenAPI(null, requester); + var error = assertThrows(KrakenException.class, () -> api.ohlc(PAIR)); + assertEquals(List.of("EQuery:Unknown asset pair"), error.getErrors()); + } + + private static Arguments facade(String path, String fixture, Class type, Function call) { + return Arguments.of(path, fixture, type, call); + } + + private static BigDecimal decimal(String value) { + return new BigDecimal(value); + } + + private static Map decode(String encoded) { + if (encoded == null || encoded.isEmpty()) return Map.of(); + return Arrays.stream(encoded.split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + } + + private static String fixture(String name) { + try (InputStream input = MarketDataTest.class.getResourceAsStream("/market/" + name + ".json")) { + assertNotNull(input, "Missing fixture " + name); + return new String(input.readAllBytes(), StandardCharsets.UTF_8); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + private static T read(Endpoint endpoint, String json) { + try { + KrakenResponse response = MAPPER.readValue(json, endpoint.wrappedResponseType(MAPPER.getTypeFactory())); + return response.result().orElseThrow(() -> new KrakenException(response.error())); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + private static final class FixtureRequester implements KrakenRestRequester { + private final String json; + private Endpoint endpoint; + private KrakenCredentials credentials; + private KrakenNonceGenerator nonceGenerator; + private String postBody; + + private FixtureRequester(String json) { + this.json = json; + } + + @Override + public T execute(PublicEndpoint endpoint) { + this.endpoint = endpoint; + return read(endpoint, json); + } + + @Override + public T execute(PrivateEndpoint endpoint, KrakenCredentials credentials, KrakenNonceGenerator nonceGenerator) { + this.endpoint = endpoint; + this.credentials = credentials; + this.nonceGenerator = nonceGenerator; + this.postBody = endpoint.encodedParamsWith(nonceGenerator.generate()); + return read(endpoint, json); + } + } +} diff --git a/library/src/test/resources/market/README.md b/library/src/test/resources/market/README.md new file mode 100644 index 0000000..c0d478d --- /dev/null +++ b/library/src/test/resources/market/README.md @@ -0,0 +1,7 @@ +Fixtures are representative excerpts from the response examples in Kraken's +[Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), +retrieved 2026-09-07. Order books and time series are shortened. `depth.json` +includes an extra property to check forward-compatible deserialization. + +Tests also generate empty responses, display-name keys, future enum values, +and extra precision cases from these fixtures. No test calls the Kraken API. diff --git a/library/src/test/resources/market/depth.json b/library/src/test/resources/market/depth.json new file mode 100644 index 0000000..ca0d556 --- /dev/null +++ b/library/src/test/resources/market/depth.json @@ -0,0 +1,10 @@ +{ + "error": [], + "result": { + "XXBTZUSD": { + "asks": [["30384.10000", "2.059", 1688671659], ["30387.90000", "1.500", 1688671380]], + "bids": [["30297.00000", "1.115", 1688671636], ["30296.70000", "2.002", 1688671674]], + "future_field": true + } + } +} diff --git a/library/src/test/resources/market/grouped-book.json b/library/src/test/resources/market/grouped-book.json new file mode 100644 index 0000000..d3a5860 --- /dev/null +++ b/library/src/test/resources/market/grouped-book.json @@ -0,0 +1,9 @@ +{ + "error": [], + "result": { + "pair": "BTC/USD", + "grouping": 1000, + "bids": [{"price": "90400.00000", "qty": "19.83057746"}, {"price": "90300.00000", "qty": "45.35073006"}], + "asks": [{"price": "90500.00000", "qty": "38.96185061"}, {"price": "90600.00000", "qty": "55.96402032"}] + } +} diff --git a/library/src/test/resources/market/level3.json b/library/src/test/resources/market/level3.json new file mode 100644 index 0000000..0381bbc --- /dev/null +++ b/library/src/test/resources/market/level3.json @@ -0,0 +1,14 @@ +{ + "error": [], + "result": { + "pair": "YFI/EUR", + "bids": [ + {"price": "3062.00000", "qty": "0.29665800", "order_id": "O5KJU4-IEQTM-NDMS6W", "timestamp": 1765622008594292000}, + {"price": "3062.00000", "qty": "0.13917400", "order_id": "OERRY6-MXYER-6EQKNY", "timestamp": 1765622011396903000} + ], + "asks": [ + {"price": "3066.00000", "qty": "0.00278335", "order_id": "ORAWGV-N5L4J-LBA3WH", "timestamp": 1765622008499456000}, + {"price": "3067.00000", "qty": "0.13902210", "order_id": "OZWNZS-QE3G6-ZPKZUT", "timestamp": 1765622021013826600} + ] + } +} diff --git a/library/src/test/resources/market/maintenance-schedule.json b/library/src/test/resources/market/maintenance-schedule.json new file mode 100644 index 0000000..519bf9a --- /dev/null +++ b/library/src/test/resources/market/maintenance-schedule.json @@ -0,0 +1,18 @@ +{ + "error": [], + "result": { + "events": [{ + "event_id": 21, + "title": "Scheduled Maintenance - Website", + "expected_start_utc": "2026-05-11T09:00:00Z", + "expected_end_utc": "2026-05-11T10:00:00Z", + "time_to_start_s": 1740, + "phase": "approaching_30m", + "affected_services": ["spot_trading"], + "order_submission": "allowed", + "recommended_action": "reduce_activity", + "cancel_before_utc": "2026-05-11T08:55:00Z", + "source_url": "https://status.kraken.com/incidents/b7k2r9wqmn41" + }] + } +} diff --git a/library/src/test/resources/market/ohlc.json b/library/src/test/resources/market/ohlc.json new file mode 100644 index 0000000..4d87b71 --- /dev/null +++ b/library/src/test/resources/market/ohlc.json @@ -0,0 +1,10 @@ +{ + "error": [], + "result": { + "XXBTZUSD": [ + [1688671200, "30306.1", "30306.2", "30305.7", "30305.7", "30306.1", "3.39243896", 23], + [1688671260, "30304.5", "30304.5", "30300.0", "30300.0", "30300.0", "4.42996871", 18] + ], + "last": 1688672160 + } +} diff --git a/library/src/test/resources/market/spread.json b/library/src/test/resources/market/spread.json new file mode 100644 index 0000000..2910c91 --- /dev/null +++ b/library/src/test/resources/market/spread.json @@ -0,0 +1,10 @@ +{ + "error": [], + "result": { + "XXBTZUSD": [ + [1688671834, "30292.10000", "30297.50000"], + [1688671834, "30292.10000", "30296.70000"] + ], + "last": 1688672106 + } +} diff --git a/library/src/test/resources/market/trades.json b/library/src/test/resources/market/trades.json new file mode 100644 index 0000000..99db8db --- /dev/null +++ b/library/src/test/resources/market/trades.json @@ -0,0 +1,10 @@ +{ + "error": [], + "result": { + "XXBTZUSD": [ + ["30243.40000", "0.34507674", 1688669597.8277369, "b", "m", "", 61044952], + ["30243.30000", "0.00376960", 1688669598.2804112, "s", "l", "", 61044953] + ], + "last": "1688671969993150842" + } +} diff --git a/pom.xml b/pom.xml index 5976be8..18f3d14 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 2.0.18 2.23.0 5.12.0 + 5.14.0 3.15.0 @@ -62,6 +63,7 @@ 3.12.0 3.3.1 3.4.0 + 3.5.4 0.11.0 @@ -127,6 +129,11 @@ + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + org.apache.maven.plugins maven-compiler-plugin From ae4d5e7f0b824678e52be25a44e513d814b2dd5e Mon Sep 17 00:00:00 2001 From: nyg Date: Mon, 7 Sep 2026 23:05:43 +0200 Subject: [PATCH 2/4] style: align PR with updated agent rules Move new API documentation into the README, preserve existing comments, and apply the no-comments rule to all added code. Rewrite endpoint and facade tests with MockitoExtension, AssertJ, explicit types, unit variables, and self-contained should/when methods. Verified with Temurin 25 and mvnd: both modules package successfully, all 60 tests pass, and the Javadoc artifact builds. --- AGENTS.md | 2 +- README.md | 13 + .../kraken/example/MarketDataExample.java | 2 - library/pom.xml | 12 + .../dev/andstuff/kraken/api/KrakenAPI.java | 92 ---- .../market/GroupedOrderBookEndpoint.java | 13 - .../market/Level3OrderBookEndpoint.java | 13 - .../market/MaintenanceScheduleEndpoint.java | 6 - .../api/endpoint/market/OhlcEndpoint.java | 13 - .../endpoint/market/OrderBookEndpoint.java | 13 - .../market/RecentSpreadsEndpoint.java | 13 - .../endpoint/market/RecentTradesEndpoint.java | 13 - .../market/params/GroupedOrderBookParams.java | 15 - .../market/params/Level3OrderBookParams.java | 12 - .../endpoint/market/params/OhlcParams.java | 21 - .../market/params/OrderBookParams.java | 18 - .../market/params/RecentSpreadsParams.java | 18 - .../market/params/RecentTradesParams.java | 21 - .../market/response/GroupedOrderBook.java | 14 - .../market/response/Level3OrderBook.java | 15 - .../market/response/MaintenanceSchedule.java | 32 -- .../endpoint/market/response/OhlcData.java | 18 - .../endpoint/market/response/OrderBook.java | 13 - .../market/response/RecentSpreads.java | 13 - .../market/response/RecentTrades.java | 23 - .../market/GroupedOrderBookEndpointTest.java | 111 +++++ .../market/Level3OrderBookEndpointTest.java | 127 +++++ .../MaintenanceScheduleEndpointTest.java | 111 +++++ .../api/endpoint/market/MarketDataTest.java | 467 +++++++----------- .../api/endpoint/market/OhlcEndpointTest.java | 151 ++++++ .../market/OrderBookEndpointTest.java | 130 +++++ .../market/RecentSpreadsEndpointTest.java | 149 ++++++ .../market/RecentTradesEndpointTest.java | 194 ++++++++ library/src/test/resources/market/README.md | 8 +- pom.xml | 2 + 35 files changed, 1177 insertions(+), 711 deletions(-) create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java create mode 100644 library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java diff --git a/AGENTS.md b/AGENTS.md index 1ebe91f..b253fa2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ mvn -pl library package # build only the library module mvn -pl examples package # build only the examples module ``` -The library's JUnit 5 tests use JSON fixtures and a fake `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`. +The library's JUnit 5 tests use JSON fixtures and Mockito mocks of `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`. Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `25`). diff --git a/README.md b/README.md index 5b9a269..3336366 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,19 @@ RecentTrades nextBatch = api.recentTrades(RecentTradesParams.builder() `OHLC`, `Depth`, `Trades`, and `Spread` accept `assetVersion(1)` for display pair keys such as `BTC/USD`; without it, Kraken returns internal keys such as `XXBTZUSD`. Their `assetClass("tokenized_asset")` option supports xStocks. Response maps preserve the keys Kraken returns. +The required `pair` field selects one asset pair. Optional numeric fields accept the following values; omit them to use Kraken's defaults: + +| Endpoint | Option | Values | Default | +|---|---|---|---| +| `OHLC` | `interval` | 1, 5, 15, 30, 60, 240, 1440, 10080, 21600 minutes | 1 | +| `Depth` | `count` | 1–500 entries per side | 100 | +| `Trades` | `count` | 1–1000 trades | 1000 | +| `GroupedBook` | `depth` | 10, 25, 100, 250, 1000 levels per side | 10 | +| `GroupedBook` | `grouping` | 1, 5, 10, 25, 50, 100, 250, 500, 1000 ticks per level | 1 | +| `Level3` | `depth` | 0 (full book), 10, 25, 100, 250, 1000 levels per side | 100 | + +OHLC candle times, L2 level times, spread times, and the `since` fields for OHLC and spreads use Unix seconds. Grouped books round asks up and bids down to the nearest grouped price level. `MaintenanceSchedule` returns scheduled events for the next seven days, ordered by expected start time; its times use `Instant`, and `cancelBefore` can be absent. + OHLC includes a final candle that is still forming and retains at most 720 entries. Reuse its `last()` cursor as `since` to poll for committed updates. Trade cursors are opaque strings: pass `last()` unchanged. Prices and quantities use `BigDecimal`; trade times retain fractional Unix seconds, while Level3 timestamps are Unix nanoseconds. Level3 requires credentials with **Orders and trades – Query open orders & trades** permission: diff --git a/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java b/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java index 7c9ac75..96acfb0 100644 --- a/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java +++ b/examples/src/main/java/dev/andstuff/kraken/example/MarketDataExample.java @@ -18,7 +18,6 @@ static void main() { OhlcData candles = api.ohlc(OhlcParams.builder().pair(pair).interval(60).assetVersion(1).build()); log.info("Hourly candles: {}", candles.candles().get(pair)); - // The final candle is still forming. Reuse candles.last() as since for committed updates. log.info("L2 order book: {}", api.orderBook(OrderBookParams.builder().pair(pair).count(10).build())); log.info("Grouped book: {}", api.groupedOrderBook(GroupedOrderBookParams.builder() @@ -27,7 +26,6 @@ static void main() { RecentTrades trades = api.recentTrades(RecentTradesParams.builder().pair(pair).count(2).build()); log.info("Recent trades: {}", trades.trades()); log.info("Next trade cursor: {}", trades.last()); - // Pass trades.last() unchanged as since when requesting the next batch. log.info("Recent spreads: {}", api.recentSpreads(pair)); log.info("Upcoming maintenance: {}", api.maintenanceSchedule()); diff --git a/library/pom.xml b/library/pom.xml index 226d85c..f977d31 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -19,6 +19,18 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + 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 58395c4..6004b62 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -262,122 +262,46 @@ public Map ticker(List pairs) { return query(new TickerEndpoint(pairs)); } - /** - * Queries the {@code OHLC} endpoint using Kraken's default options. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the candles by returned pair name and the cursor for committed updates - * @throws KrakenException if Kraken returns an error - */ public OhlcData ohlc(String pair) { return query(new OhlcEndpoint(pair)); } - /** - * Queries the {@code OHLC} endpoint. - * - * @param params the request parameters - * @return the candles by returned pair name and the cursor for committed updates - * @throws KrakenException if Kraken returns an error - */ public OhlcData ohlc(OhlcParams params) { return query(new OhlcEndpoint(params)); } - /** - * Queries the {@code Depth} endpoint using Kraken's default options. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the L2 order books by returned pair name - * @throws KrakenException if Kraken returns an error - */ public Map orderBook(String pair) { return query(new OrderBookEndpoint(pair)); } - /** - * Queries the {@code Depth} endpoint. - * - * @param params the request parameters - * @return the L2 order books by returned pair name - * @throws KrakenException if Kraken returns an error - */ public Map orderBook(OrderBookParams params) { return query(new OrderBookEndpoint(params)); } - /** - * Queries the {@code Trades} endpoint using Kraken's default options. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the trades by returned pair name and the next polling cursor - * @throws KrakenException if Kraken returns an error - */ public RecentTrades recentTrades(String pair) { return query(new RecentTradesEndpoint(pair)); } - /** - * Queries the {@code Trades} endpoint. - * - * @param params the request parameters - * @return the trades by returned pair name and the next polling cursor - * @throws KrakenException if Kraken returns an error - */ public RecentTrades recentTrades(RecentTradesParams params) { return query(new RecentTradesEndpoint(params)); } - /** - * Queries the {@code Spread} endpoint using Kraken's default options. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the spreads by returned pair name and the next polling cursor - * @throws KrakenException if Kraken returns an error - */ public RecentSpreads recentSpreads(String pair) { return query(new RecentSpreadsEndpoint(pair)); } - /** - * Queries the {@code Spread} endpoint. - * - * @param params the request parameters - * @return the spreads by returned pair name and the next polling cursor - * @throws KrakenException if Kraken returns an error - */ public RecentSpreads recentSpreads(RecentSpreadsParams params) { return query(new RecentSpreadsEndpoint(params)); } - /** - * Queries the {@code GroupedBook} endpoint using Kraken's default options. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the grouped bids and asks, pair and grouping value - * @throws KrakenException if Kraken returns an error - */ public GroupedOrderBook groupedOrderBook(String pair) { return query(new GroupedOrderBookEndpoint(pair)); } - /** - * Queries the {@code GroupedBook} endpoint. - * - * @param params the request parameters - * @return the grouped bids and asks, pair and grouping value - * @throws KrakenException if Kraken returns an error - */ public GroupedOrderBook groupedOrderBook(GroupedOrderBookParams params) { return query(new GroupedOrderBookEndpoint(params)); } - /** - * Queries the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. - * - * @return the maintenance schedule - * @throws KrakenException if Kraken returns an error - */ public MaintenanceSchedule maintenanceSchedule() { return query(new MaintenanceScheduleEndpoint()); } @@ -417,26 +341,10 @@ public PostTrade postTrade(PostTradeParams params) { /* Implemented private endpoints */ - /** - * Queries the {@code Level3} endpoint using Kraken's default options. Requires the Orders and trades - Query open orders & trades API key permission. - * - * @param pair the asset pair to query, e.g. {@code BTC/USD} - * @return the individual bid and ask orders with IDs and nanosecond timestamps - * @throws KrakenException if Kraken returns an error - * @throws IllegalStateException if credentials are missing - */ public Level3OrderBook level3OrderBook(String pair) { return query(new Level3OrderBookEndpoint(pair)); } - /** - * Queries the {@code Level3} endpoint. Requires the Orders and trades - Query open orders & trades API key permission. - * - * @param params the request parameters - * @return the individual bid and ask orders with IDs and nanosecond timestamps - * @throws KrakenException if Kraken returns an error - * @throws IllegalStateException if credentials are missing - */ public Level3OrderBook level3OrderBook(Level3OrderBookParams params) { return query(new Level3OrderBookEndpoint(params)); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java index 14f4bbb..b17dfea 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java @@ -6,25 +6,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code GroupedBook} endpoint, returning typed market data. - */ public class GroupedOrderBookEndpoint extends PublicEndpoint { - /** - * Creates the {@code GroupedBook} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public GroupedOrderBookEndpoint(String pair) { this(GroupedOrderBookParams.builder().pair(pair).build()); } - /** - * Creates the {@code GroupedBook} endpoint. - * - * @param params the request parameters - */ public GroupedOrderBookEndpoint(GroupedOrderBookParams params) { super("GroupedBook", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java index 735846b..db0de89 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java @@ -6,25 +6,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; -/** - * The private {@code Level3} endpoint, requiring the Orders and trades - Query open orders & trades API key permission. - */ public class Level3OrderBookEndpoint extends PrivateEndpoint { - /** - * Creates the {@code Level3} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public Level3OrderBookEndpoint(String pair) { this(Level3OrderBookParams.builder().pair(pair).build()); } - /** - * Creates the {@code Level3} endpoint. - * - * @param params the request parameters - */ public Level3OrderBookEndpoint(Level3OrderBookParams params) { super("Level3", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java index 8ad4c43..67011e4 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java @@ -5,14 +5,8 @@ import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code MaintenanceSchedule} endpoint, returning typed market data. - */ public class MaintenanceScheduleEndpoint extends PublicEndpoint { - /** - * Creates the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. - */ public MaintenanceScheduleEndpoint() { super("MaintenanceSchedule", new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java index bafe8c2..393f982 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java @@ -6,25 +6,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code OHLC} endpoint, returning typed market data. - */ public class OhlcEndpoint extends PublicEndpoint { - /** - * Creates the {@code OHLC} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public OhlcEndpoint(String pair) { this(OhlcParams.builder().pair(pair).build()); } - /** - * Creates the {@code OHLC} endpoint. - * - * @param params the request parameters - */ public OhlcEndpoint(OhlcParams params) { super("OHLC", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java index 8dbe25b..8b6c6f0 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java @@ -8,25 +8,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code Depth} endpoint, returning typed market data. - */ public class OrderBookEndpoint extends PublicEndpoint> { - /** - * Creates the {@code Depth} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public OrderBookEndpoint(String pair) { this(OrderBookParams.builder().pair(pair).build()); } - /** - * Creates the {@code Depth} endpoint. - * - * @param params the request parameters - */ public OrderBookEndpoint(OrderBookParams params) { super("Depth", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java index fe51109..e9b9a9a 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java @@ -6,25 +6,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code Spread} endpoint, returning typed market data. - */ public class RecentSpreadsEndpoint extends PublicEndpoint { - /** - * Creates the {@code Spread} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public RecentSpreadsEndpoint(String pair) { this(RecentSpreadsParams.builder().pair(pair).build()); } - /** - * Creates the {@code Spread} endpoint. - * - * @param params the request parameters - */ public RecentSpreadsEndpoint(RecentSpreadsParams params) { super("Spread", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java index 5aa4fa1..3d29d7f 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java @@ -6,25 +6,12 @@ import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; -/** - * The public {@code Trades} endpoint, returning typed market data. - */ public class RecentTradesEndpoint extends PublicEndpoint { - /** - * Creates the {@code Trades} endpoint using Kraken's default options. - * - * @param pair the asset pair to query - */ public RecentTradesEndpoint(String pair) { this(RecentTradesParams.builder().pair(pair).build()); } - /** - * Creates the {@code Trades} endpoint. - * - * @param params the request parameters - */ public RecentTradesEndpoint(RecentTradesParams params) { super("Trades", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java index dd84e48..dba5fae 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java @@ -10,32 +10,17 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code GroupedBook} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class GroupedOrderBookParams implements QueryParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Price levels per side: 10, 25, 100, 250 or 1000; defaults to 10. - */ private final Integer depth; - /** - * Ticks per price level: 1, 5, 10, 25, 50, 100, 250, 500 or 1000; defaults to 1. - */ private final Integer grouping; - /** - * {@inheritDoc} - */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java index 569aa92..cd68a23 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java @@ -8,27 +8,15 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code Level3} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class Level3OrderBookParams extends PostParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Price levels per side: 0 (full book), 10, 25, 100, 250 or 1000; defaults to 100. - */ private final Integer depth; - /** - * {@inheritDoc} - */ @Override protected Map params() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java index 602ea66..f831df3 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java @@ -10,42 +10,21 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code OHLC} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class OhlcParams implements QueryParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Candle interval in minutes: 1, 5, 15, 30, 60, 240, 1440, 10080 or 21600; defaults to 1. - */ private final Integer interval; - /** - * Unix timestamp in seconds, or the last cursor from the previous OHLC response. - */ private final Long since; - /** - * Use 1 for display pair names in the response; omitted for internal names. - */ private final Integer assetVersion; - /** - * Use tokenized_asset for non-crypto pairs such as xStocks. - */ private final String assetClass; - /** - * {@inheritDoc} - */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java index 54903e2..f02f242 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java @@ -10,37 +10,19 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code Depth} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class OrderBookParams implements QueryParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Maximum entries per side, from 1 to 500; defaults to 100. - */ private final Integer count; - /** - * Use 1 for display pair names in the response; omitted for internal names. - */ private final Integer assetVersion; - /** - * Use tokenized_asset for non-crypto pairs such as xStocks. - */ private final String assetClass; - /** - * {@inheritDoc} - */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java index 7fd5a64..5a7de2d 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java @@ -10,37 +10,19 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code Spread} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class RecentSpreadsParams implements QueryParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Unix timestamp in seconds, or the last cursor from the previous Spread response. - */ private final Long since; - /** - * Use 1 for display pair names in the response; omitted for internal names. - */ private final Integer assetVersion; - /** - * Use tokenized_asset for non-crypto pairs such as xStocks. - */ private final String assetClass; - /** - * {@inheritDoc} - */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java index 4d65a89..09c2202 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java @@ -10,42 +10,21 @@ import lombok.Getter; import lombok.NonNull; -/** - * The parameters of the {@code Trades} endpoint; unset options use Kraken's defaults. - */ @Getter @Builder(toBuilder = true) public class RecentTradesParams implements QueryParams { - /** - * The asset pair to query, e.g. {@code BTC/USD}. - */ @NonNull private final String pair; - /** - * Timestamp or opaque last cursor from the previous Trades response, preserved verbatim. - */ private final String since; - /** - * Maximum number of trades, from 1 to 1000; defaults to 1000. - */ private final Integer count; - /** - * Use 1 for display pair names in the response; omitted for internal names. - */ private final Integer assetVersion; - /** - * Use tokenized_asset for non-crypto pairs such as xStocks. - */ private final String assetClass; - /** - * {@inheritDoc} - */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java index 9ae4099..c3c16d3 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java @@ -5,21 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; -/** - * The {@code GroupedBook} response, with asks rounded up and bids down to grouped price levels. - * - * @param pair asset pair - * @param grouping ticks per price level used by Kraken - * @param bids aggregated bid levels - * @param asks aggregated ask levels - */ public record GroupedOrderBook(String pair, int grouping, List bids, List asks) { - /** - * A grouped price level returned by the {@code GroupedBook} endpoint. - * - * @param price grouped price - * @param quantity aggregated quantity at this price - */ public record Level(BigDecimal price, @JsonProperty("qty") BigDecimal quantity) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java index 8477b81..42cb6c8 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java @@ -5,23 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; -/** - * The {@code Level3} response, containing individual orders in the book. - * - * @param pair asset pair - * @param bids bid orders in the order returned by Kraken - * @param asks ask orders in the order returned by Kraken - */ public record Level3OrderBook(String pair, List bids, List asks) { - /** - * An individual order returned by the {@code Level3} endpoint. - * - * @param price order price - * @param quantity order quantity - * @param orderId order identifier - * @param timestamp Unix timestamp in nanoseconds - */ public record Order(BigDecimal price, @JsonProperty("qty") BigDecimal quantity, @JsonProperty("order_id") String orderId, long timestamp) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java index 1c89fee..cafe6a6 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java @@ -6,28 +6,8 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; -/** - * The {@code MaintenanceSchedule} response, covering the next seven days. - * - * @param events scheduled events ordered by expected start time, empty when none are scheduled - */ public record MaintenanceSchedule(List events) { - /** - * A scheduled maintenance event returned by the {@code MaintenanceSchedule} endpoint. - * - * @param eventId stable event identifier - * @param title event title - * @param expectedStart expected start time - * @param expectedEnd expected end time - * @param timeToStart seconds until the start, evaluated when Kraken served the response - * @param phase approach phase - * @param affectedServices affected Kraken services - * @param orderSubmission guidance about submitting orders - * @param recommendedAction suggested client action - * @param cancelBefore cancellation deadline, absent when not scheduled - * @param sourceUrl link to the event on Kraken's status page - */ public record Event(@JsonProperty("event_id") long eventId, String title, @JsonProperty("expected_start_utc") Instant expectedStart, @@ -40,34 +20,22 @@ public record Event(@JsonProperty("event_id") long eventId, @JsonProperty("cancel_before_utc") Instant cancelBefore, @JsonProperty("source_url") String sourceUrl) {} - /** - * The approach phase in a {@code MaintenanceSchedule} event. - */ public enum Phase { ANNOUNCED, REMINDER_24H, APPROACHING_30M, IMMINENT_5M, FINAL_WARNING_30S, @JsonEnumDefaultValue UNKNOWN } - /** - * A Kraken service affected by a {@code MaintenanceSchedule} event. - */ public enum Service { SPOT_WS, SPOT_REST, SPOT_FIX, SPOT_TRADING, FUTURES_WS, FUTURES_REST, FUTURES_FIX, FUTURES_TRADING, ALL, @JsonEnumDefaultValue UNKNOWN } - /** - * Order submission guidance in a {@code MaintenanceSchedule} event. - */ public enum OrderSubmission { ALLOWED, DISCOURAGED, BLOCKED, @JsonEnumDefaultValue UNKNOWN } - /** - * Suggested client action in a {@code MaintenanceSchedule} event. - */ public enum RecommendedAction { CONTINUE, REDUCE_ACTIVITY, CANCEL_OPEN_ORDERS, STOP_NEW_ORDERS, @JsonEnumDefaultValue UNKNOWN diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java index 02f3b71..799a31a 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java @@ -12,27 +12,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -/** - * The {@code OHLC} response containing OHLC candles, including the final, uncommitted candle. At most 720 entries are available. - * - * @param candles entries by Kraken's returned pair name (internal or display name) - * @param last cursor to pass unchanged as {@code since} when polling for committed candles - */ @JsonDeserialize(builder = OhlcData.ResponseBuilder.class) public record OhlcData(Map> candles, Long last) { - /** - * A positional OHLC candle returned by the {@code OHLC} endpoint. - * - * @param time candle start, in Unix seconds - * @param open opening price - * @param high highest price - * @param low lowest price - * @param close closing price - * @param vwap volume weighted average price - * @param volume traded volume - * @param count number of trades - */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "open", "high", "low", "close", "vwap", "volume", "count"}) public record Candle(long time, BigDecimal open, BigDecimal high, BigDecimal low, diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java index f1614c2..5daf303 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java @@ -6,21 +6,8 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -/** - * A pair's aggregated L2 order book from the {@code Depth} endpoint. - * - * @param asks ask levels in the order returned by Kraken - * @param bids bid levels in the order returned by Kraken - */ public record OrderBook(List asks, List bids) { - /** - * An aggregated price level returned by the {@code Depth} endpoint. - * - * @param price price of this level - * @param volume aggregated volume at this price - * @param time Unix timestamp in seconds - */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time"}) public record Level(BigDecimal price, BigDecimal volume, long time) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java index 4d960ef..1fa91c6 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java @@ -12,22 +12,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -/** - * The {@code Spread} response containing recent top-of-book spreads; Kraken retains approximately 200 entries. - * - * @param spreads entries by Kraken's returned pair name (internal or display name) - * @param last cursor to pass unchanged as {@code since} when polling for new data - */ @JsonDeserialize(builder = RecentSpreads.ResponseBuilder.class) public record RecentSpreads(Map> spreads, Long last) { - /** - * A positional top-of-book spread returned by the {@code Spread} endpoint. - * - * @param time Unix timestamp in seconds - * @param bid best bid price - * @param ask best ask price - */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "bid", "ask"}) public record Spread(long time, BigDecimal bid, BigDecimal ask) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java index 8a3205c..0e2e737 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java @@ -13,43 +13,20 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -/** - * The {@code Trades} response containing recent trades; by default Kraken returns the last 1000 trades. - * - * @param trades entries by Kraken's returned pair name (internal or display name) - * @param last cursor to pass unchanged as {@code since} when polling for new data - */ @JsonDeserialize(builder = RecentTrades.ResponseBuilder.class) public record RecentTrades(Map> trades, String last) { - /** - * A positional trade returned by the {@code Trades} endpoint. - * - * @param price trade price - * @param volume traded volume - * @param time Unix timestamp in seconds, retaining fractional precision - * @param side buy or sell - * @param orderType market or limit - * @param miscellaneous additional trade information - * @param tradeId trade identifier - */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time", "side", "orderType", "miscellaneous", "tradeId"}) public record Trade(BigDecimal price, BigDecimal volume, BigDecimal time, Side side, OrderType orderType, String miscellaneous, long tradeId) {} - /** - * The buy/sell code in a {@code Trades} row. - */ public enum Side { @JsonProperty("b") BUY, @JsonProperty("s") SELL, @JsonEnumDefaultValue UNKNOWN } - /** - * The market/limit code in a {@code Trades} row. - */ public enum OrderType { @JsonProperty("m") MARKET, @JsonProperty("l") LIMIT, diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java new file mode 100644 index 0000000..8512125 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java @@ -0,0 +1,111 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; + +@ExtendWith(MockitoExtension.class) +class GroupedOrderBookEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + GroupedOrderBookEndpoint unit = new GroupedOrderBookEndpoint(GroupedOrderBookParams.builder().pair("BTC/USD").depth(25).grouping(1000).build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(url.getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/GroupedBook"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "depth", "25", "grouping", "1000")); + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("GroupedBook"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + GroupedOrderBookEndpoint unit = new GroupedOrderBookEndpoint("BTC/USD"); + + URL result = unit.buildURL(); + + assertThat(result).hasParameter("pair", "BTC/USD"); + assertThat(result.getQuery()).isEqualTo("pair=BTC%2FUSD"); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + GroupedOrderBookParams.GroupedOrderBookParamsBuilder unit = GroupedOrderBookParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_read_grouping_and_aggregated_quantities_when_decoding_documented_response() throws Exception { + GroupedOrderBookEndpoint unit = new GroupedOrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/grouped-book.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + GroupedOrderBook result = response.result().orElseThrow(); + + assertThat(result).extracting(GroupedOrderBook::pair, GroupedOrderBook::grouping).containsExactly("BTC/USD", 1000); + assertThat(result.bids()).hasSize(2).first() + .isEqualTo(new GroupedOrderBook.Level(new BigDecimal("90400.00000"), new BigDecimal("19.83057746"))); + assertThat(result.asks()).hasSize(2).first() + .isEqualTo(new GroupedOrderBook.Level(new BigDecimal("90500.00000"), new BigDecimal("38.96185061"))); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + GroupedOrderBookEndpoint unit = new GroupedOrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"pair":"BTC/USD","grouping":1,"asks":[],"bids":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + GroupedOrderBook result = response.result().orElseThrow(); + + assertThat(result.asks()).isEmpty(); + assertThat(result.bids()).isEmpty(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java new file mode 100644 index 0000000..10f98ff --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java @@ -0,0 +1,127 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.Level3OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; + +@ExtendWith(MockitoExtension.class) +class Level3OrderBookEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + Level3OrderBookEndpoint unit = new Level3OrderBookEndpoint(Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(unit.encodedParamsWith("123").split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("POST"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/private/Level3"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "123")); + assertThat(List.of(KrakenAPI.Private.LEVEL3)).extracting(KrakenAPI.Private::getPath).contains("Level3"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + Level3OrderBookEndpoint unit = new Level3OrderBookEndpoint("BTC/USD"); + + Map parameters = Arrays.stream(unit.encodedParamsWith("123").split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "nonce", "123")); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + Level3OrderBookParams.Level3OrderBookParamsBuilder unit = Level3OrderBookParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_retain_individual_orders_and_nanosecond_timestamps_when_decoding_documented_response() throws Exception { + Level3OrderBookEndpoint unit = new Level3OrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/level3.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Level3OrderBook result = response.result().orElseThrow(); + + assertThat(result.pair()).isEqualTo("YFI/EUR"); + assertThat(result.bids()).hasSize(2).first().isEqualTo( + new Level3OrderBook.Order(new BigDecimal("3062.00000"), new BigDecimal("0.29665800"), "O5KJU4-IEQTM-NDMS6W", 1765622008594292000L)); + assertThat(result.bids().getLast().orderId()).isNotEqualTo(result.bids().getFirst().orderId()); + assertThat(result.asks().getFirst().quantity()).isEqualTo(new BigDecimal("0.00278335")); + assertThat(result.asks().getLast().timestamp()).isEqualTo(1765622021013826600L); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + Level3OrderBookEndpoint unit = new Level3OrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"pair":"BTC/USD","grouping":1,"asks":[],"bids":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Level3OrderBook result = response.result().orElseThrow(); + + assertThat(result.asks()).isEmpty(); + assertThat(result.bids()).isEmpty(); + } + + @Test + void should_replace_previous_nonce_when_reusing_endpoint() { + Level3OrderBookEndpoint unit = new Level3OrderBookEndpoint("YFI/EUR"); + unit.encodedParamsWith("123"); + + Map parameters = Arrays.stream(unit.encodedParamsWith("124").split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "YFI/EUR", "nonce", "124")); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java new file mode 100644 index 0000000..8cbcb9e --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java @@ -0,0 +1,111 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; + +@ExtendWith(MockitoExtension.class) +class MaintenanceScheduleEndpointTest { + + @InjectMocks + private MaintenanceScheduleEndpoint unit; + + @Test + void should_use_public_get_without_parameters_when_requesting_the_schedule() { + URL result = unit.buildURL(); + + assertThat(result).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/MaintenanceSchedule").hasNoParameters(); + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(KrakenAPI.Public.MAINTENANCE_SCHEDULE.getPath()).isEqualTo("MaintenanceSchedule"); + } + + @Test + void should_read_every_maintenance_event_field_when_decoding_documented_response() throws Exception { + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/maintenance-schedule.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + MaintenanceSchedule result = response.result().orElseThrow(); + + assertThat(result.events()).containsExactly(new MaintenanceSchedule.Event(21, "Scheduled Maintenance - Website", + Instant.parse("2026-05-11T09:00:00Z"), Instant.parse("2026-05-11T10:00:00Z"), 1740, + MaintenanceSchedule.Phase.APPROACHING_30M, List.of(MaintenanceSchedule.Service.SPOT_TRADING), + MaintenanceSchedule.OrderSubmission.ALLOWED, MaintenanceSchedule.RecommendedAction.REDUCE_ACTIVITY, + Instant.parse("2026-05-11T08:55:00Z"), "https://status.kraken.com/incidents/b7k2r9wqmn41")); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"events":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + MaintenanceSchedule result = response.result().orElseThrow(); + + assertThat(result.events()).isEmpty(); + } + + @Test + void should_retain_unknown_enums_and_absent_deadline_when_schedule_gains_new_values() throws Exception { + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/maintenance-schedule.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("approaching_30m", "future-phase") + .replace("spot_trading", "future-service").replace("allowed", "future-guidance") + .replace("reduce_activity", "future-action") + .replace("\"cancel_before_utc\": \"2026-05-11T08:55:00Z\",", "\"future_field\": true,"); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + MaintenanceSchedule result = response.result().orElseThrow(); + + assertThat(result.events().getFirst()).satisfies(event -> { + assertThat(event.phase()).isEqualTo(MaintenanceSchedule.Phase.UNKNOWN); + assertThat(event.affectedServices()).containsExactly(MaintenanceSchedule.Service.UNKNOWN); + assertThat(event.orderSubmission()).isEqualTo(MaintenanceSchedule.OrderSubmission.UNKNOWN); + assertThat(event.recommendedAction()).isEqualTo(MaintenanceSchedule.RecommendedAction.UNKNOWN); + assertThat(event.cancelBefore()).isNull(); + }); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java index 25baac5..2069183 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MarketDataTest.java @@ -1,43 +1,28 @@ package dev.andstuff.kraken.api.endpoint.market; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import dev.andstuff.kraken.api.KrakenAPI; -import dev.andstuff.kraken.api.endpoint.Endpoint; import dev.andstuff.kraken.api.endpoint.KrakenException; -import dev.andstuff.kraken.api.endpoint.KrakenResponse; import dev.andstuff.kraken.api.endpoint.market.params.GroupedOrderBookParams; import dev.andstuff.kraken.api.endpoint.market.params.Level3OrderBookParams; import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; @@ -51,332 +36,228 @@ import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; -import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; -import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; import dev.andstuff.kraken.api.rest.KrakenCredentials; import dev.andstuff.kraken.api.rest.KrakenNonceGenerator; import dev.andstuff.kraken.api.rest.KrakenRestRequester; +@ExtendWith(MockitoExtension.class) class MarketDataTest { - // Match DefaultKrakenRestRequester: bind directly from JSON, not an intermediate tree. - private static final ObjectMapper 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(); - - private static final String PAIR = "BTC/USD"; - - @ParameterizedTest - @MethodSource("publicRequests") - void encodesEveryDocumentedPublicParameter(PublicEndpoint endpoint, String path, Map expected) { - assertEquals("GET", endpoint.getHttpMethod()); - assertEquals("https", endpoint.buildURL().getProtocol()); - assertEquals("api.kraken.com", endpoint.buildURL().getHost()); - assertEquals("/0/public/" + path, endpoint.buildURL().getPath()); - assertEquals(expected, decode(endpoint.buildURL().getQuery())); - assertTrue(Arrays.stream(KrakenAPI.Public.values()).anyMatch(value -> value.getPath().equals(path))); - } + @Mock + private KrakenRestRequester requester; - static Stream publicRequests() { - return Stream.of( - Arguments.of(new OhlcEndpoint(OhlcParams.builder().pair(PAIR).interval(60).since(1688671200L) - .assetVersion(1).assetClass("tokenized_asset").build()), "OHLC", - Map.of("pair", PAIR, "interval", "60", "since", "1688671200", "assetVersion", "1", "asset_class", "tokenized_asset")), - Arguments.of(new OrderBookEndpoint(OrderBookParams.builder().pair(PAIR).count(500) - .assetVersion(1).assetClass("tokenized_asset").build()), "Depth", - Map.of("pair", PAIR, "count", "500", "assetVersion", "1", "asset_class", "tokenized_asset")), - Arguments.of(new RecentTradesEndpoint(RecentTradesParams.builder().pair(PAIR).since("1688671969993150842").count(2) - .assetVersion(1).assetClass("tokenized_asset").build()), "Trades", - Map.of("pair", PAIR, "since", "1688671969993150842", "count", "2", "assetVersion", "1", "asset_class", "tokenized_asset")), - Arguments.of(new RecentSpreadsEndpoint(RecentSpreadsParams.builder().pair(PAIR).since(1688672106L) - .assetVersion(1).assetClass("tokenized_asset").build()), "Spread", - Map.of("pair", PAIR, "since", "1688672106", "assetVersion", "1", "asset_class", "tokenized_asset")), - Arguments.of(new GroupedOrderBookEndpoint(GroupedOrderBookParams.builder().pair(PAIR).depth(25).grouping(1000).build()), "GroupedBook", - Map.of("pair", PAIR, "depth", "25", "grouping", "1000")), - Arguments.of(new OhlcEndpoint(PAIR), "OHLC", Map.of("pair", PAIR)), - Arguments.of(new OrderBookEndpoint(PAIR), "Depth", Map.of("pair", PAIR)), - Arguments.of(new RecentTradesEndpoint(PAIR), "Trades", Map.of("pair", PAIR)), - Arguments.of(new RecentSpreadsEndpoint(PAIR), "Spread", Map.of("pair", PAIR)), - Arguments.of(new GroupedOrderBookEndpoint(PAIR), "GroupedBook", Map.of("pair", PAIR)), - Arguments.of(new MaintenanceScheduleEndpoint(), "MaintenanceSchedule", Map.of())); - } + @Mock + private KrakenCredentials credentials; - @Test - void encodesLevel3WithNonceAndFullBookDepth() { - var endpoint = new Level3OrderBookEndpoint(Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build()); - assertEquals("POST", endpoint.getHttpMethod()); - assertEquals("https://api.kraken.com/0/private/Level3", endpoint.buildURL().toString()); - assertEquals(KrakenAPI.Private.LEVEL3.getPath(), endpoint.getPath()); - assertEquals(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "123"), decode(endpoint.encodedParamsWith("123"))); - assertEquals("124", decode(endpoint.encodedParamsWith("124")).get("nonce")); - assertEquals(Map.of("pair", PAIR, "nonce", "123"), decode(new Level3OrderBookEndpoint(PAIR).encodedParamsWith("123"))); - } + @Mock + private KrakenNonceGenerator nonceGenerator; @Test - void requiresThePairForEachParameterizedEndpoint() { - assertThrows(NullPointerException.class, () -> OhlcParams.builder().build()); - assertThrows(NullPointerException.class, () -> OrderBookParams.builder().build()); - assertThrows(NullPointerException.class, () -> RecentTradesParams.builder().build()); - assertThrows(NullPointerException.class, () -> RecentSpreadsParams.builder().build()); - assertThrows(NullPointerException.class, () -> GroupedOrderBookParams.builder().build()); - assertThrows(NullPointerException.class, () -> Level3OrderBookParams.builder().build()); - } + void should_route_ohlc_through_configured_requester_when_using_default_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + OhlcData expected = new OhlcData(Map.of(), 123L); + when(requester.execute(any(OhlcEndpoint.class))).thenReturn(expected); - @Test - void readsAllCandlePositionsAndReusesTheCommittedCursor() { - OhlcData data = read(new OhlcEndpoint(PAIR), fixture("ohlc")); - assertEquals(new OhlcData.Candle(1688671200, decimal("30306.1"), decimal("30306.2"), decimal("30305.7"), - decimal("30305.7"), decimal("30306.1"), decimal("3.39243896"), 23), data.candles().get("XXBTZUSD").getFirst()); - assertEquals(2, data.candles().get("XXBTZUSD").size()); - assertEquals(1688671260L, data.candles().get("XXBTZUSD").getLast().time()); - assertEquals(1688672160L, data.last()); - assertEquals("1688672160", OhlcParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); - } + OhlcData result = unit.ohlc("BTC/USD"); - @Test - void readsDepthLevelsAndIgnoresUnknownBookProperties() { - Map books = read(new OrderBookEndpoint(PAIR), fixture("depth")); - OrderBook book = books.get("XXBTZUSD"); - assertEquals(new OrderBook.Level(decimal("30384.10000"), decimal("2.059"), 1688671659), book.asks().getFirst()); - assertEquals(new OrderBook.Level(decimal("30297.00000"), decimal("1.115"), 1688671636), book.bids().getFirst()); - assertEquals(2, book.asks().size()); - assertEquals(2, book.bids().size()); + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((OhlcEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("pair=BTC%2FUSD"))); } @Test - void readsTradeCodesAndPreservesDecimalTimeAndOpaqueCursor() { - RecentTrades data = read(new RecentTradesEndpoint(PAIR), fixture("trades")); - assertEquals(new RecentTrades.Trade(decimal("30243.40000"), decimal("0.34507674"), decimal("1688669597.8277369"), - RecentTrades.Side.BUY, RecentTrades.OrderType.MARKET, "", 61044952), data.trades().get("XXBTZUSD").getFirst()); - assertEquals(RecentTrades.Side.SELL, data.trades().get("XXBTZUSD").getLast().side()); - assertEquals(RecentTrades.OrderType.LIMIT, data.trades().get("XXBTZUSD").getLast().orderType()); - assertEquals("1688671969993150842", data.last()); - assertEquals(data.last(), RecentTradesParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); + void should_route_ohlc_through_configured_requester_when_using_custom_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + OhlcParams params = OhlcParams.builder().pair("BTC/USD").interval(60).build(); + OhlcData expected = new OhlcData(Map.of(), 123L); + when(requester.execute(any(OhlcEndpoint.class))).thenReturn(expected); + + OhlcData result = unit.ohlc(params); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((OhlcEndpoint endpoint) -> Arrays.stream(endpoint.buildURL().getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> entry[0], entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))) + .equals(params.toMap()))); } @Test - void preservesSubMicrosecondTradePrecisionAndLargeIdentifiers() { - String json = fixture("trades").replace("1688669597.8277369", "1688669597.123456789") - .replace("61044952", "61044952000000001"); - var trade = read(new RecentTradesEndpoint(PAIR), json).trades().get("XXBTZUSD").getFirst(); - assertEquals(decimal("1688669597.123456789"), trade.time()); - assertEquals(61044952000000001L, trade.tradeId()); - } + void should_route_order_book_through_configured_requester_when_using_default_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + Map expected = Map.of("BTC/USD", new OrderBook(List.of(), List.of())); + when(requester.execute(any(OrderBookEndpoint.class))).thenReturn(expected); - @Test - void toleratesFutureTradeCodes() { - String json = fixture("trades").replace("\"b\"", "\"future-side\"").replace("\"m\"", "\"future-type\""); - var trade = read(new RecentTradesEndpoint(PAIR), json).trades().get("XXBTZUSD").getFirst(); - assertEquals(RecentTrades.Side.UNKNOWN, trade.side()); - assertEquals(RecentTrades.OrderType.UNKNOWN, trade.orderType()); + Map result = unit.orderBook("BTC/USD"); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((OrderBookEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("pair=BTC%2FUSD"))); } @Test - void readsSpreadsAndReusesTheCursor() { - RecentSpreads data = read(new RecentSpreadsEndpoint(PAIR), fixture("spread")); - assertEquals(new RecentSpreads.Spread(1688671834, decimal("30292.10000"), decimal("30297.50000")), - data.spreads().get("XXBTZUSD").getFirst()); - assertEquals(1688672106L, data.last()); - assertEquals("1688672106", RecentSpreadsParams.builder().pair(PAIR).since(data.last()).build().toMap().get("since")); + void should_route_order_book_through_configured_requester_when_using_custom_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + OrderBookParams params = OrderBookParams.builder().pair("BTC/USD").count(10).build(); + Map expected = Map.of("BTC/USD", new OrderBook(List.of(), List.of())); + when(requester.execute(any(OrderBookEndpoint.class))).thenReturn(expected); + + Map result = unit.orderBook(params); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((OrderBookEndpoint endpoint) -> Arrays.stream(endpoint.buildURL().getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> entry[0], entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))) + .equals(params.toMap()))); } @Test - void acceptsDisplayPairKeysForAllLegacyMarketResponses() { - assertTrue(read(new OhlcEndpoint(PAIR), fixture("ohlc").replace("XXBTZUSD", PAIR)).candles().containsKey(PAIR)); - assertTrue(read(new OrderBookEndpoint(PAIR), fixture("depth").replace("XXBTZUSD", PAIR)).containsKey(PAIR)); - assertTrue(read(new RecentTradesEndpoint(PAIR), fixture("trades").replace("XXBTZUSD", PAIR)).trades().containsKey(PAIR)); - assertTrue(read(new RecentSpreadsEndpoint(PAIR), fixture("spread").replace("XXBTZUSD", PAIR)).spreads().containsKey(PAIR)); + void should_route_recent_trades_through_configured_requester_when_using_default_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + RecentTrades expected = new RecentTrades(Map.of(), "123"); + when(requester.execute(any(RecentTradesEndpoint.class))).thenReturn(expected); + + RecentTrades result = unit.recentTrades("BTC/USD"); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((RecentTradesEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("pair=BTC%2FUSD"))); } @Test - void separatesCursorFromMultiplePairKeysRegardlessOfPropertyOrder() { - String json = """ - {"error":[],"result":{"last":123,"BTC/USD":[],"ETH/USD":[]}} - """; - assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), read(new OhlcEndpoint(PAIR), json).candles()); - assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), read(new RecentSpreadsEndpoint(PAIR), json).spreads()); - var trades = read(new RecentTradesEndpoint(PAIR), json.replace(":123", ":\"001234567890123456789\"")); - assertEquals(Map.of("BTC/USD", List.of(), "ETH/USD", List.of()), trades.trades()); - assertEquals("001234567890123456789", trades.last()); + void should_route_recent_trades_through_configured_requester_when_using_custom_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + RecentTradesParams params = RecentTradesParams.builder().pair("BTC/USD").count(10).build(); + RecentTrades expected = new RecentTrades(Map.of(), "123"); + when(requester.execute(any(RecentTradesEndpoint.class))).thenReturn(expected); + + RecentTrades result = unit.recentTrades(params); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((RecentTradesEndpoint endpoint) -> Arrays.stream(endpoint.buildURL().getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> entry[0], entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))) + .equals(params.toMap()))); } @Test - void readsGroupedQuantitiesAndGrouping() { - GroupedOrderBook book = read(new GroupedOrderBookEndpoint(PAIR), fixture("grouped-book")); - assertEquals(PAIR, book.pair()); - assertEquals(1000, book.grouping()); - assertEquals(new GroupedOrderBook.Level(decimal("90400.00000"), decimal("19.83057746")), book.bids().getFirst()); - assertEquals(new GroupedOrderBook.Level(decimal("90500.00000"), decimal("38.96185061")), book.asks().getFirst()); + void should_route_recent_spreads_through_configured_requester_when_using_default_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + RecentSpreads expected = new RecentSpreads(Map.of(), 123L); + when(requester.execute(any(RecentSpreadsEndpoint.class))).thenReturn(expected); + + RecentSpreads result = unit.recentSpreads("BTC/USD"); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((RecentSpreadsEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("pair=BTC%2FUSD"))); } @Test - void readsIndividualLevel3OrdersAndNanosecondTimestamps() { - Level3OrderBook book = read(new Level3OrderBookEndpoint("YFI/EUR"), fixture("level3")); - assertEquals("YFI/EUR", book.pair()); - assertEquals(new Level3OrderBook.Order(decimal("3062.00000"), decimal("0.29665800"), "O5KJU4-IEQTM-NDMS6W", 1765622008594292000L), - book.bids().getFirst()); - assertEquals(2, book.bids().size()); - assertNotEquals(book.bids().getFirst().orderId(), book.bids().getLast().orderId()); - assertEquals(decimal("0.00278335"), book.asks().getFirst().quantity()); - assertEquals(1765622021013826600L, book.asks().getLast().timestamp()); + void should_route_recent_spreads_through_configured_requester_when_using_custom_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + RecentSpreadsParams params = RecentSpreadsParams.builder().pair("BTC/USD").since(123L).build(); + RecentSpreads expected = new RecentSpreads(Map.of(), 123L); + when(requester.execute(any(RecentSpreadsEndpoint.class))).thenReturn(expected); + + RecentSpreads result = unit.recentSpreads(params); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((RecentSpreadsEndpoint endpoint) -> Arrays.stream(endpoint.buildURL().getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> entry[0], entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))) + .equals(params.toMap()))); } @Test - void readsEveryMaintenanceEventField() { - var event = read(new MaintenanceScheduleEndpoint(), fixture("maintenance-schedule")).events().getFirst(); - assertEquals(new MaintenanceSchedule.Event(21, "Scheduled Maintenance - Website", - Instant.parse("2026-05-11T09:00:00Z"), Instant.parse("2026-05-11T10:00:00Z"), 1740, - MaintenanceSchedule.Phase.APPROACHING_30M, List.of(MaintenanceSchedule.Service.SPOT_TRADING), - MaintenanceSchedule.OrderSubmission.ALLOWED, MaintenanceSchedule.RecommendedAction.REDUCE_ACTIVITY, - Instant.parse("2026-05-11T08:55:00Z"), "https://status.kraken.com/incidents/b7k2r9wqmn41"), event); + void should_route_grouped_order_book_through_configured_requester_when_using_default_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + GroupedOrderBook expected = new GroupedOrderBook("BTC/USD", 1000, List.of(), List.of()); + when(requester.execute(any(GroupedOrderBookEndpoint.class))).thenReturn(expected); + + GroupedOrderBook result = unit.groupedOrderBook("BTC/USD"); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((GroupedOrderBookEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("pair=BTC%2FUSD"))); } @Test - void toleratesFutureMaintenanceValuesAndAnAbsentCancellationDeadline() { - String json = fixture("maintenance-schedule").replace("approaching_30m", "future-phase") - .replace("spot_trading", "future-service").replace("allowed", "future-guidance") - .replace("reduce_activity", "future-action") - .replace("\"cancel_before_utc\": \"2026-05-11T08:55:00Z\",", "\"future_field\": true,"); - var event = read(new MaintenanceScheduleEndpoint(), json).events().getFirst(); - assertEquals(MaintenanceSchedule.Phase.UNKNOWN, event.phase()); - assertEquals(List.of(MaintenanceSchedule.Service.UNKNOWN), event.affectedServices()); - assertEquals(MaintenanceSchedule.OrderSubmission.UNKNOWN, event.orderSubmission()); - assertEquals(MaintenanceSchedule.RecommendedAction.UNKNOWN, event.recommendedAction()); - assertNull(event.cancelBefore()); + void should_route_grouped_order_book_through_configured_requester_when_using_custom_options() { + KrakenAPI unit = new KrakenAPI(null, requester); + GroupedOrderBookParams params = GroupedOrderBookParams.builder().pair("BTC/USD").grouping(1000).build(); + GroupedOrderBook expected = new GroupedOrderBook("BTC/USD", 1000, List.of(), List.of()); + when(requester.execute(any(GroupedOrderBookEndpoint.class))).thenReturn(expected); + + GroupedOrderBook result = unit.groupedOrderBook(params); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((GroupedOrderBookEndpoint endpoint) -> Arrays.stream(endpoint.buildURL().getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> entry[0], entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))) + .equals(params.toMap()))); } @Test - void handlesEmptyBooksSchedulesAndTimeSeries() { - assertTrue(read(new MaintenanceScheduleEndpoint(), "{\"error\":[],\"result\":{\"events\":[]}}").events().isEmpty()); - assertTrue(read(new OrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"BTC/USD\":{\"asks\":[],\"bids\":[]}}}").get(PAIR).bids().isEmpty()); - assertTrue(read(new GroupedOrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"pair\":\"BTC/USD\",\"grouping\":1,\"asks\":[],\"bids\":[]}}").asks().isEmpty()); - assertTrue(read(new Level3OrderBookEndpoint(PAIR), "{\"error\":[],\"result\":{\"pair\":\"BTC/USD\",\"asks\":[],\"bids\":[]}}").bids().isEmpty()); - String json = "{\"error\":[],\"result\":{\"BTC/USD\":[],\"last\":123}}"; - assertTrue(read(new OhlcEndpoint(PAIR), json).candles().get(PAIR).isEmpty()); - assertTrue(read(new RecentTradesEndpoint(PAIR), json).trades().get(PAIR).isEmpty()); - assertTrue(read(new RecentSpreadsEndpoint(PAIR), json).spreads().get(PAIR).isEmpty()); - } + void should_route_maintenance_schedule_through_configured_requester_when_querying_events() { + KrakenAPI unit = new KrakenAPI(null, requester); + MaintenanceSchedule expected = new MaintenanceSchedule(List.of()); + when(requester.execute(any(MaintenanceScheduleEndpoint.class))).thenReturn(expected); - @ParameterizedTest - @MethodSource("publicFacadeCalls") - void facadeRoutesEveryPublicOverloadThroughTheConfiguredRequester(String path, String fixture, Class type, Function call) { - var requester = new FixtureRequester(fixture(fixture)); - Object result = call.apply(new KrakenAPI(null, requester)); - assertInstanceOf(type, result); - assertEquals(path, requester.endpoint.getPath()); - assertInstanceOf(PublicEndpoint.class, requester.endpoint); - } + MaintenanceSchedule result = unit.maintenanceSchedule(); - static Stream publicFacadeCalls() { - return Stream.of( - facade("OHLC", "ohlc", OhlcData.class, api -> api.ohlc(PAIR)), - facade("OHLC", "ohlc", OhlcData.class, api -> api.ohlc(OhlcParams.builder().pair(PAIR).build())), - facade("Depth", "depth", Map.class, api -> api.orderBook(PAIR)), - facade("Depth", "depth", Map.class, api -> api.orderBook(OrderBookParams.builder().pair(PAIR).build())), - facade("Trades", "trades", RecentTrades.class, api -> api.recentTrades(PAIR)), - facade("Trades", "trades", RecentTrades.class, api -> api.recentTrades(RecentTradesParams.builder().pair(PAIR).build())), - facade("Spread", "spread", RecentSpreads.class, api -> api.recentSpreads(PAIR)), - facade("Spread", "spread", RecentSpreads.class, api -> api.recentSpreads(RecentSpreadsParams.builder().pair(PAIR).build())), - facade("GroupedBook", "grouped-book", GroupedOrderBook.class, api -> api.groupedOrderBook(PAIR)), - facade("GroupedBook", "grouped-book", GroupedOrderBook.class, api -> api.groupedOrderBook(GroupedOrderBookParams.builder().pair(PAIR).build())), - facade("MaintenanceSchedule", "maintenance-schedule", MaintenanceSchedule.class, KrakenAPI::maintenanceSchedule)); + assertThat(result).isSameAs(expected); + verify(requester).execute(any(MaintenanceScheduleEndpoint.class)); } @Test - void level3FacadePassesConfiguredCredentialsAndNonceGenerator() { - var requester = new FixtureRequester(fixture("level3")); - var credentials = new KrakenCredentials("test-key", "c2VjcmV0"); - KrakenNonceGenerator nonce = () -> "987654321"; - KrakenAPI api = new KrakenAPI(credentials, nonce, requester); - assertEquals("YFI/EUR", api.level3OrderBook("YFI/EUR").pair()); - assertSame(credentials, requester.credentials); - assertSame(nonce, requester.nonceGenerator); - assertEquals(Map.of("pair", "YFI/EUR", "nonce", "987654321"), decode(requester.postBody)); - api.level3OrderBook(Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build()); - assertEquals(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "987654321"), decode(requester.postBody)); - assertEquals("Level3", requester.endpoint.getPath()); + void should_pass_credentials_and_nonce_generator_when_querying_level3_with_default_options() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + Level3OrderBook expected = new Level3OrderBook("YFI/EUR", List.of(), List.of()); + when(requester.execute(any(Level3OrderBookEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(expected); + + Level3OrderBook result = unit.level3OrderBook("YFI/EUR"); + + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((Level3OrderBookEndpoint endpoint) -> { + Level3OrderBookParams params = (Level3OrderBookParams) endpoint.getPostParams(); + return params.getPair().equals("YFI/EUR") && params.getDepth() == null; + }), same(credentials), same(nonceGenerator)); } @Test - void rejectsLevel3WithoutCredentialsBeforeCallingTheRequester() { - var requester = new FixtureRequester(fixture("level3")); - KrakenAPI api = new KrakenAPI(null, requester); - assertTrue(assertThrows(IllegalStateException.class, () -> api.level3OrderBook(PAIR)).getMessage().contains("Level3")); - assertThrows(IllegalStateException.class, () -> api.level3OrderBook(Level3OrderBookParams.builder().pair(PAIR).build())); - assertNull(requester.endpoint); - } + void should_preserve_parameters_and_authentication_when_querying_level3_with_custom_options() { + KrakenAPI unit = new KrakenAPI(credentials, nonceGenerator, requester); + Level3OrderBookParams params = Level3OrderBookParams.builder().pair("YFI/EUR").depth(0).build(); + Level3OrderBook expected = new Level3OrderBook("YFI/EUR", List.of(), List.of()); + when(requester.execute(any(Level3OrderBookEndpoint.class), same(credentials), same(nonceGenerator))).thenReturn(expected); - @Test - void preservesKrakenErrors() { - var requester = new FixtureRequester("{\"error\":[\"EQuery:Unknown asset pair\"]}"); - var api = new KrakenAPI(null, requester); - var error = assertThrows(KrakenException.class, () -> api.ohlc(PAIR)); - assertEquals(List.of("EQuery:Unknown asset pair"), error.getErrors()); - } + Level3OrderBook result = unit.level3OrderBook(params); - private static Arguments facade(String path, String fixture, Class type, Function call) { - return Arguments.of(path, fixture, type, call); + assertThat(result).isSameAs(expected); + verify(requester).execute(argThat((Level3OrderBookEndpoint endpoint) -> endpoint.getPostParams() == params), same(credentials), same(nonceGenerator)); } - private static BigDecimal decimal(String value) { - return new BigDecimal(value); - } + @Test + void should_reject_level3_before_requesting_when_credentials_are_absent() { + KrakenAPI unit = new KrakenAPI(null, requester); - private static Map decode(String encoded) { - if (encoded == null || encoded.isEmpty()) return Map.of(); - return Arrays.stream(encoded.split("&")) - .map(entry -> entry.split("=", 2)) - .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), - entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + assertThatThrownBy(() -> unit.level3OrderBook("BTC/USD")) + .isInstanceOf(IllegalStateException.class).hasMessageContaining("Level3"); + verifyNoInteractions(requester); } - private static String fixture(String name) { - try (InputStream input = MarketDataTest.class.getResourceAsStream("/market/" + name + ".json")) { - assertNotNull(input, "Missing fixture " + name); - return new String(input.readAllBytes(), StandardCharsets.UTF_8); - } - catch (IOException e) { - throw new AssertionError(e); - } - } + @Test + void should_reject_custom_level3_before_requesting_when_credentials_are_absent() { + KrakenAPI unit = new KrakenAPI(null, requester); + Level3OrderBookParams params = Level3OrderBookParams.builder().pair("BTC/USD").depth(10).build(); - private static T read(Endpoint endpoint, String json) { - try { - KrakenResponse response = MAPPER.readValue(json, endpoint.wrappedResponseType(MAPPER.getTypeFactory())); - return response.result().orElseThrow(() -> new KrakenException(response.error())); - } - catch (IOException e) { - throw new AssertionError(e); - } + assertThatThrownBy(() -> unit.level3OrderBook(params)) + .isInstanceOf(IllegalStateException.class).hasMessageContaining("Level3"); + verifyNoInteractions(requester); } - private static final class FixtureRequester implements KrakenRestRequester { - private final String json; - private Endpoint endpoint; - private KrakenCredentials credentials; - private KrakenNonceGenerator nonceGenerator; - private String postBody; - - private FixtureRequester(String json) { - this.json = json; - } - - @Override - public T execute(PublicEndpoint endpoint) { - this.endpoint = endpoint; - return read(endpoint, json); - } - - @Override - public T execute(PrivateEndpoint endpoint, KrakenCredentials credentials, KrakenNonceGenerator nonceGenerator) { - this.endpoint = endpoint; - this.credentials = credentials; - this.nonceGenerator = nonceGenerator; - this.postBody = endpoint.encodedParamsWith(nonceGenerator.generate()); - return read(endpoint, json); - } + @Test + void should_propagate_kraken_error_when_requester_rejects_pair() { + KrakenAPI unit = new KrakenAPI(null, requester); + KrakenException error = new KrakenException(List.of("EQuery:Unknown asset pair")); + when(requester.execute(any(OhlcEndpoint.class))).thenThrow(error); + + assertThatThrownBy(() -> unit.ohlc("BTC/USD")).isSameAs(error); + verify(requester).execute(any(OhlcEndpoint.class)); } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java new file mode 100644 index 0000000..dbd1950 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java @@ -0,0 +1,151 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.OhlcParams; +import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; + +@ExtendWith(MockitoExtension.class) +class OhlcEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + OhlcEndpoint unit = new OhlcEndpoint(OhlcParams.builder().pair("BTC/USD").interval(60).since(1688671200L).assetVersion(1).assetClass("tokenized_asset").build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(url.getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/OHLC"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "interval", "60", "since", "1688671200", "assetVersion", "1", "asset_class", "tokenized_asset")); + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("OHLC"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + OhlcEndpoint unit = new OhlcEndpoint("BTC/USD"); + + URL result = unit.buildURL(); + + assertThat(result).hasParameter("pair", "BTC/USD"); + assertThat(result.getQuery()).isEqualTo("pair=BTC%2FUSD"); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + OhlcParams.OhlcParamsBuilder unit = OhlcParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_retain_all_candle_positions_and_committed_cursor_when_decoding_documented_response() throws Exception { + OhlcEndpoint unit = new OhlcEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/ohlc.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OhlcData result = response.result().orElseThrow(); + + assertThat(result.candles().get("XXBTZUSD")).hasSize(2).first().isEqualTo( + new OhlcData.Candle(1688671200, new BigDecimal("30306.1"), new BigDecimal("30306.2"), new BigDecimal("30305.7"), + new BigDecimal("30305.7"), new BigDecimal("30306.1"), new BigDecimal("3.39243896"), 23)); + assertThat(result.candles().get("XXBTZUSD").getLast().time()).isEqualTo(1688671260L); + assertThat(result.last()).isEqualTo(1688672160L); + assertThat(OhlcParams.builder().pair("BTC/USD").since(result.last()).build().toMap()).containsEntry("since", "1688672160"); + } + + @Test + void should_retain_display_pair_keys_when_asset_version_is_enabled() throws Exception { + OhlcEndpoint unit = new OhlcEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/ohlc.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("XXBTZUSD", "BTC/USD"); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OhlcData result = response.result().orElseThrow(); + + assertThat(result.candles()).containsOnlyKeys("BTC/USD"); + } + + @Test + void should_separate_cursor_from_pair_entries_when_cursor_precedes_multiple_pairs() throws Exception { + OhlcEndpoint unit = new OhlcEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"last":123,"BTC/USD":[],"ETH/USD":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OhlcData result = response.result().orElseThrow(); + + assertThat(result.candles()).containsExactlyInAnyOrderEntriesOf(Map.of("BTC/USD", List.of(), "ETH/USD", List.of())); + assertThat(result.last()).isEqualTo(123L); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + OhlcEndpoint unit = new OhlcEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"BTC/USD":[],"last":123}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + OhlcData result = response.result().orElseThrow(); + + assertThat(result.candles().get("BTC/USD")).isEmpty(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java new file mode 100644 index 0000000..351902e --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java @@ -0,0 +1,130 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.OrderBookParams; +import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; + +@ExtendWith(MockitoExtension.class) +class OrderBookEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + OrderBookEndpoint unit = new OrderBookEndpoint(OrderBookParams.builder().pair("BTC/USD").count(500).assetVersion(1).assetClass("tokenized_asset").build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(url.getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Depth"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "count", "500", "assetVersion", "1", "asset_class", "tokenized_asset")); + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Depth"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + OrderBookEndpoint unit = new OrderBookEndpoint("BTC/USD"); + + URL result = unit.buildURL(); + + assertThat(result).hasParameter("pair", "BTC/USD"); + assertThat(result.getQuery()).isEqualTo("pair=BTC%2FUSD"); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + OrderBookParams.OrderBookParamsBuilder unit = OrderBookParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_read_levels_and_ignore_unknown_properties_when_decoding_documented_response() throws Exception { + OrderBookEndpoint unit = new OrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/depth.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = response.result().orElseThrow(); + + assertThat(result.get("XXBTZUSD").asks()).hasSize(2).first() + .isEqualTo(new OrderBook.Level(new BigDecimal("30384.10000"), new BigDecimal("2.059"), 1688671659)); + assertThat(result.get("XXBTZUSD").bids()).hasSize(2).first() + .isEqualTo(new OrderBook.Level(new BigDecimal("30297.00000"), new BigDecimal("1.115"), 1688671636)); + } + + @Test + void should_retain_display_pair_keys_when_asset_version_is_enabled() throws Exception { + OrderBookEndpoint unit = new OrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/depth.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("XXBTZUSD", "BTC/USD"); + } + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = response.result().orElseThrow(); + + assertThat(result).containsOnlyKeys("BTC/USD"); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + OrderBookEndpoint unit = new OrderBookEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"BTC/USD":{"asks":[],"bids":[]}}} + """; + + KrakenResponse> response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + Map result = response.result().orElseThrow(); + + assertThat(result.get("BTC/USD").asks()).isEmpty(); + assertThat(result.get("BTC/USD").bids()).isEmpty(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java new file mode 100644 index 0000000..b354e14 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java @@ -0,0 +1,149 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.RecentSpreadsParams; +import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; + +@ExtendWith(MockitoExtension.class) +class RecentSpreadsEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint(RecentSpreadsParams.builder().pair("BTC/USD").since(1688672106L).assetVersion(1).assetClass("tokenized_asset").build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(url.getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Spread"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "since", "1688672106", "assetVersion", "1", "asset_class", "tokenized_asset")); + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Spread"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint("BTC/USD"); + + URL result = unit.buildURL(); + + assertThat(result).hasParameter("pair", "BTC/USD"); + assertThat(result.getQuery()).isEqualTo("pair=BTC%2FUSD"); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + RecentSpreadsParams.RecentSpreadsParamsBuilder unit = RecentSpreadsParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_retain_spread_positions_and_cursor_when_decoding_documented_response() throws Exception { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/spread.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentSpreads result = response.result().orElseThrow(); + + assertThat(result.spreads().get("XXBTZUSD")).hasSize(2).first() + .isEqualTo(new RecentSpreads.Spread(1688671834, new BigDecimal("30292.10000"), new BigDecimal("30297.50000"))); + assertThat(result.last()).isEqualTo(1688672106L); + assertThat(RecentSpreadsParams.builder().pair("BTC/USD").since(result.last()).build().toMap()).containsEntry("since", "1688672106"); + } + + @Test + void should_retain_display_pair_keys_when_asset_version_is_enabled() throws Exception { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/spread.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("XXBTZUSD", "BTC/USD"); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentSpreads result = response.result().orElseThrow(); + + assertThat(result.spreads()).containsOnlyKeys("BTC/USD"); + } + + @Test + void should_separate_cursor_from_pair_entries_when_cursor_precedes_multiple_pairs() throws Exception { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"last":123,"BTC/USD":[],"ETH/USD":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentSpreads result = response.result().orElseThrow(); + + assertThat(result.spreads()).containsExactlyInAnyOrderEntriesOf(Map.of("BTC/USD", List.of(), "ETH/USD", List.of())); + assertThat(result.last()).isEqualTo(123L); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + RecentSpreadsEndpoint unit = new RecentSpreadsEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"BTC/USD":[],"last":123}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentSpreads result = response.result().orElseThrow(); + + assertThat(result.spreads().get("BTC/USD")).isEmpty(); + } +} diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java new file mode 100644 index 0000000..78b997b --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java @@ -0,0 +1,194 @@ +package dev.andstuff.kraken.api.endpoint.market; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import dev.andstuff.kraken.api.KrakenAPI; +import dev.andstuff.kraken.api.endpoint.KrakenResponse; +import dev.andstuff.kraken.api.endpoint.market.params.RecentTradesParams; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; + +@ExtendWith(MockitoExtension.class) +class RecentTradesEndpointTest { + + @Test + void should_encode_all_documented_fields_when_all_options_are_set() { + RecentTradesEndpoint unit = new RecentTradesEndpoint(RecentTradesParams.builder().pair("BTC/USD").since("1688671969993150842").count(2).assetVersion(1).assetClass("tokenized_asset").build()); + + URL url = unit.buildURL(); + Map parameters = Arrays.stream(url.getQuery().split("&")) + .map(entry -> entry.split("=", 2)) + .collect(Collectors.toMap(entry -> URLDecoder.decode(entry[0], StandardCharsets.UTF_8), + entry -> URLDecoder.decode(entry[1], StandardCharsets.UTF_8))); + + assertThat(unit.getHttpMethod()).isEqualTo("GET"); + assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Trades"); + assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "since", "1688671969993150842", "count", "2", "assetVersion", "1", "asset_class", "tokenized_asset")); + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Trades"); + } + + @Test + void should_omit_optional_parameters_when_only_the_pair_is_provided() { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + + URL result = unit.buildURL(); + + assertThat(result).hasParameter("pair", "BTC/USD"); + assertThat(result.getQuery()).isEqualTo("pair=BTC%2FUSD"); + } + + @Test + void should_reject_missing_pair_when_building_parameters() { + RecentTradesParams.RecentTradesParamsBuilder unit = RecentTradesParams.builder(); + + assertThatThrownBy(unit::build).isInstanceOf(NullPointerException.class).hasMessageContaining("pair"); + } + + @Test + void should_retain_trade_codes_decimal_time_and_opaque_cursor_when_decoding_documented_response() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/trades.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades().get("XXBTZUSD")).hasSize(2).first().isEqualTo( + new RecentTrades.Trade(new BigDecimal("30243.40000"), new BigDecimal("0.34507674"), new BigDecimal("1688669597.8277369"), + RecentTrades.Side.BUY, RecentTrades.OrderType.MARKET, "", 61044952)); + assertThat(result.trades().get("XXBTZUSD").getLast()).extracting(RecentTrades.Trade::side, RecentTrades.Trade::orderType) + .containsExactly(RecentTrades.Side.SELL, RecentTrades.OrderType.LIMIT); + assertThat(result.last()).isEqualTo("1688671969993150842"); + assertThat(RecentTradesParams.builder().pair("BTC/USD").since(result.last()).build().toMap()).containsEntry("since", result.last()); + } + + @Test + void should_retain_display_pair_keys_when_asset_version_is_enabled() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/trades.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("XXBTZUSD", "BTC/USD"); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades()).containsOnlyKeys("BTC/USD"); + } + + @Test + void should_separate_cursor_from_pair_entries_when_cursor_precedes_multiple_pairs() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"last":"001234567890123456789","BTC/USD":[],"ETH/USD":[]}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades()).containsExactlyInAnyOrderEntriesOf(Map.of("BTC/USD", List.of(), "ETH/USD", List.of())); + assertThat(result.last()).isEqualTo("001234567890123456789"); + } + + @Test + void should_return_empty_collections_when_no_entries_are_available() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json = """ + {"error":[],"result":{"BTC/USD":[],"last":"001234567890123456789"}} + """; + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades().get("BTC/USD")).isEmpty(); + } + + @Test + void should_preserve_fractional_precision_and_large_trade_ids_when_decoding_numeric_values() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/trades.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("1688669597.8277369", "1688669597.123456789").replace("61044952", "61044952000000001"); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades().get("XXBTZUSD").getFirst()).extracting(RecentTrades.Trade::time, RecentTrades.Trade::tradeId) + .containsExactly(new BigDecimal("1688669597.123456789"), 61044952000000001L); + } + + @Test + void should_fall_back_to_unknown_when_trade_codes_are_unrecognized() throws Exception { + RecentTradesEndpoint unit = new RecentTradesEndpoint("BTC/USD"); + ObjectMapper 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(); + String json; + try (InputStream fixture = getClass().getResourceAsStream("/market/trades.json")) { + json = new String(fixture.readAllBytes(), StandardCharsets.UTF_8).replace("\"b\"", "\"future-side\"").replace("\"m\"", "\"future-type\""); + } + + KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); + RecentTrades result = response.result().orElseThrow(); + + assertThat(result.trades().get("XXBTZUSD").getFirst()).extracting(RecentTrades.Trade::side, RecentTrades.Trade::orderType) + .containsExactly(RecentTrades.Side.UNKNOWN, RecentTrades.OrderType.UNKNOWN); + } +} diff --git a/library/src/test/resources/market/README.md b/library/src/test/resources/market/README.md index c0d478d..09fab25 100644 --- a/library/src/test/resources/market/README.md +++ b/library/src/test/resources/market/README.md @@ -1,7 +1,3 @@ -Fixtures are representative excerpts from the response examples in Kraken's -[Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), -retrieved 2026-09-07. Order books and time series are shortened. `depth.json` -includes an extra property to check forward-compatible deserialization. +Fixtures are representative excerpts from the response examples in Kraken's [Spot REST OpenAPI specification](https://docs.kraken.com/openapi/spot-rest.yaml), retrieved 2026-09-07. Order books and time series are shortened. `depth.json` includes an extra property to check forward-compatible deserialization. -Tests also generate empty responses, display-name keys, future enum values, -and extra precision cases from these fixtures. No test calls the Kraken API. +Tests also generate empty responses, display-name keys, future enum values, and extra precision cases from these fixtures. No test calls the Kraken API. diff --git a/pom.xml b/pom.xml index 18f3d14..b7d6779 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,8 @@ 2.23.0 5.12.0 5.14.0 + 3.27.7 + 5.23.0 3.15.0 From b722a1c8bfa04e9782e4fe00e38af77409b0aa92 Mon Sep 17 00:00:00 2001 From: nyg Date: Tue, 8 Sep 2026 07:26:01 +0200 Subject: [PATCH 3/4] docs: restore Javadoc for public Market Data API Document client-facing types, constructors, methods, parameters, response records, and enums in the existing library style. Keep implementation helpers and tests free of new comments. --- .../dev/andstuff/kraken/api/KrakenAPI.java | 92 +++++++++++++++++++ .../market/GroupedOrderBookEndpoint.java | 13 +++ .../market/Level3OrderBookEndpoint.java | 13 +++ .../market/MaintenanceScheduleEndpoint.java | 6 ++ .../api/endpoint/market/OhlcEndpoint.java | 13 +++ .../endpoint/market/OrderBookEndpoint.java | 13 +++ .../market/RecentSpreadsEndpoint.java | 13 +++ .../endpoint/market/RecentTradesEndpoint.java | 13 +++ .../market/params/GroupedOrderBookParams.java | 15 +++ .../market/params/Level3OrderBookParams.java | 9 ++ .../endpoint/market/params/OhlcParams.java | 21 +++++ .../market/params/OrderBookParams.java | 18 ++++ .../market/params/RecentSpreadsParams.java | 18 ++++ .../market/params/RecentTradesParams.java | 21 +++++ .../market/response/GroupedOrderBook.java | 14 +++ .../market/response/Level3OrderBook.java | 15 +++ .../market/response/MaintenanceSchedule.java | 32 +++++++ .../endpoint/market/response/OhlcData.java | 18 ++++ .../endpoint/market/response/OrderBook.java | 13 +++ .../market/response/RecentSpreads.java | 13 +++ .../market/response/RecentTrades.java | 23 +++++ 21 files changed, 406 insertions(+) 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 6004b62..58395c4 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java +++ b/library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java @@ -262,46 +262,122 @@ public Map ticker(List pairs) { return query(new TickerEndpoint(pairs)); } + /** + * Queries the {@code OHLC} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the candles by returned pair name and the cursor for committed updates + * @throws KrakenException if Kraken returns an error + */ public OhlcData ohlc(String pair) { return query(new OhlcEndpoint(pair)); } + /** + * Queries the {@code OHLC} endpoint. + * + * @param params the request parameters + * @return the candles by returned pair name and the cursor for committed updates + * @throws KrakenException if Kraken returns an error + */ public OhlcData ohlc(OhlcParams params) { return query(new OhlcEndpoint(params)); } + /** + * Queries the {@code Depth} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the L2 order books by returned pair name + * @throws KrakenException if Kraken returns an error + */ public Map orderBook(String pair) { return query(new OrderBookEndpoint(pair)); } + /** + * Queries the {@code Depth} endpoint. + * + * @param params the request parameters + * @return the L2 order books by returned pair name + * @throws KrakenException if Kraken returns an error + */ public Map orderBook(OrderBookParams params) { return query(new OrderBookEndpoint(params)); } + /** + * Queries the {@code Trades} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the trades by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ public RecentTrades recentTrades(String pair) { return query(new RecentTradesEndpoint(pair)); } + /** + * Queries the {@code Trades} endpoint. + * + * @param params the request parameters + * @return the trades by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ public RecentTrades recentTrades(RecentTradesParams params) { return query(new RecentTradesEndpoint(params)); } + /** + * Queries the {@code Spread} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the spreads by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ public RecentSpreads recentSpreads(String pair) { return query(new RecentSpreadsEndpoint(pair)); } + /** + * Queries the {@code Spread} endpoint. + * + * @param params the request parameters + * @return the spreads by returned pair name and the next polling cursor + * @throws KrakenException if Kraken returns an error + */ public RecentSpreads recentSpreads(RecentSpreadsParams params) { return query(new RecentSpreadsEndpoint(params)); } + /** + * Queries the {@code GroupedBook} endpoint using Kraken's default options. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the grouped bids and asks, pair and grouping value + * @throws KrakenException if Kraken returns an error + */ public GroupedOrderBook groupedOrderBook(String pair) { return query(new GroupedOrderBookEndpoint(pair)); } + /** + * Queries the {@code GroupedBook} endpoint. + * + * @param params the request parameters + * @return the grouped bids and asks, pair and grouping value + * @throws KrakenException if Kraken returns an error + */ public GroupedOrderBook groupedOrderBook(GroupedOrderBookParams params) { return query(new GroupedOrderBookEndpoint(params)); } + /** + * Queries the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. + * + * @return the maintenance schedule + * @throws KrakenException if Kraken returns an error + */ public MaintenanceSchedule maintenanceSchedule() { return query(new MaintenanceScheduleEndpoint()); } @@ -341,10 +417,26 @@ public PostTrade postTrade(PostTradeParams params) { /* Implemented private endpoints */ + /** + * Queries the {@code Level3} endpoint using Kraken's default options. Requires the Orders and trades - Query open orders & trades API key permission. + * + * @param pair the asset pair to query, e.g. {@code BTC/USD} + * @return the individual bid and ask orders with IDs and nanosecond timestamps + * @throws KrakenException if Kraken returns an error + * @throws IllegalStateException if credentials are missing + */ public Level3OrderBook level3OrderBook(String pair) { return query(new Level3OrderBookEndpoint(pair)); } + /** + * Queries the {@code Level3} endpoint. Requires the Orders and trades - Query open orders & trades API key permission. + * + * @param params the request parameters + * @return the individual bid and ask orders with IDs and nanosecond timestamps + * @throws KrakenException if Kraken returns an error + * @throws IllegalStateException if credentials are missing + */ public Level3OrderBook level3OrderBook(Level3OrderBookParams params) { return query(new Level3OrderBookEndpoint(params)); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java index b17dfea..7b4b8ac 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpoint.java @@ -6,12 +6,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.GroupedOrderBook; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code GroupedBook} endpoint, returning bid and ask quantities aggregated over grouped price levels. + */ public class GroupedOrderBookEndpoint extends PublicEndpoint { + /** + * Creates the {@code GroupedBook} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public GroupedOrderBookEndpoint(String pair) { this(GroupedOrderBookParams.builder().pair(pair).build()); } + /** + * Creates the {@code GroupedBook} endpoint. + * + * @param params the request parameters + */ public GroupedOrderBookEndpoint(GroupedOrderBookParams params) { super("GroupedBook", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java index db0de89..735846b 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpoint.java @@ -6,12 +6,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.Level3OrderBook; import dev.andstuff.kraken.api.endpoint.priv.PrivateEndpoint; +/** + * The private {@code Level3} endpoint, requiring the Orders and trades - Query open orders & trades API key permission. + */ public class Level3OrderBookEndpoint extends PrivateEndpoint { + /** + * Creates the {@code Level3} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public Level3OrderBookEndpoint(String pair) { this(Level3OrderBookParams.builder().pair(pair).build()); } + /** + * Creates the {@code Level3} endpoint. + * + * @param params the request parameters + */ public Level3OrderBookEndpoint(Level3OrderBookParams params) { super("Level3", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java index 67011e4..458e99e 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpoint.java @@ -5,8 +5,14 @@ import dev.andstuff.kraken.api.endpoint.market.response.MaintenanceSchedule; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code MaintenanceSchedule} endpoint, returning scheduled maintenance events in the next seven days. + */ public class MaintenanceScheduleEndpoint extends PublicEndpoint { + /** + * Creates the {@code MaintenanceSchedule} endpoint for scheduled events in the next seven days. + */ public MaintenanceScheduleEndpoint() { super("MaintenanceSchedule", new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java index 393f982..e0bac12 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpoint.java @@ -6,12 +6,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.OhlcData; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code OHLC} endpoint, returning candles by asset pair and a cursor for committed updates. + */ public class OhlcEndpoint extends PublicEndpoint { + /** + * Creates the {@code OHLC} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public OhlcEndpoint(String pair) { this(OhlcParams.builder().pair(pair).build()); } + /** + * Creates the {@code OHLC} endpoint. + * + * @param params the request parameters + */ public OhlcEndpoint(OhlcParams params) { super("OHLC", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java index 8b6c6f0..8543828 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpoint.java @@ -8,12 +8,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.OrderBook; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code Depth} endpoint, returning aggregated L2 price levels by asset pair. + */ public class OrderBookEndpoint extends PublicEndpoint> { + /** + * Creates the {@code Depth} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public OrderBookEndpoint(String pair) { this(OrderBookParams.builder().pair(pair).build()); } + /** + * Creates the {@code Depth} endpoint. + * + * @param params the request parameters + */ public OrderBookEndpoint(OrderBookParams params) { super("Depth", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java index e9b9a9a..3128534 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpoint.java @@ -6,12 +6,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.RecentSpreads; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code Spread} endpoint, returning recent top-of-book spreads by asset pair and a polling cursor. + */ public class RecentSpreadsEndpoint extends PublicEndpoint { + /** + * Creates the {@code Spread} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public RecentSpreadsEndpoint(String pair) { this(RecentSpreadsParams.builder().pair(pair).build()); } + /** + * Creates the {@code Spread} endpoint. + * + * @param params the request parameters + */ public RecentSpreadsEndpoint(RecentSpreadsParams params) { super("Spread", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java index 3d29d7f..027c5d5 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpoint.java @@ -6,12 +6,25 @@ import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; import dev.andstuff.kraken.api.endpoint.pub.PublicEndpoint; +/** + * The public {@code Trades} endpoint, returning recent trades by asset pair and a polling cursor. + */ public class RecentTradesEndpoint extends PublicEndpoint { + /** + * Creates the {@code Trades} endpoint using Kraken's default options. + * + * @param pair the asset pair to query + */ public RecentTradesEndpoint(String pair) { this(RecentTradesParams.builder().pair(pair).build()); } + /** + * Creates the {@code Trades} endpoint. + * + * @param params the request parameters + */ public RecentTradesEndpoint(RecentTradesParams params) { super("Trades", params, new TypeReference<>() {}); } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java index dba5fae..dd84e48 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/GroupedOrderBookParams.java @@ -10,17 +10,32 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code GroupedBook} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class GroupedOrderBookParams implements QueryParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Price levels per side: 10, 25, 100, 250 or 1000; defaults to 10. + */ private final Integer depth; + /** + * Ticks per price level: 1, 5, 10, 25, 50, 100, 250, 500 or 1000; defaults to 1. + */ private final Integer grouping; + /** + * {@inheritDoc} + */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java index cd68a23..b16b361 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java @@ -8,13 +8,22 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code Level3} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class Level3OrderBookParams extends PostParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Price levels per side: 0 (full book), 10, 25, 100, 250 or 1000; defaults to 100. + */ private final Integer depth; @Override diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java index f831df3..602ea66 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OhlcParams.java @@ -10,21 +10,42 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code OHLC} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class OhlcParams implements QueryParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Candle interval in minutes: 1, 5, 15, 30, 60, 240, 1440, 10080 or 21600; defaults to 1. + */ private final Integer interval; + /** + * Unix timestamp in seconds, or the last cursor from the previous OHLC response. + */ private final Long since; + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ private final Integer assetVersion; + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ private final String assetClass; + /** + * {@inheritDoc} + */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java index f02f242..54903e2 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/OrderBookParams.java @@ -10,19 +10,37 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code Depth} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class OrderBookParams implements QueryParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Maximum entries per side, from 1 to 500; defaults to 100. + */ private final Integer count; + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ private final Integer assetVersion; + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ private final String assetClass; + /** + * {@inheritDoc} + */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java index 5a7de2d..7fd5a64 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentSpreadsParams.java @@ -10,19 +10,37 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code Spread} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class RecentSpreadsParams implements QueryParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Unix timestamp in seconds, or the last cursor from the previous Spread response. + */ private final Long since; + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ private final Integer assetVersion; + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ private final String assetClass; + /** + * {@inheritDoc} + */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java index 09c2202..4d65a89 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/RecentTradesParams.java @@ -10,21 +10,42 @@ import lombok.Getter; import lombok.NonNull; +/** + * The parameters of the {@code Trades} endpoint; unset options use Kraken's defaults. + */ @Getter @Builder(toBuilder = true) public class RecentTradesParams implements QueryParams { + /** + * The asset pair to query, e.g. {@code BTC/USD}. + */ @NonNull private final String pair; + /** + * Timestamp or opaque last cursor from the previous Trades response, preserved verbatim. + */ private final String since; + /** + * Maximum number of trades, from 1 to 1000; defaults to 1000. + */ private final Integer count; + /** + * Use 1 for display pair names in the response; omitted for internal names. + */ private final Integer assetVersion; + /** + * Use tokenized_asset for non-crypto pairs such as xStocks. + */ private final String assetClass; + /** + * {@inheritDoc} + */ @Override public Map toMap() { Map params = new HashMap<>(); diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java index c3c16d3..9ae4099 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/GroupedOrderBook.java @@ -5,7 +5,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; +/** + * The {@code GroupedBook} response, with asks rounded up and bids down to grouped price levels. + * + * @param pair asset pair + * @param grouping ticks per price level used by Kraken + * @param bids aggregated bid levels + * @param asks aggregated ask levels + */ public record GroupedOrderBook(String pair, int grouping, List bids, List asks) { + /** + * A grouped price level returned by the {@code GroupedBook} endpoint. + * + * @param price grouped price + * @param quantity aggregated quantity at this price + */ public record Level(BigDecimal price, @JsonProperty("qty") BigDecimal quantity) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java index 42cb6c8..8477b81 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java @@ -5,8 +5,23 @@ import com.fasterxml.jackson.annotation.JsonProperty; +/** + * The {@code Level3} response, containing individual orders in the book. + * + * @param pair asset pair + * @param bids bid orders in the order returned by Kraken + * @param asks ask orders in the order returned by Kraken + */ public record Level3OrderBook(String pair, List bids, List asks) { + /** + * An individual order returned by the {@code Level3} endpoint. + * + * @param price order price + * @param quantity order quantity + * @param orderId order identifier + * @param timestamp Unix timestamp in nanoseconds + */ public record Order(BigDecimal price, @JsonProperty("qty") BigDecimal quantity, @JsonProperty("order_id") String orderId, long timestamp) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java index cafe6a6..1c89fee 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/MaintenanceSchedule.java @@ -6,8 +6,28 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonProperty; +/** + * The {@code MaintenanceSchedule} response, covering the next seven days. + * + * @param events scheduled events ordered by expected start time, empty when none are scheduled + */ public record MaintenanceSchedule(List events) { + /** + * A scheduled maintenance event returned by the {@code MaintenanceSchedule} endpoint. + * + * @param eventId stable event identifier + * @param title event title + * @param expectedStart expected start time + * @param expectedEnd expected end time + * @param timeToStart seconds until the start, evaluated when Kraken served the response + * @param phase approach phase + * @param affectedServices affected Kraken services + * @param orderSubmission guidance about submitting orders + * @param recommendedAction suggested client action + * @param cancelBefore cancellation deadline, absent when not scheduled + * @param sourceUrl link to the event on Kraken's status page + */ public record Event(@JsonProperty("event_id") long eventId, String title, @JsonProperty("expected_start_utc") Instant expectedStart, @@ -20,22 +40,34 @@ public record Event(@JsonProperty("event_id") long eventId, @JsonProperty("cancel_before_utc") Instant cancelBefore, @JsonProperty("source_url") String sourceUrl) {} + /** + * The approach phase in a {@code MaintenanceSchedule} event. + */ public enum Phase { ANNOUNCED, REMINDER_24H, APPROACHING_30M, IMMINENT_5M, FINAL_WARNING_30S, @JsonEnumDefaultValue UNKNOWN } + /** + * A Kraken service affected by a {@code MaintenanceSchedule} event. + */ public enum Service { SPOT_WS, SPOT_REST, SPOT_FIX, SPOT_TRADING, FUTURES_WS, FUTURES_REST, FUTURES_FIX, FUTURES_TRADING, ALL, @JsonEnumDefaultValue UNKNOWN } + /** + * Order submission guidance in a {@code MaintenanceSchedule} event. + */ public enum OrderSubmission { ALLOWED, DISCOURAGED, BLOCKED, @JsonEnumDefaultValue UNKNOWN } + /** + * Suggested client action in a {@code MaintenanceSchedule} event. + */ public enum RecommendedAction { CONTINUE, REDUCE_ACTIVITY, CANCEL_OPEN_ORDERS, STOP_NEW_ORDERS, @JsonEnumDefaultValue UNKNOWN diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java index 799a31a..02f3b71 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java @@ -12,9 +12,27 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +/** + * The {@code OHLC} response containing OHLC candles, including the final, uncommitted candle. At most 720 entries are available. + * + * @param candles entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for committed candles + */ @JsonDeserialize(builder = OhlcData.ResponseBuilder.class) public record OhlcData(Map> candles, Long last) { + /** + * A positional OHLC candle returned by the {@code OHLC} endpoint. + * + * @param time candle start, in Unix seconds + * @param open opening price + * @param high highest price + * @param low lowest price + * @param close closing price + * @param vwap volume weighted average price + * @param volume traded volume + * @param count number of trades + */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "open", "high", "low", "close", "vwap", "volume", "count"}) public record Candle(long time, BigDecimal open, BigDecimal high, BigDecimal low, diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java index 5daf303..f1614c2 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java @@ -6,8 +6,21 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +/** + * A pair's aggregated L2 order book from the {@code Depth} endpoint. + * + * @param asks ask levels in the order returned by Kraken + * @param bids bid levels in the order returned by Kraken + */ public record OrderBook(List asks, List bids) { + /** + * An aggregated price level returned by the {@code Depth} endpoint. + * + * @param price price of this level + * @param volume aggregated volume at this price + * @param time Unix timestamp in seconds + */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time"}) public record Level(BigDecimal price, BigDecimal volume, long time) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java index 1fa91c6..4d960ef 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java @@ -12,9 +12,22 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +/** + * The {@code Spread} response containing recent top-of-book spreads; Kraken retains approximately 200 entries. + * + * @param spreads entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for new data + */ @JsonDeserialize(builder = RecentSpreads.ResponseBuilder.class) public record RecentSpreads(Map> spreads, Long last) { + /** + * A positional top-of-book spread returned by the {@code Spread} endpoint. + * + * @param time Unix timestamp in seconds + * @param bid best bid price + * @param ask best ask price + */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "bid", "ask"}) public record Spread(long time, BigDecimal bid, BigDecimal ask) {} diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java index 0e2e737..8a3205c 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java @@ -13,20 +13,43 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +/** + * The {@code Trades} response containing recent trades; by default Kraken returns the last 1000 trades. + * + * @param trades entries by Kraken's returned pair name (internal or display name) + * @param last cursor to pass unchanged as {@code since} when polling for new data + */ @JsonDeserialize(builder = RecentTrades.ResponseBuilder.class) public record RecentTrades(Map> trades, String last) { + /** + * A positional trade returned by the {@code Trades} endpoint. + * + * @param price trade price + * @param volume traded volume + * @param time Unix timestamp in seconds, retaining fractional precision + * @param side buy or sell + * @param orderType market or limit + * @param miscellaneous additional trade information + * @param tradeId trade identifier + */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time", "side", "orderType", "miscellaneous", "tradeId"}) public record Trade(BigDecimal price, BigDecimal volume, BigDecimal time, Side side, OrderType orderType, String miscellaneous, long tradeId) {} + /** + * The buy/sell code in a {@code Trades} row. + */ public enum Side { @JsonProperty("b") BUY, @JsonProperty("s") SELL, @JsonEnumDefaultValue UNKNOWN } + /** + * The market/limit code in a {@code Trades} row. + */ public enum OrderType { @JsonProperty("m") MARKET, @JsonProperty("l") LIMIT, From 3548d5e34a537d7450586dedf4ef7fc4207635a8 Mon Sep 17 00:00:00 2001 From: nyg Date: Thu, 10 Sep 2026 07:35:35 +0200 Subject: [PATCH 4/4] fix(market): address coverage review findings --- AGENTS.md | 2 +- README.md | 4 +- library/pom.xml | 3 -- .../market/params/Level3OrderBookParams.java | 2 +- .../market/response/Level3OrderBook.java | 18 ++++++- .../endpoint/market/response/OhlcData.java | 5 +- .../endpoint/market/response/OrderBook.java | 5 +- .../market/response/RecentSpreads.java | 5 +- .../market/response/RecentTrades.java | 5 +- .../api/rest/DefaultKrakenRestRequester.java | 21 ++++++-- .../market/GroupedOrderBookEndpointTest.java | 6 ++- .../market/Level3OrderBookEndpointTest.java | 13 +++-- .../MaintenanceScheduleEndpointTest.java | 1 - .../api/endpoint/market/OhlcEndpointTest.java | 10 ++-- .../market/OrderBookEndpointTest.java | 11 +++-- .../market/RecentSpreadsEndpointTest.java | 8 ++- .../market/RecentTradesEndpointTest.java | 10 ++-- .../DefaultKrakenRestRequesterMarketTest.java | 49 +++++++++++++++++++ pom.xml | 17 +++++++ 19 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterMarketTest.java diff --git a/AGENTS.md b/AGENTS.md index b253fa2..a5d597b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `< `KrakenAPI` is the entry point: typed methods for implemented endpoints, generic `query()` methods taking a `Public`/`Private` enum value and returning a `JsonNode`, and raw `queryPublic()`/`queryPrivate()` taking a path string. -Every endpoint extends `Endpoint`, either `PublicEndpoint` (GET on `/0/public/{path}`, parameters from `QueryParams`) or `PrivateEndpoint` (POST on `/0/private/{path}`, parameters from `PostParams`, signed with a nonce-based HMAC). Concrete endpoints live in a domain package under `endpoint/` — `market/` for public market data, `account/` for private account data, `subaccount/` for subaccount management, `transparency/` for public pre- and post-trade data, `earn/` for earn strategies and allocations — and follow `{Name}Endpoint`, `params/{Name}Params`, `response/{ResponseType}`. +Every endpoint extends `Endpoint`, either `PublicEndpoint` (GET on `/0/public/{path}`, parameters from `QueryParams`) or `PrivateEndpoint` (POST on `/0/private/{path}`, parameters from `PostParams`, signed with a nonce-based HMAC). Concrete endpoints live in a domain package under `endpoint/` — `market/` for market data, including authenticated Level3, `account/` for private account data, `subaccount/` for subaccount management, `transparency/` for public pre- and post-trade data, `earn/` for earn strategies and allocations — and follow `{Name}Endpoint`, `params/{Name}Params`, `response/{ResponseType}`. `KrakenRestRequester` performs the HTTP calls and can be swapped for another HTTP client. Responses are unwrapped from the Kraken `{error, result}` envelope by `KrakenResponse`; ZIP responses (report exports) go through `Endpoint.processZipResponse()`. diff --git a/README.md b/README.md index 3336366..65e4dfd 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,9 @@ The required `pair` field selects one asset pair. Optional numeric fields accept | `GroupedBook` | `grouping` | 1, 5, 10, 25, 50, 100, 250, 500, 1000 ticks per level | 1 | | `Level3` | `depth` | 0 (full book), 10, 25, 100, 250, 1000 levels per side | 100 | -OHLC candle times, L2 level times, spread times, and the `since` fields for OHLC and spreads use Unix seconds. Grouped books round asks up and bids down to the nearest grouped price level. `MaintenanceSchedule` returns scheduled events for the next seven days, ordered by expected start time; its times use `Instant`, and `cancelBefore` can be absent. +OHLC candle times, L2 level times and spread times use `Instant`; the `since` fields for OHLC and spreads remain Unix seconds. Grouped books round asks up and bids down to the nearest grouped price level. `MaintenanceSchedule` returns scheduled events for the next seven days, ordered by expected start time; its times use `Instant`, and `cancelBefore` can be absent. -OHLC includes a final candle that is still forming and retains at most 720 entries. Reuse its `last()` cursor as `since` to poll for committed updates. Trade cursors are opaque strings: pass `last()` unchanged. Prices and quantities use `BigDecimal`; trade times retain fractional Unix seconds, while Level3 timestamps are Unix nanoseconds. +OHLC includes a final candle that is still forming and retains at most 720 entries. Reuse its `last()` cursor as `since` to poll for committed updates. Trade cursors are opaque strings: pass `last()` unchanged. Prices and quantities use `BigDecimal`; trade and Level3 times use `Instant`, retaining nanosecond precision. Level3 decodes Kraken's integer epoch nanoseconds explicitly. Level3 requires credentials with **Orders and trades – Query open orders & trades** permission: diff --git a/library/pom.xml b/library/pom.xml index f977d31..5700858 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -16,19 +16,16 @@ org.junit.jupiter junit-jupiter - ${junit.version} test org.assertj assertj-core - ${assertj.version} test org.mockito mockito-junit-jupiter - ${mockito.version} test diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java index b16b361..4ff8d3b 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/params/Level3OrderBookParams.java @@ -30,7 +30,7 @@ public class Level3OrderBookParams extends PostParams { protected Map params() { Map params = new HashMap<>(); params.put("pair", pair); - putIfNonNull(params, "depth", depth, String::valueOf); + putIfNonNull(params, "depth", depth); return params; } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java index 8477b81..e2b7703 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/Level3OrderBook.java @@ -1,9 +1,15 @@ package dev.andstuff.kraken.api.endpoint.market.response; +import java.io.IOException; import java.math.BigDecimal; +import java.time.Instant; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; /** * The {@code Level3} response, containing individual orders in the book. @@ -20,8 +26,16 @@ public record Level3OrderBook(String pair, List bids, List asks) { * @param price order price * @param quantity order quantity * @param orderId order identifier - * @param timestamp Unix timestamp in nanoseconds + * @param timestamp order instant, decoded from epoch nanoseconds */ public record Order(BigDecimal price, @JsonProperty("qty") BigDecimal quantity, - @JsonProperty("order_id") String orderId, long timestamp) {} + @JsonProperty("order_id") String orderId, @JsonDeserialize(using = NanosecondsDeserializer.class) Instant timestamp) {} + + static class NanosecondsDeserializer extends JsonDeserializer { + + @Override + public Instant deserialize(JsonParser parser, DeserializationContext context) throws IOException { + return Instant.ofEpochSecond(0, parser.getLongValue()); + } + } } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java index 02f3b71..6e1f9a8 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OhlcData.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.market.response; import java.math.BigDecimal; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,7 +25,7 @@ public record OhlcData(Map> candles, Long last) { /** * A positional OHLC candle returned by the {@code OHLC} endpoint. * - * @param time candle start, in Unix seconds + * @param time candle start instant * @param open opening price * @param high highest price * @param low lowest price @@ -35,7 +36,7 @@ public record OhlcData(Map> candles, Long last) { */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "open", "high", "low", "close", "vwap", "volume", "count"}) - public record Candle(long time, BigDecimal open, BigDecimal high, BigDecimal low, + public record Candle(Instant time, BigDecimal open, BigDecimal high, BigDecimal low, BigDecimal close, BigDecimal vwap, BigDecimal volume, long count) {} @JsonPOJOBuilder(withPrefix = "") diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java index f1614c2..87b8648 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/OrderBook.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.market.response; import java.math.BigDecimal; +import java.time.Instant; import java.util.List; import com.fasterxml.jackson.annotation.JsonFormat; @@ -19,9 +20,9 @@ public record OrderBook(List asks, List bids) { * * @param price price of this level * @param volume aggregated volume at this price - * @param time Unix timestamp in seconds + * @param time timestamp as an instant */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time"}) - public record Level(BigDecimal price, BigDecimal volume, long time) {} + public record Level(BigDecimal price, BigDecimal volume, Instant time) {} } diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java index 4d960ef..f6e5879 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentSpreads.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.market.response; import java.math.BigDecimal; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,13 +25,13 @@ public record RecentSpreads(Map> spreads, Lon /** * A positional top-of-book spread returned by the {@code Spread} endpoint. * - * @param time Unix timestamp in seconds + * @param time timestamp as an instant * @param bid best bid price * @param ask best ask price */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"time", "bid", "ask"}) - public record Spread(long time, BigDecimal bid, BigDecimal ask) {} + public record Spread(Instant time, BigDecimal bid, BigDecimal ask) {} @JsonPOJOBuilder(withPrefix = "") static class ResponseBuilder { diff --git a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java index 8a3205c..0059bef 100644 --- a/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java +++ b/library/src/main/java/dev/andstuff/kraken/api/endpoint/market/response/RecentTrades.java @@ -1,6 +1,7 @@ package dev.andstuff.kraken.api.endpoint.market.response; import java.math.BigDecimal; +import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,7 +28,7 @@ public record RecentTrades(Map> trades, String * * @param price trade price * @param volume traded volume - * @param time Unix timestamp in seconds, retaining fractional precision + * @param time trade instant, retaining nanosecond precision * @param side buy or sell * @param orderType market or limit * @param miscellaneous additional trade information @@ -35,7 +36,7 @@ public record RecentTrades(Map> trades, String */ @JsonFormat(shape = JsonFormat.Shape.ARRAY) @JsonPropertyOrder({"price", "volume", "time", "side", "orderType", "miscellaneous", "tradeId"}) - public record Trade(BigDecimal price, BigDecimal volume, BigDecimal time, Side side, + public record Trade(BigDecimal price, BigDecimal volume, Instant time, Side side, OrderType orderType, String miscellaneous, long tradeId) {} /** 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 efc4f55..5fbd8de 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 @@ -2,8 +2,8 @@ import java.io.IOException; import java.io.OutputStreamWriter; +import java.net.URL; import java.util.zip.ZipInputStream; - import javax.net.ssl.HttpsURLConnection; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -40,6 +40,21 @@ public class DefaultKrakenRestRequester implements KrakenRestRequester { .build(); } + private final ConnectionFactory connectionFactory; + + public DefaultKrakenRestRequester() { + this(url -> (HttpsURLConnection) url.openConnection()); + } + + DefaultKrakenRestRequester(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @FunctionalInterface + interface ConnectionFactory { + HttpsURLConnection open(URL url) throws IOException; + } + /** * {@inheritDoc} * @@ -85,8 +100,8 @@ public T execute(PrivateEndpoint endpoint, KrakenCredentials credentials, } } - private static HttpsURLConnection createHttpsConnection(Endpoint endpoint) throws IOException { - HttpsURLConnection connection = (HttpsURLConnection) endpoint.buildURL().openConnection(); + private HttpsURLConnection createHttpsConnection(Endpoint endpoint) throws IOException { + HttpsURLConnection connection = connectionFactory.open(endpoint.buildURL()); connection.setRequestMethod(endpoint.getHttpMethod()); connection.addRequestProperty("User-Agent", "github.com/nyg/kraken-api-java"); return connection; diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java index 8512125..6380fbc 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/GroupedOrderBookEndpointTest.java @@ -9,7 +9,6 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -45,7 +44,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("GET"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/GroupedBook"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "depth", "25", "grouping", "1000")); - assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("GroupedBook"); } @Test @@ -108,4 +106,8 @@ void should_return_empty_collections_when_no_entries_are_available() throws Exce assertThat(result.asks()).isEmpty(); assertThat(result.bids()).isEmpty(); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("GroupedBook"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java index 10f98ff..bd79f44 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/Level3OrderBookEndpointTest.java @@ -8,8 +8,8 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -45,7 +45,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("POST"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/private/Level3"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "YFI/EUR", "depth", "0", "nonce", "123")); - assertThat(List.of(KrakenAPI.Private.LEVEL3)).extracting(KrakenAPI.Private::getPath).contains("Level3"); } @Test @@ -86,10 +85,10 @@ void should_retain_individual_orders_and_nanosecond_timestamps_when_decoding_doc assertThat(result.pair()).isEqualTo("YFI/EUR"); assertThat(result.bids()).hasSize(2).first().isEqualTo( - new Level3OrderBook.Order(new BigDecimal("3062.00000"), new BigDecimal("0.29665800"), "O5KJU4-IEQTM-NDMS6W", 1765622008594292000L)); + new Level3OrderBook.Order(new BigDecimal("3062.00000"), new BigDecimal("0.29665800"), "O5KJU4-IEQTM-NDMS6W", Instant.ofEpochSecond(0, 1765622008594292000L))); assertThat(result.bids().getLast().orderId()).isNotEqualTo(result.bids().getFirst().orderId()); assertThat(result.asks().getFirst().quantity()).isEqualTo(new BigDecimal("0.00278335")); - assertThat(result.asks().getLast().timestamp()).isEqualTo(1765622021013826600L); + assertThat(result.asks().getLast().timestamp()).isEqualTo(Instant.ofEpochSecond(0, 1765622021013826600L)); } @Test @@ -102,7 +101,7 @@ void should_return_empty_collections_when_no_entries_are_available() throws Exce .addModules(new JavaTimeModule(), new Jdk8Module()) .build(); String json = """ - {"error":[],"result":{"pair":"BTC/USD","grouping":1,"asks":[],"bids":[]}} + {"error":[],"result":{"pair":"BTC/USD","asks":[],"bids":[]}} """; KrakenResponse response = mapper.readValue(json, unit.wrappedResponseType(mapper.getTypeFactory())); @@ -124,4 +123,8 @@ void should_replace_previous_nonce_when_reusing_endpoint() { assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "YFI/EUR", "nonce", "124")); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Private.values())).extracting(KrakenAPI.Private::getPath).contains("Level3"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java index 8cbcb9e..f011f4b 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/MaintenanceScheduleEndpointTest.java @@ -7,7 +7,6 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java index dbd1950..536d90b 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OhlcEndpointTest.java @@ -8,6 +8,7 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -45,7 +46,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("GET"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/OHLC"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "interval", "60", "since", "1688671200", "assetVersion", "1", "asset_class", "tokenized_asset")); - assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("OHLC"); } @Test @@ -83,9 +83,9 @@ void should_retain_all_candle_positions_and_committed_cursor_when_decoding_docum OhlcData result = response.result().orElseThrow(); assertThat(result.candles().get("XXBTZUSD")).hasSize(2).first().isEqualTo( - new OhlcData.Candle(1688671200, new BigDecimal("30306.1"), new BigDecimal("30306.2"), new BigDecimal("30305.7"), + new OhlcData.Candle(Instant.ofEpochSecond(1688671200), new BigDecimal("30306.1"), new BigDecimal("30306.2"), new BigDecimal("30305.7"), new BigDecimal("30305.7"), new BigDecimal("30306.1"), new BigDecimal("3.39243896"), 23)); - assertThat(result.candles().get("XXBTZUSD").getLast().time()).isEqualTo(1688671260L); + assertThat(result.candles().get("XXBTZUSD").getLast().time()).isEqualTo(Instant.ofEpochSecond(1688671260L)); assertThat(result.last()).isEqualTo(1688672160L); assertThat(OhlcParams.builder().pair("BTC/USD").since(result.last()).build().toMap()).containsEntry("since", "1688672160"); } @@ -148,4 +148,8 @@ void should_return_empty_collections_when_no_entries_are_available() throws Exce assertThat(result.candles().get("BTC/USD")).isEmpty(); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("OHLC"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java index 351902e..3b24de1 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/OrderBookEndpointTest.java @@ -8,8 +8,8 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -45,7 +45,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("GET"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Depth"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "count", "500", "assetVersion", "1", "asset_class", "tokenized_asset")); - assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Depth"); } @Test @@ -83,9 +82,9 @@ void should_read_levels_and_ignore_unknown_properties_when_decoding_documented_r Map result = response.result().orElseThrow(); assertThat(result.get("XXBTZUSD").asks()).hasSize(2).first() - .isEqualTo(new OrderBook.Level(new BigDecimal("30384.10000"), new BigDecimal("2.059"), 1688671659)); + .isEqualTo(new OrderBook.Level(new BigDecimal("30384.10000"), new BigDecimal("2.059"), Instant.ofEpochSecond(1688671659))); assertThat(result.get("XXBTZUSD").bids()).hasSize(2).first() - .isEqualTo(new OrderBook.Level(new BigDecimal("30297.00000"), new BigDecimal("1.115"), 1688671636)); + .isEqualTo(new OrderBook.Level(new BigDecimal("30297.00000"), new BigDecimal("1.115"), Instant.ofEpochSecond(1688671636))); } @Test @@ -127,4 +126,8 @@ void should_return_empty_collections_when_no_entries_are_available() throws Exce assertThat(result.get("BTC/USD").asks()).isEmpty(); assertThat(result.get("BTC/USD").bids()).isEmpty(); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Depth"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java index b354e14..bc2d808 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentSpreadsEndpointTest.java @@ -8,6 +8,7 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -45,7 +46,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("GET"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Spread"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "since", "1688672106", "assetVersion", "1", "asset_class", "tokenized_asset")); - assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Spread"); } @Test @@ -83,7 +83,7 @@ void should_retain_spread_positions_and_cursor_when_decoding_documented_response RecentSpreads result = response.result().orElseThrow(); assertThat(result.spreads().get("XXBTZUSD")).hasSize(2).first() - .isEqualTo(new RecentSpreads.Spread(1688671834, new BigDecimal("30292.10000"), new BigDecimal("30297.50000"))); + .isEqualTo(new RecentSpreads.Spread(Instant.ofEpochSecond(1688671834), new BigDecimal("30292.10000"), new BigDecimal("30297.50000"))); assertThat(result.last()).isEqualTo(1688672106L); assertThat(RecentSpreadsParams.builder().pair("BTC/USD").since(result.last()).build().toMap()).containsEntry("since", "1688672106"); } @@ -146,4 +146,8 @@ void should_return_empty_collections_when_no_entries_are_available() throws Exce assertThat(result.spreads().get("BTC/USD")).isEmpty(); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Spread"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java index 78b997b..49b951b 100644 --- a/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java +++ b/library/src/test/java/dev/andstuff/kraken/api/endpoint/market/RecentTradesEndpointTest.java @@ -8,6 +8,7 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -45,7 +46,6 @@ void should_encode_all_documented_fields_when_all_options_are_set() { assertThat(unit.getHttpMethod()).isEqualTo("GET"); assertThat(url).hasProtocol("https").hasHost("api.kraken.com").hasPath("/0/public/Trades"); assertThat(parameters).containsExactlyInAnyOrderEntriesOf(Map.of("pair", "BTC/USD", "since", "1688671969993150842", "count", "2", "assetVersion", "1", "asset_class", "tokenized_asset")); - assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Trades"); } @Test @@ -83,7 +83,7 @@ void should_retain_trade_codes_decimal_time_and_opaque_cursor_when_decoding_docu RecentTrades result = response.result().orElseThrow(); assertThat(result.trades().get("XXBTZUSD")).hasSize(2).first().isEqualTo( - new RecentTrades.Trade(new BigDecimal("30243.40000"), new BigDecimal("0.34507674"), new BigDecimal("1688669597.8277369"), + new RecentTrades.Trade(new BigDecimal("30243.40000"), new BigDecimal("0.34507674"), Instant.ofEpochSecond(1688669597L, 827736900L), RecentTrades.Side.BUY, RecentTrades.OrderType.MARKET, "", 61044952)); assertThat(result.trades().get("XXBTZUSD").getLast()).extracting(RecentTrades.Trade::side, RecentTrades.Trade::orderType) .containsExactly(RecentTrades.Side.SELL, RecentTrades.OrderType.LIMIT); @@ -168,7 +168,7 @@ void should_preserve_fractional_precision_and_large_trade_ids_when_decoding_nume RecentTrades result = response.result().orElseThrow(); assertThat(result.trades().get("XXBTZUSD").getFirst()).extracting(RecentTrades.Trade::time, RecentTrades.Trade::tradeId) - .containsExactly(new BigDecimal("1688669597.123456789"), 61044952000000001L); + .containsExactly(Instant.ofEpochSecond(1688669597L, 123456789L), 61044952000000001L); } @Test @@ -191,4 +191,8 @@ void should_fall_back_to_unknown_when_trade_codes_are_unrecognized() throws Exce assertThat(result.trades().get("XXBTZUSD").getFirst()).extracting(RecentTrades.Trade::side, RecentTrades.Trade::orderType) .containsExactly(RecentTrades.Side.UNKNOWN, RecentTrades.OrderType.UNKNOWN); } + @Test + void should_expose_endpoint_path_when_listing_api_endpoints() { + assertThat(Arrays.asList(KrakenAPI.Public.values())).extracting(KrakenAPI.Public::getPath).contains("Trades"); + } } diff --git a/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterMarketTest.java b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterMarketTest.java new file mode 100644 index 0000000..6a0cfd1 --- /dev/null +++ b/library/src/test/java/dev/andstuff/kraken/api/rest/DefaultKrakenRestRequesterMarketTest.java @@ -0,0 +1,49 @@ +package dev.andstuff.kraken.api.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import javax.net.ssl.HttpsURLConnection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import dev.andstuff.kraken.api.endpoint.market.RecentTradesEndpoint; +import dev.andstuff.kraken.api.endpoint.market.response.RecentTrades; + +@ExtendWith(MockitoExtension.class) +class DefaultKrakenRestRequesterMarketTest { + + @Mock private DefaultKrakenRestRequester.ConnectionFactory connectionFactory; + @Mock private HttpsURLConnection connection; + + @Test + void should_decode_precise_timestamps_and_future_enums_when_using_production_mapper() throws Exception { + DefaultKrakenRestRequester unit = new DefaultKrakenRestRequester(connectionFactory); + RecentTradesEndpoint endpoint = new RecentTradesEndpoint("BTC/USD"); + when(connectionFactory.open(any(URL.class))).thenReturn(connection); + when(connection.getHeaderField("Content-Type")).thenReturn("application/json"); + String json = """ + {"error":[],"result":{"BTC/USD":[["0.0000000000123456789","0.1",1688669597.123456789,"future-side","m","",61044952000000001]],"last":"opaque-cursor"}} + """; + when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))); + + RecentTrades result = unit.execute(endpoint); + + assertThat(result.last()).isEqualTo("opaque-cursor"); + assertThat(result.trades().get("BTC/USD")).singleElement().satisfies(trade -> { + assertThat(trade.time()).isEqualTo(Instant.ofEpochSecond(1688669597L, 123456789L)); + assertThat(trade.price()).isEqualByComparingTo("0.0000000000123456789"); + assertThat(trade.side()).isEqualTo(RecentTrades.Side.UNKNOWN); + assertThat(trade.orderType()).isEqualTo(RecentTrades.OrderType.MARKET); + assertThat(trade.tradeId()).isEqualTo(61044952000000001L); + }); + } +} diff --git a/pom.xml b/pom.xml index b7d6779..4b6a361 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,23 @@ opencsv ${opencsv.version} + + org.junit + junit-bom + ${junit.version} + import + pom + + + org.assertj + assertj-core + ${assertj.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} +