项目作者: brcolow

项目描述 :
Candle-stick Chart Library for JavaFX
高级语言: Java
项目地址: git://github.com/brcolow/candlefx.git
创建时间: 2020-10-03T23:15:28Z
项目社区:https://github.com/brcolow/candlefx

开源协议:MIT License

下载


CandleFX Library

CandleFX is a JavaFX library that provides a candle-stick chart implementation that supports incremental paging of data,
live syncing of real-time trading data, and tries hard to be responsive and snappy.

CandleFX screenshot

Caveat

This code was written about 5 years ago (circa 2015) for a project that never saw the light of day. I am ripping out
salvageable stand-alone parts in the hope that it benefits even one person (maybe they find this project through
the magic of search engines (including Github’s own)).

Getting Started

Simplest Possible Chart

CandleFX can display real-time candle-stick charts for trading commodities but let’s start, for simplicity’s sake, with
a candle-stick chart that only displays historical (past) trading data. The full source code for these examples can
be found in CandleStickChartExample.

In order to create a candle-stick chart we need the following objects:

  • An exchange instance
  • A tradePair on that exchange

An exchange object represents some trading facilitator of commodities. For example the New York Stock Exchange
which facilitates trading in certain stock trade pairs (such as Tesla’s stock to U.S. Dollars - the TSLA/USD trade pair)
or Coinbase exchange which facilitates trading of cryptocurrencies with other currencies (fiat or crypto) such as
the BTC/USD trade pair.

Exchange is an abstract class that should be implemented for your own needs. In these examples we will use Coinbase
Exchange. In order for the candle-stick chart to retrieve historical candle data you must, at a minimum, implement the
getCandleDataSupplier method. One way using the Coinbase exchange public API could be done like so:

  1. public class Coinbase extends Exchange {
  2. Coinbase(Set<TradePair> tradePairs, RecentTrades recentTrades) {
  3. super(null); // This argument is for creating a WebSocket client for live trading data.
  4. }
  5. @Override
  6. public CandleDataSupplier getCandleDataSupplier(int secondsPerCandle, TradePair tradePair) {
  7. return new CoinbaseCandleDataSupplier(secondsPerCandle, tradePair);
  8. }
  9. public static class CoinbaseCandleDataSupplier extends CandleDataSupplier {
  10. private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
  11. .registerModule(new JavaTimeModule())
  12. .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
  13. .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  14. private static final int EARLIEST_DATA = 1422144000; // roughly the first trade
  15. CoinbaseCandleDataSupplier(int secondsPerCandle, TradePair tradePair) {
  16. super(200, secondsPerCandle, tradePair, new SimpleIntegerProperty(-1));
  17. }
  18. @Override
  19. public Set<Integer> getSupportedGranularities() {
  20. return Set.of(60, 300, 900, 3600, 21600, 86400);
  21. }
  22. @Override
  23. public Future<List<CandleData>> get() {
  24. if (endTime.get() == -1) {
  25. endTime.set((int) (Instant.now().toEpochMilli() / 1000L));
  26. }
  27. String endDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME
  28. .format(LocalDateTime.ofEpochSecond(endTime.get(), 0, ZoneOffset.UTC));
  29. int startTime = Math.max(endTime.get() - (numCandles * secondsPerCandle), EARLIEST_DATA);
  30. String startDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME
  31. .format(LocalDateTime.ofEpochSecond(startTime, 0, ZoneOffset.UTC));
  32. String uriStr = "https://api.pro.coinbase.com/" +
  33. "products/" + tradePair.toString('-') + "/candles" +
  34. "?granularity=" + secondsPerCandle +
  35. "&start=" + startDateString +
  36. "&end=" + endDateString;
  37. if (startTime <= EARLIEST_DATA) {
  38. // signal more data is false
  39. return CompletableFuture.completedFuture(Collections.emptyList());
  40. }
  41. return HttpClient.newHttpClient().sendAsync(
  42. HttpRequest.newBuilder()
  43. .uri(URI.create(uriStr))
  44. .GET().build(),
  45. HttpResponse.BodyHandlers.ofString())
  46. .thenApply(HttpResponse::body)
  47. .thenApply(response -> {
  48. JsonNode res;
  49. try {
  50. res = OBJECT_MAPPER.readTree(response);
  51. } catch (JsonProcessingException ex) {
  52. throw new RuntimeException(ex);
  53. }
  54. if (!res.isEmpty()) {
  55. // Remove the current in-progress candle
  56. if (res.get(0).get(0).asInt() + secondsPerCandle > endTime.get()) {
  57. ((ArrayNode) res).remove(0);
  58. }
  59. endTime.set(startTime);
  60. List<CandleData> candleData = new ArrayList<>();
  61. for (JsonNode candle : res) {
  62. candleData.add(new CandleData(
  63. candle.get(3).asDouble(), // open price
  64. candle.get(4).asDouble(), // close price
  65. candle.get(2).asDouble(), // high price
  66. candle.get(1).asDouble(), // low price
  67. candle.get(0).asInt(), // open time
  68. candle.get(5).asDouble() // volume
  69. ));
  70. }
  71. candleData.sort(Comparator.comparingInt(CandleData::getOpenTime));
  72. return candleData;
  73. } else {
  74. return Collections.emptyList();
  75. }
  76. });
  77. }
  78. }
  79. }

