Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions library/src/main/java/dev/andstuff/kraken/api/KrakenAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ public ServerTime serverTime() {
}

/**
* Queries the {@code SystemStatus} endpoint, returning the current status of the Kraken trading system.
* Queries the {@code SystemStatus} endpoint, returning the current status of the Kraken trading system, along with scheduled maintenance and unresolved incidents.
*
* @return the system status
* @throws KrakenException if Kraken returns an error
Expand Down Expand Up @@ -532,6 +532,16 @@ public PreTrade preTrade(String symbol) {
return query(new PreTradeEndpoint(PreTradeParams.of(symbol)));
}

/**
* Queries the {@code PostTrade} endpoint, returning the last 1000 trades executed on the spot exchange, all pairs included.
*
* @return the executed trades
* @throws KrakenException if Kraken returns an error
*/
public PostTrade postTrade() {
return postTrade(PostTradeParams.builder().build());
}

/**
* Queries the {@code PostTrade} endpoint, returning the last 1000 trades executed on a currency pair.
*
Expand All @@ -544,9 +554,9 @@ public PostTrade postTrade(String symbol) {
}

/**
* Queries the {@code PostTrade} endpoint, returning the trades executed on a currency pair over the given period. Trades are returned in ascending time order and at most 1000 at a time: {@link PostTrade#lastTimestamp()} gives the timestamp to use as the next {@code fromTimestamp}.
* Queries the {@code PostTrade} endpoint, returning the trades matching the given symbol, period and count. Trades are returned in ascending time order and at most 1000 at a time: {@link PostTrade#lastTimestamp()} gives the timestamp to use as the next {@code fromTimestamp}.
*
* @param params the currency pair and the period and count restricting the trades returned
* @param params the currency pair, period and count restricting the trades returned
* @return the executed trades
* @throws KrakenException if Kraken returns an error
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Map;

import dev.andstuff.kraken.api.endpoint.priv.PostParams;
import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier;
import lombok.Builder;
import lombok.Getter;

Expand All @@ -18,12 +19,14 @@ public class LedgerEntriesParams extends PostParams {
@Builder.Default
private final List<String> entryIds = List.of();
private final boolean includeTrades;
private final RebaseMultiplier rebaseMultiplier;

@Override
public Map<String, String> params() {
Map<String, String> params = new HashMap<>();
putIfNonNull(params, "id", entryIds, v -> String.join(",", v));
putIfNonNull(params, "trades", includeTrades);
putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
return params;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;

import dev.andstuff.kraken.api.endpoint.priv.PostParams;
import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier;
import lombok.Builder;
import lombok.Getter;
import lombok.With;
Expand All @@ -27,6 +28,7 @@ public class LedgerInfoParams extends PostParams {
private final String fromLedgerId;
private final String toLedgerId;
private final boolean withoutCount;
private final RebaseMultiplier rebaseMultiplier;

@With
@Builder.Default
Expand Down Expand Up @@ -55,6 +57,7 @@ protected Map<String, String> params() {

putIfNonNull(params, "without_count", withoutCount);
putIfNonNull(params, "ofs", resultOffset);
putIfNonNull(params, "rebase_multiplier", rebaseMultiplier, RebaseMultiplier::getValue);
return params;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public record MaintenanceSchedule(List<Event> events) {

/**
* A scheduled maintenance event returned by the {@code MaintenanceSchedule} endpoint.
* A scheduled maintenance event returned by the {@code MaintenanceSchedule} and {@code SystemStatus} endpoints.
*
* @param eventId stable event identifier
* @param title event title
Expand Down Expand Up @@ -49,7 +49,7 @@ public enum Phase {
}

/**
* A Kraken service affected by a {@code MaintenanceSchedule} event.
* A Kraken service affected by a scheduled maintenance event or an incident.
*/
public enum Service {
SPOT_WS, SPOT_REST, SPOT_FIX, SPOT_TRADING,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
package dev.andstuff.kraken.api.endpoint.market.response;

import java.time.Instant;
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* The response of the {@code SystemStatus} endpoint.
* The response of the {@code SystemStatus} endpoint. The advisories explain why a trading mode is in effect or coming, but the trading mode itself is only given by {@code status}.
*
* @param status the current status of the Kraken trading system
* @param timestamp the time the status was last updated
* @param upcomingMaintenance the maintenance events scheduled within the next 72 hours, by ascending start time, empty if there are none
* @param emergency the unresolved incidents, empty if there are none
*/
public record SystemStatus(Description status,
Instant timestamp) {
Instant timestamp,
@JsonProperty("upcoming_maintenance") List<MaintenanceSchedule.Event> upcomingMaintenance,
List<Emergency> emergency) {

/**
* Creates the response, replacing advisory lists Kraken omits with empty ones.
*
* @param status the current status of the Kraken trading system
* @param timestamp the time the status was last updated
* @param upcomingMaintenance the scheduled maintenance events, possibly {@code null}
* @param emergency the unresolved incidents, possibly {@code null}
*/
public SystemStatus {
upcomingMaintenance = Objects.requireNonNullElse(upcomingMaintenance, List.of());
emergency = Objects.requireNonNullElse(emergency, List.of());
}

/**
* The trading mode of the Kraken trading system.
Expand All @@ -25,4 +45,61 @@ public enum Description {
@JsonEnumDefaultValue
UNKNOWN
}

/**
* An unplanned incident, relayed from Kraken's status page until it is resolved.
*
* @param eventId stable incident identifier
* @param title incident title
* @param incidentStatus lifecycle state of the incident
* @param impact severity of the incident
* @param affectedServices affected Kraken services
* @param startedAt time the incident was opened
* @param nextSteps forecast operational actions, by ascending time, empty if none was published
* @param sourceUrl link to the incident on Kraken's status page
*/
public record Emergency(@JsonProperty("event_id") long eventId,
String title,
@JsonProperty("incident_status") IncidentStatus incidentStatus,
Impact impact,
@JsonProperty("affected_services") List<MaintenanceSchedule.Service> affectedServices,
@JsonProperty("started_at_utc") Instant startedAt,
@JsonProperty("next_steps") List<NextStep> nextSteps,
@JsonProperty("source_url") String sourceUrl) {}

/**
* An operational action Kraken forecasts during an incident.
*
* @param appliesTo Kraken services the action applies to
* @param type nature of the action
* @param expectedAt forecast time of the action
*/
public record NextStep(@JsonProperty("applies_to") List<MaintenanceSchedule.Service> appliesTo,
Type type,
@JsonProperty("expected_at_utc") Instant expectedAt) {

/**
* The nature of a forecast action.
*/
public enum Type {
EXPECTED_RESTART, EXPECTED_CANCEL_ONLY, EXPECTED_POST_ONLY, EXPECTED_ONLINE,
@JsonEnumDefaultValue UNKNOWN
}
}

/**
* The lifecycle state of an incident.
*/
public enum IncidentStatus {
INVESTIGATING, IDENTIFIED, MONITORING,
@JsonEnumDefaultValue UNKNOWN
}

/**
* The severity of an incident, as published on Kraken's status page.
*/
public enum Impact {
NONE, MINOR, MAJOR, CRITICAL,
@JsonEnumDefaultValue UNKNOWN
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,23 @@
import dev.andstuff.kraken.api.endpoint.pub.QueryParams;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
* The parameters of the {@code PostTrade} endpoint. The symbol is required, in the {@code BASE/QUOTE} display format, and the trades can be further restricted to a period and to a maximum count.
* The parameters of the {@code PostTrade} endpoint. All of them are optional: the trades can be restricted to a symbol, in the {@code BASE/QUOTE} display format, to a period and to a maximum count. Without any of them, Kraken returns the last 1000 trades of all pairs.
*/
@Getter
@Builder(toBuilder = true)
public class PostTradeParams implements QueryParams {

@NonNull
private final String symbol;

private final Instant fromTimestamp;
private final Instant toTimestamp;
private final Integer count;

@Override
public Map<String, String> toMap() {
Map<String, String> params = new HashMap<>();
params.put("symbol", symbol);
putIfNonNull(params, "symbol", symbol, v -> v);
putIfNonNull(params, "from_ts", fromTimestamp, Instant::toString);
putIfNonNull(params, "to_ts", toTimestamp, Instant::toString);
putIfNonNull(params, "count", count, String::valueOf);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
/**
* {@link KrakenRestRequester} implementation using {@link HttpsURLConnection}.
*
* <p>JSON responses are deserialized with a Jackson mapper configured to be lenient with unknown properties and enum values, so that new fields returned by Kraken don't break deserialization. Responses of type {@code application/zip}, e.g. report exports, are handed to {@link Endpoint#processZipResponse(java.util.zip.ZipInputStream)}. Funding (Beta) responses are deserialized from the whole body, and their HTTP error statuses are raised as a {@link KrakenException}.
* <p>JSON responses are deserialized with a Jackson mapper configured to be lenient with unknown properties and enum values, so that new fields returned by Kraken don't break deserialization. Responses of type {@code application/zip} or {@code application/octet-stream}, e.g. report exports, are handed to {@link Endpoint#processZipResponse(java.util.zip.ZipInputStream)}. Funding (Beta) responses are deserialized from the whole body, and their HTTP error statuses are raised as a {@link KrakenException}.
*/
@Slf4j
public class DefaultKrakenRestRequester implements KrakenRestRequester {
Expand Down Expand Up @@ -153,7 +153,7 @@ private static <T> T parseResponse(HttpsURLConnection connection, Endpoint<T> en
KrakenResponse<T> response = OBJECT_MAPPER.readValue(connection.getInputStream(), krakenResponseType);
return endpoint.unwrapResponse(response);
}
else if ("application/zip".equals(contentType)) {
else if ("application/zip".equals(contentType) || "application/octet-stream".equals(contentType)) {
try (ZipInputStream zipStream = new ZipInputStream(connection.getInputStream())) {
return endpoint.processZipResponse(zipStream);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ void should_route_postTrade_symbol_without_credentials_when_called() {
verify(requester).execute(argThat((PostTradeEndpoint endpoint) -> endpoint.buildURL().getQuery().equals("symbol=BTC%2FUSD")));
}

@Test
void should_route_postTrade_of_all_pairs_without_credentials_when_called() {
PostTrade postTradeResponse = new PostTrade(null, 0, List.of());
KrakenAPI unit = new KrakenAPI(null, requester);
when(requester.execute(any(PostTradeEndpoint.class))).thenReturn(postTradeResponse);

PostTrade result = unit.postTrade();

assertThat(result).isSameAs(postTradeResponse);
verify(requester).execute(argThat((PostTradeEndpoint endpoint) -> endpoint.buildURL().getQuery() == null));
}

@Test
void should_route_postTrade_options_without_credentials_when_called() {
PostTrade postTradeResponse = new PostTrade(Instant.parse("2024-05-30T12:34:56.123456789Z"), 0, List.of());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@
import dev.andstuff.kraken.api.endpoint.KrakenResponse;
import dev.andstuff.kraken.api.endpoint.account.params.LedgerEntriesParams;
import dev.andstuff.kraken.api.endpoint.account.response.LedgerEntry;
import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier;

@ExtendWith(MockitoExtension.class)
class LedgerEntriesEndpointTest {

@Test
void should_encode_all_options_when_supplied() {
LedgerEntriesEndpoint unit = new LedgerEntriesEndpoint(LedgerEntriesParams.builder()
.entryIds(List.of("L4UESK-KG3EQ-UFO4T5", "LMKZCZ-Z3GVL-CXKK4H")).includeTrades(true).build());
.entryIds(List.of("L4UESK-KG3EQ-UFO4T5", "LMKZCZ-Z3GVL-CXKK4H")).includeTrades(true).rebaseMultiplier(RebaseMultiplier.REBASED).build());

Map<String, String> result = Arrays.stream(unit.encodedParamsWith("123456789").split("&"))
.map(value -> value.split("=", 2))
Expand All @@ -41,7 +42,8 @@ void should_encode_all_options_when_supplied() {
assertThat(result).containsExactlyInAnyOrderEntriesOf(Map.ofEntries(
Map.entry("nonce", "123456789"),
Map.entry("id", "L4UESK-KG3EQ-UFO4T5,LMKZCZ-Z3GVL-CXKK4H"),
Map.entry("trades", "true")));
Map.entry("trades", "true"),
Map.entry("rebase_multiplier", "rebased")));
assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/QueryLedgers");
assertThat(unit.getHttpMethod()).isEqualTo("POST");
assertThat(unit.getContentType()).isEqualTo("application/x-www-form-urlencoded");
Expand All @@ -53,7 +55,7 @@ void should_exclude_trades_when_not_requested() {

String result = unit.encodedParamsWith("123");

assertThat(result).contains("trades=false").contains("id=L4UESK-KG3EQ-UFO4T5").endsWith("nonce=123");
assertThat(result).contains("trades=false").contains("id=L4UESK-KG3EQ-UFO4T5").doesNotContain("rebase_multiplier").endsWith("nonce=123");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import dev.andstuff.kraken.api.endpoint.account.params.LedgerInfoParams;
import dev.andstuff.kraken.api.endpoint.account.response.LedgerEntry;
import dev.andstuff.kraken.api.endpoint.account.response.LedgerInfo;
import dev.andstuff.kraken.api.endpoint.priv.RebaseMultiplier;

@ExtendWith(MockitoExtension.class)
class LedgerInfoEndpointTest {
Expand All @@ -36,7 +37,7 @@ class LedgerInfoEndpointTest {
void should_encode_dates_over_ledger_ids_when_both_bounds_are_supplied() {
LedgerInfoEndpoint unit = new LedgerInfoEndpoint(LedgerInfoParams.builder().assets(List.of("XXBT", "ZUSD")).assetClass("currency")
.assetType(LedgerInfoParams.Type.NFT_REBATE).fromDate(Instant.ofEpochSecond(1688444262L)).toDate(Instant.ofEpochSecond(1688464484L))
.fromLedgerId("LMKZCZ-Z3GVL-CXKK4H").toLedgerId("L4UESK-KG3EQ-UFO4T5").withoutCount(true).resultOffset(50).build());
.fromLedgerId("LMKZCZ-Z3GVL-CXKK4H").toLedgerId("L4UESK-KG3EQ-UFO4T5").withoutCount(true).resultOffset(50).rebaseMultiplier(RebaseMultiplier.BASE).build());

Map<String, String> result = Arrays.stream(unit.encodedParamsWith("123456789").split("&"))
.map(value -> value.split("=", 2))
Expand All @@ -50,7 +51,8 @@ void should_encode_dates_over_ledger_ids_when_both_bounds_are_supplied() {
Map.entry("start", "1688444262"),
Map.entry("end", "1688464484"),
Map.entry("without_count", "true"),
Map.entry("ofs", "50")));
Map.entry("ofs", "50"),
Map.entry("rebase_multiplier", "base")));
assertThat(unit.buildURL().getPath()).isEqualTo("/0/private/Ledgers");
assertThat(unit.getHttpMethod()).isEqualTo("POST");
assertThat(unit.getContentType()).isEqualTo("application/x-www-form-urlencoded");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ void should_route_server_time_through_configured_requester_when_querying_time()
@Test
void should_route_system_status_through_configured_requester_when_querying_status() {
KrakenAPI unit = new KrakenAPI(null, requester);
SystemStatus expected = new SystemStatus(SystemStatus.Description.ONLINE, Instant.parse("2023-07-06T18:52:00Z"));
SystemStatus expected = new SystemStatus(SystemStatus.Description.ONLINE, Instant.parse("2023-07-06T18:52:00Z"), List.of(), List.of());
when(requester.execute(any(SystemStatusEndpoint.class))).thenReturn(expected);

SystemStatus result = unit.systemStatus();
Expand Down
Loading
Loading