Create a CandleStickChartContainer object. For this example we will use the Coinbase exchange implementation and
create a candle-stick chart for the BTC/USD trade pair.

  1. Exchange coinbase = new Coinbase();
  2. CandleStickChartContainer candleStickChartContainer =
  3. new CandleStickChartContainer(
  4. coinbase,
  5. TradePair.of(Currency.ofCrypto("BTC"), Currency.ofFiat("USD")));

Add the CandleStickChartContainer to a JavaFX layout:

  1. AnchorPane.setTopAnchor(candleStickChartContainer, 30.0);
  2. AnchorPane.setLeftAnchor(candleStickChartContainer, 30.0);
  3. AnchorPane.setRightAnchor(candleStickChartContainer, 30.0);
  4. AnchorPane.setBottomAnchor(candleStickChartContainer, 30.0);
  5. candleStickChartContainer.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
  6. Scene scene = new Scene(new AnchorPane(candleStickChartContainer), 1200, 800);

Enable Live Syncing to Create a Real-Time Chart

Now that we have constructed a simple chart that starts contains data from when the chart is created (and can go
backwards to the first trade of that tradepair on that exchange) we now want to look at creating a real-time chart
that updates as trades happen. This means that, if the most recent candle is in the view port, it will be redrawn
as trades happen and, once the current candle duration is over, the chart will add a new candle to the right
and begin syncing it with current trading activity.

In order to support live syncing mode we need to implement two additional methods of the Exchange class:

  1. @Override
  2. CompletableFuture<Optional<InProgressCandleData>> fetchCandleDataForInProgressCandle(
  3. TradePair tradePair,
  4. Instant currentCandleStartedAt,
  5. long secondsIntoCurrentCandle,
  6. int secondsPerCandle) {}
  7. @Override
  8. CompletableFuture<List<Trade>> fetchRecentTradesUntil(TradePair tradePair, Instant stopAt) {}

The first method fetches data using a “sub-candle method” (that is, fetching data for completed candles of a less
duration than the chart’s selected granularity. The second method is then used to fetch the raw, individual trades
for the duration between the last sub-candle (from the first method) and the current time. We go through the trouble
of having these two methods work in tandem (as opposed to only needing the second method) because it can take a
prohibitively long time to fetch the raw trade data in the candle duration is too large. An example of this would
be if the candle duration is one day the number of trades on an exchange in a single day could be in the millions.

Let’s implement the first method fetchCandleDataForInProgressCandle which uses the sub-candle strategy for syncing
with real-time data using the Coinbase API:

  1. @Override
  2. public CompletableFuture<Optional<InProgressCandleData>> fetchCandleDataForInProgressCandle(
  3. TradePair tradePair, Instant currentCandleStartedAt, long secondsIntoCurrentCandle, int secondsPerCandle) {
  4. String startDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.ofInstant(
  5. currentCandleStartedAt, ZoneOffset.UTC));
  6. // Compute the ideal sub-candle granularity.
  7. long idealGranularity = Math.max(10, secondsIntoCurrentCandle / 200);
  8. // Get the closest supported granularity to the ideal granularity.
  9. int actualGranularity = getCandleDataSupplier(secondsPerCandle, tradePair).getSupportedGranularities().stream()
  10. .min(Comparator.comparingInt(i -> (int) Math.abs(i - idealGranularity)))
  11. .orElseThrow(() -> new NoSuchElementException("Supported granularities was empty!"));
  12. // TODO: If actualGranularity = secondsPerCandle there are no sub-candles to fetch and we must get all the
  13. // data for the current live syncing candle from the raw trades method.
  14. return HttpClient.newHttpClient().sendAsync(
  15. HttpRequest.newBuilder()
  16. .uri(URI.create(String.format(
  17. "https://api.pro.coinbase.com/products/%s/candles?granularity=%s&start=%s",
  18. tradePair.toString('-'), actualGranularity, startDateString)))
  19. .GET().build(),
  20. HttpResponse.BodyHandlers.ofString())
  21. .thenApply(HttpResponse::body)
  22. .thenApply(response -> {
  23. logger.info("coinbase response: " + response);
  24. JsonNode res;
  25. try {
  26. res = OBJECT_MAPPER.readTree(response);
  27. } catch (JsonProcessingException ex) {
  28. throw new RuntimeException(ex);
  29. }
  30. if (res.isEmpty()) {
  31. return Optional.empty();
  32. }
  33. JsonNode currCandle;
  34. Iterator<JsonNode> candleItr = res.iterator();
  35. int currentTill = -1;
  36. double openPrice = -1;
  37. double highSoFar = -1;
  38. double lowSoFar = Double.MAX_VALUE;
  39. double volumeSoFar = 0;
  40. double lastTradePrice = -1;
  41. boolean foundFirst = false;
  42. while (candleItr.hasNext()) {
  43. currCandle = candleItr.next();
  44. if (currCandle.get(0).asInt() < currentCandleStartedAt.getEpochSecond() ||
  45. currCandle.get(0).asInt() >= currentCandleStartedAt.getEpochSecond() +
  46. secondsPerCandle) {
  47. // Skip this sub-candle if it is not in the parent candle's duration (this is just a
  48. // sanity guard).
  49. continue;
  50. } else {
  51. if (!foundFirst) {
  52. // FIXME: Why are we only using the first sub-candle here?
  53. // Unless foundFirst is actually the *last* (that is, most recent in time) sub-candle?
  54. currentTill = currCandle.get(0).asInt();
  55. lastTradePrice = currCandle.get(4).asDouble();
  56. foundFirst = true;
  57. }
  58. }
  59. openPrice = currCandle.get(3).asDouble();
  60. if (currCandle.get(2).asDouble() > highSoFar) {
  61. highSoFar = currCandle.get(2).asDouble();
  62. }
  63. if (currCandle.get(1).asDouble() < lowSoFar) {
  64. lowSoFar = currCandle.get(1).asDouble();
  65. }
  66. volumeSoFar += currCandle.get(5).asDouble();
  67. }
  68. int openTime = (int) (currentCandleStartedAt.toEpochMilli() / 1000L);
  69. return Optional.of(new InProgressCandleData(openTime, openPrice, highSoFar, lowSoFar,
  70. currentTill, lastTradePrice, volumeSoFar));
  71. });
  72. }

Now let’s implement the second method fetchRecentTradesUntil which uses the raw trade data strategy for syncing
with real-time data using the Coinbase API to fill in the missing data trades from the end of the last sub-candle (from
the first method):

  1. /**
  2. * Fetches the recent trades for the given trade pair from {@code stopAt} till now (the current time).
  3. * <p>
  4. * This method only needs to be implemented to support live syncing.
  5. */
  6. @Override
  7. public CompletableFuture<List<Trade>> fetchRecentTradesUntil(TradePair tradePair, Instant stopAt) {
  8. Objects.requireNonNull(tradePair);
  9. Objects.requireNonNull(stopAt);
  10. // We were asked to fetch data from the future but we don't have a time machine (yet).
  11. if (stopAt.isAfter(Instant.now())) {
  12. return CompletableFuture.completedFuture(Collections.emptyList());
  13. }
  14. CompletableFuture<List<Trade>> futureResult = new CompletableFuture<>();
  15. // It is not easy (possible?) to fetch trades concurrently because we need to get the "cb-after" header after each
  16. // request.
  17. CompletableFuture.runAsync(() -> {
  18. IntegerProperty afterCursor = new SimpleIntegerProperty(0);
  19. List<Trade> tradesBeforeStopTime = new ArrayList<>();
  20. for (int i = 0; !futureResult.isDone(); i++) {
  21. String uriStr = "https://api.pro.coinbase.com/";
  22. uriStr += "products/" + tradePair.toString('-') + "/trades";
  23. if (i != 0) {
  24. uriStr += "?after=" + afterCursor.get();
  25. }
  26. try {
  27. HttpResponse<String> response = HttpClient.newHttpClient().send(
  28. HttpRequest.newBuilder()
  29. .uri(URI.create(uriStr))
  30. .GET().build(),
  31. HttpResponse.BodyHandlers.ofString());
  32. if (response.headers().firstValue("cb-after").isEmpty()) {
  33. futureResult.completeExceptionally(new RuntimeException(
  34. "coinbase trades response did not contain header \"cb-after\": " + response));
  35. return;
  36. }
  37. afterCursor.setValue(Integer.valueOf((response.headers().firstValue("cb-after").get())));
  38. JsonNode tradesResponse = OBJECT_MAPPER.readTree(response.body());
  39. if (!tradesResponse.isArray()) {
  40. futureResult.completeExceptionally(new RuntimeException(
  41. "coinbase trades response was not an array!"));
  42. }
  43. if (tradesResponse.isEmpty()) {
  44. futureResult.completeExceptionally(new IllegalArgumentException("coinbase trades response was empty"));
  45. } else {
  46. for (int j = 0; j < tradesResponse.size(); j++) {
  47. JsonNode trade = tradesResponse.get(j);
  48. Instant time = Instant.from(ISO_INSTANT.parse(trade.get("time").asText()));
  49. if (time.compareTo(stopAt) <= 0) {
  50. // We have caught up with all trades until the requested stop time, so we are finished.
  51. futureResult.complete(tradesBeforeStopTime);
  52. break;
  53. } else {
  54. // Add this raw trade to the list.
  55. tradesBeforeStopTime.add(new Trade(tradePair,
  56. DefaultMoney.ofFiat(trade.get("price").asText(), tradePair.getCounterCurrency()),
  57. DefaultMoney.ofCrypto(trade.get("size").asText(), tradePair.getBaseCurrency()),
  58. Side.getSide(trade.get("side").asText()), trade.get("trade_id").asLong(), time));
  59. }
  60. }
  61. }
  62. } catch (IOException | InterruptedException ex) {
  63. logger.error("ex: ", ex);
  64. }
  65. }
  66. });
  67. return futureResult;
  68. }

Next we create a CandleStickChartContainer making sure to pass in true for the liveSyncing argument:

  1. Exchange coinbase = new Coinbase();
  2. CandleStickChartContainer candleStickChartContainer =
  3. new CandleStickChartContainer(
  4. coinbase,
  5. TradePair.of(Currency.ofCrypto("BTC"), Currency.ofFiat("USD")),
  6. true // Turn live-syncing on
  7. );

Attribution

CandleFX would not be possible without the following open source projects:

The FontAwesome icon set for the chart toolbar.

The PopOver and ToggleSwitch controls from ControlsFX.

The StableTicksAxis implementation from JFXUtils.

The FastMoney implementation from mikvor/money-conversion.

TODO

  • Flesh out a full README example.
  • Add examples from more cryptocurrency exchanges.
  • Add example using finnhub.io (https://finnhub.io/docs/api#stock-candles).
  • Create subpackages (monetary, controls, etc.) and have better separation of private/public API with help of JPMS.
  • Create websocket interface instead of having a strong tie to one websocket library so consumers can plug in their
    desired one.
  • Fix the ServiceLoaders for CurrencyDataProviders.
  • Fix bugs :)