{"id":15056209,"url":"https://github.com/brcolow/candlefx","last_synced_at":"2025-04-10T04:07:03.045Z","repository":{"id":85823092,"uuid":"300999965","full_name":"brcolow/candlefx","owner":"brcolow","description":"Candle-stick Chart Library for JavaFX","archived":false,"fork":false,"pushed_at":"2020-10-13T19:34:48.000Z","size":439,"stargazers_count":17,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-10T04:06:59.601Z","etag":null,"topics":["candle-stick-chart","coinbase-exchange","javafx","openjfx"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brcolow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-03T23:15:28.000Z","updated_at":"2025-03-30T17:26:21.000Z","dependencies_parsed_at":"2023-03-05T14:00:25.141Z","dependency_job_id":null,"html_url":"https://github.com/brcolow/candlefx","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brcolow%2Fcandlefx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brcolow%2Fcandlefx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brcolow%2Fcandlefx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brcolow%2Fcandlefx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brcolow","download_url":"https://codeload.github.com/brcolow/candlefx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248154984,"owners_count":21056543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["candle-stick-chart","coinbase-exchange","javafx","openjfx"],"created_at":"2024-09-24T21:49:08.702Z","updated_at":"2025-04-10T04:07:03.037Z","avatar_url":"https://github.com/brcolow.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CandleFX Library\r\n\r\nCandleFX is a JavaFX library that provides a candle-stick chart implementation that supports incremental paging of data, \r\nlive syncing of real-time trading data, and tries hard to be responsive and snappy.\r\n\r\n![CandleFX screenshot](https://github.com/brcolow/candlefx/raw/assets/candle-stick-chart.png)\r\n\r\n## Caveat\r\n\r\nThis code was written about 5 years ago (circa 2015) for a project that never saw the light of day. I am ripping out\r\nsalvageable stand-alone parts in the hope that it benefits even one person (maybe they find this project through\r\nthe magic of search engines (including Github's own)).\r\n\r\n## Getting Started\r\n\r\n### Simplest Possible Chart\r\nCandleFX can display real-time candle-stick charts for trading commodities but let's start, for simplicity's sake, with\r\na candle-stick chart that only displays historical (past) trading data. The full source code for these examples can\r\nbe found in [CandleStickChartExample](./example/src/main/java/com/brcolow/candlefx/example/CandleStickChartExample.java).\r\n\r\nIn order to create a candle-stick chart we need the following objects:\r\n\r\n* An `exchange` instance\r\n* A `tradePair` on that exchange\r\n\r\nAn `exchange` object represents some trading facilitator of commodities. For example the New York Stock Exchange\r\nwhich facilitates trading in certain stock trade pairs (such as Tesla's stock to U.S. Dollars - the TSLA/USD trade pair)\r\nor Coinbase exchange which facilitates trading of cryptocurrencies with other currencies (fiat or crypto) such as \r\nthe BTC/USD trade pair.\r\n\r\n`Exchange` is an abstract class that should be implemented for your own needs. In these examples we will use Coinbase\r\nExchange. In order for the candle-stick chart to retrieve historical candle data you must, at a minimum, implement the\r\n`getCandleDataSupplier` method. One way using the Coinbase exchange public API could be done like so:\r\n\r\n```java\r\npublic class Coinbase extends Exchange {\r\n    Coinbase(Set\u003cTradePair\u003e tradePairs, RecentTrades recentTrades) {\r\n        super(null); // This argument is for creating a WebSocket client for live trading data.\r\n    }\r\n\r\n    @Override\r\n    public CandleDataSupplier getCandleDataSupplier(int secondsPerCandle, TradePair tradePair) {\r\n        return new CoinbaseCandleDataSupplier(secondsPerCandle, tradePair);\r\n    }\r\n\r\n    public static class CoinbaseCandleDataSupplier extends CandleDataSupplier {\r\n        private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()\r\n                .registerModule(new JavaTimeModule())\r\n                .enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\r\n                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\r\n        private static final int EARLIEST_DATA = 1422144000; // roughly the first trade\r\n\r\n        CoinbaseCandleDataSupplier(int secondsPerCandle, TradePair tradePair) {\r\n            super(200, secondsPerCandle, tradePair, new SimpleIntegerProperty(-1));\r\n        }\r\n\r\n        @Override\r\n        public Set\u003cInteger\u003e getSupportedGranularities() {\r\n            return Set.of(60, 300, 900, 3600, 21600, 86400);\r\n        }\r\n\r\n        @Override\r\n        public Future\u003cList\u003cCandleData\u003e\u003e get() {\r\n            if (endTime.get() == -1) {\r\n                endTime.set((int) (Instant.now().toEpochMilli() / 1000L));\r\n            }\r\n\r\n            String endDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME\r\n                    .format(LocalDateTime.ofEpochSecond(endTime.get(), 0, ZoneOffset.UTC));\r\n\r\n            int startTime = Math.max(endTime.get() - (numCandles * secondsPerCandle), EARLIEST_DATA);\r\n            String startDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME\r\n                    .format(LocalDateTime.ofEpochSecond(startTime, 0, ZoneOffset.UTC));\r\n\r\n            String uriStr = \"https://api.pro.coinbase.com/\" +\r\n                    \"products/\" + tradePair.toString('-') + \"/candles\" +\r\n                    \"?granularity=\" + secondsPerCandle +\r\n                    \"\u0026start=\" + startDateString +\r\n                    \"\u0026end=\" + endDateString;\r\n\r\n            if (startTime \u003c= EARLIEST_DATA) {\r\n                // signal more data is false\r\n                return CompletableFuture.completedFuture(Collections.emptyList());\r\n            }\r\n\r\n            return HttpClient.newHttpClient().sendAsync(\r\n                    HttpRequest.newBuilder()\r\n                            .uri(URI.create(uriStr))\r\n                            .GET().build(),\r\n                    HttpResponse.BodyHandlers.ofString())\r\n                    .thenApply(HttpResponse::body)\r\n                    .thenApply(response -\u003e {\r\n                        JsonNode res;\r\n                        try {\r\n                            res = OBJECT_MAPPER.readTree(response);\r\n                        } catch (JsonProcessingException ex) {\r\n                            throw new RuntimeException(ex);\r\n                        }\r\n\r\n                        if (!res.isEmpty()) {\r\n                            // Remove the current in-progress candle\r\n                            if (res.get(0).get(0).asInt() + secondsPerCandle \u003e endTime.get()) {\r\n                                ((ArrayNode) res).remove(0);\r\n                            }\r\n                            endTime.set(startTime);\r\n\r\n                            List\u003cCandleData\u003e candleData = new ArrayList\u003c\u003e();\r\n                            for (JsonNode candle : res) {\r\n                                candleData.add(new CandleData(\r\n                                        candle.get(3).asDouble(),  // open price\r\n                                        candle.get(4).asDouble(),  // close price\r\n                                        candle.get(2).asDouble(),  // high price\r\n                                        candle.get(1).asDouble(),  // low price\r\n                                        candle.get(0).asInt(),     // open time\r\n                                        candle.get(5).asDouble()   // volume\r\n                                ));\r\n                            }\r\n                            candleData.sort(Comparator.comparingInt(CandleData::getOpenTime));\r\n                            return candleData;\r\n                        } else {\r\n                            return Collections.emptyList();\r\n                        }\r\n                    });\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nCreate a `CandleStickChartContainer` object. For this example we will use the Coinbase exchange implementation and\r\ncreate a candle-stick chart for the BTC/USD trade pair.\r\n\r\n```java\r\nExchange coinbase = new Coinbase();\r\nCandleStickChartContainer candleStickChartContainer =\r\n        new CandleStickChartContainer(\r\n                coinbase,\r\n                TradePair.of(Currency.ofCrypto(\"BTC\"), Currency.ofFiat(\"USD\")));\r\n```\r\n\r\nAdd the `CandleStickChartContainer` to a JavaFX layout:\r\n\r\n```java\r\nAnchorPane.setTopAnchor(candleStickChartContainer, 30.0);\r\nAnchorPane.setLeftAnchor(candleStickChartContainer, 30.0);\r\nAnchorPane.setRightAnchor(candleStickChartContainer, 30.0);\r\nAnchorPane.setBottomAnchor(candleStickChartContainer, 30.0);\r\ncandleStickChartContainer.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);\r\nScene scene = new Scene(new AnchorPane(candleStickChartContainer), 1200, 800);\r\n```\r\n\r\n### Enable Live Syncing to Create a Real-Time Chart\r\n\r\nNow that we have constructed a simple chart that starts contains data from when the chart is created (and can go\r\nbackwards to the first trade of that tradepair on that exchange) we now want to look at creating a real-time chart\r\nthat updates as trades happen. This means that, if the most recent candle is in the view port, it will be redrawn\r\nas trades happen and, once the current candle duration is over, the chart will add a new candle to the right\r\nand begin syncing it with current trading activity.\r\n\r\nIn order to support live syncing mode we need to implement two additional methods of the `Exchange` class:\r\n\r\n```java\r\n@Override\r\nCompletableFuture\u003cOptional\u003cInProgressCandleData\u003e\u003e fetchCandleDataForInProgressCandle(\r\n    TradePair tradePair,\r\n    Instant currentCandleStartedAt,\r\n    long secondsIntoCurrentCandle,\r\n    int secondsPerCandle) {}\r\n\r\n@Override\r\nCompletableFuture\u003cList\u003cTrade\u003e\u003e fetchRecentTradesUntil(TradePair tradePair, Instant stopAt) {}\r\n```\r\n\r\nThe first method fetches data using a \"sub-candle method\" (that is, fetching data for completed candles of a less\r\nduration than the chart's selected granularity. The second method is then used to fetch the raw, individual trades\r\nfor the duration between the last sub-candle (from the first method) and the current time. We go through the trouble\r\nof having these two methods work in tandem (as opposed to only needing the second method) because it can take a\r\nprohibitively long time to fetch the raw trade data in the candle duration is too large. An example of this would\r\nbe if the candle duration is one day the number of trades on an exchange in a single day could be in the millions.\r\n\r\nLet's implement the first method `fetchCandleDataForInProgressCandle` which uses the sub-candle strategy for syncing\r\nwith real-time data using the Coinbase API:\r\n\r\n```java\r\n@Override\r\npublic CompletableFuture\u003cOptional\u003cInProgressCandleData\u003e\u003e fetchCandleDataForInProgressCandle(\r\n        TradePair tradePair, Instant currentCandleStartedAt, long secondsIntoCurrentCandle, int secondsPerCandle) {\r\n    String startDateString = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.ofInstant(\r\n            currentCandleStartedAt, ZoneOffset.UTC));\r\n    // Compute the ideal sub-candle granularity.\r\n    long idealGranularity = Math.max(10, secondsIntoCurrentCandle / 200);\r\n    // Get the closest supported granularity to the ideal granularity.\r\n    int actualGranularity = getCandleDataSupplier(secondsPerCandle, tradePair).getSupportedGranularities().stream()\r\n            .min(Comparator.comparingInt(i -\u003e (int) Math.abs(i - idealGranularity)))\r\n            .orElseThrow(() -\u003e new NoSuchElementException(\"Supported granularities was empty!\"));\r\n    // TODO: If actualGranularity = secondsPerCandle there are no sub-candles to fetch and we must get all the\r\n    //  data for the current live syncing candle from the raw trades method.\r\n    return HttpClient.newHttpClient().sendAsync(\r\n            HttpRequest.newBuilder()\r\n                    .uri(URI.create(String.format(\r\n                            \"https://api.pro.coinbase.com/products/%s/candles?granularity=%s\u0026start=%s\",\r\n                            tradePair.toString('-'), actualGranularity, startDateString)))\r\n                    .GET().build(),\r\n            HttpResponse.BodyHandlers.ofString())\r\n            .thenApply(HttpResponse::body)\r\n            .thenApply(response -\u003e {\r\n                logger.info(\"coinbase response: \" + response);\r\n                JsonNode res;\r\n                try {\r\n                    res = OBJECT_MAPPER.readTree(response);\r\n                } catch (JsonProcessingException ex) {\r\n                    throw new RuntimeException(ex);\r\n                }\r\n\r\n                if (res.isEmpty()) {\r\n                    return Optional.empty();\r\n                }\r\n\r\n                JsonNode currCandle;\r\n                Iterator\u003cJsonNode\u003e candleItr = res.iterator();\r\n                int currentTill = -1;\r\n                double openPrice = -1;\r\n                double highSoFar = -1;\r\n                double lowSoFar = Double.MAX_VALUE;\r\n                double volumeSoFar = 0;\r\n                double lastTradePrice = -1;\r\n                boolean foundFirst = false;\r\n                while (candleItr.hasNext()) {\r\n                    currCandle = candleItr.next();\r\n                    if (currCandle.get(0).asInt() \u003c currentCandleStartedAt.getEpochSecond() ||\r\n                            currCandle.get(0).asInt() \u003e= currentCandleStartedAt.getEpochSecond() +\r\n                                    secondsPerCandle) {\r\n                        // Skip this sub-candle if it is not in the parent candle's duration (this is just a\r\n                        // sanity guard).\r\n                        continue;\r\n                    } else {\r\n                        if (!foundFirst) {\r\n                            // FIXME: Why are we only using the first sub-candle here?\r\n                            //  Unless foundFirst is actually the *last* (that is, most recent in time) sub-candle?\r\n                            currentTill = currCandle.get(0).asInt();\r\n                            lastTradePrice = currCandle.get(4).asDouble();\r\n                            foundFirst = true;\r\n                        }\r\n                    }\r\n\r\n                    openPrice = currCandle.get(3).asDouble();\r\n\r\n                    if (currCandle.get(2).asDouble() \u003e highSoFar) {\r\n                        highSoFar = currCandle.get(2).asDouble();\r\n                    }\r\n\r\n                    if (currCandle.get(1).asDouble() \u003c lowSoFar) {\r\n                        lowSoFar = currCandle.get(1).asDouble();\r\n                    }\r\n\r\n                    volumeSoFar += currCandle.get(5).asDouble();\r\n                }\r\n\r\n                int openTime = (int) (currentCandleStartedAt.toEpochMilli() / 1000L);\r\n\r\n                return Optional.of(new InProgressCandleData(openTime, openPrice, highSoFar, lowSoFar,\r\n                        currentTill, lastTradePrice, volumeSoFar));\r\n            });\r\n}\r\n```\r\n\r\nNow let's implement the second method `fetchRecentTradesUntil` which uses the raw trade data strategy for syncing\r\nwith real-time data using the Coinbase API to fill in the missing data trades from the end of the last sub-candle (from\r\nthe first method):\r\n\r\n```java\r\n/**\r\n * Fetches the recent trades for the given trade pair from  {@code stopAt} till now (the current time).\r\n * \u003cp\u003e\r\n * This method only needs to be implemented to support live syncing.\r\n */\r\n@Override\r\npublic CompletableFuture\u003cList\u003cTrade\u003e\u003e fetchRecentTradesUntil(TradePair tradePair, Instant stopAt) {\r\n    Objects.requireNonNull(tradePair);\r\n    Objects.requireNonNull(stopAt);\r\n\r\n    // We were asked to fetch data from the future but we don't have a time machine (yet).\r\n    if (stopAt.isAfter(Instant.now())) {\r\n        return CompletableFuture.completedFuture(Collections.emptyList());\r\n    }\r\n\r\n    CompletableFuture\u003cList\u003cTrade\u003e\u003e futureResult = new CompletableFuture\u003c\u003e();\r\n\r\n    // It is not easy (possible?) to fetch trades concurrently because we need to get the \"cb-after\" header after each\r\n    // request.\r\n    CompletableFuture.runAsync(() -\u003e {\r\n        IntegerProperty afterCursor = new SimpleIntegerProperty(0);\r\n        List\u003cTrade\u003e tradesBeforeStopTime = new ArrayList\u003c\u003e();\r\n\r\n        for (int i = 0; !futureResult.isDone(); i++) {\r\n            String uriStr = \"https://api.pro.coinbase.com/\";\r\n            uriStr += \"products/\" + tradePair.toString('-') + \"/trades\";\r\n\r\n            if (i != 0) {\r\n                uriStr += \"?after=\" + afterCursor.get();\r\n            }\r\n\r\n            try {\r\n                HttpResponse\u003cString\u003e response = HttpClient.newHttpClient().send(\r\n                        HttpRequest.newBuilder()\r\n                                .uri(URI.create(uriStr))\r\n                                .GET().build(),\r\n                        HttpResponse.BodyHandlers.ofString());\r\n                if (response.headers().firstValue(\"cb-after\").isEmpty()) {\r\n                    futureResult.completeExceptionally(new RuntimeException(\r\n                            \"coinbase trades response did not contain header \\\"cb-after\\\": \" + response));\r\n                    return;\r\n                }\r\n\r\n                afterCursor.setValue(Integer.valueOf((response.headers().firstValue(\"cb-after\").get())));\r\n\r\n                JsonNode tradesResponse = OBJECT_MAPPER.readTree(response.body());\r\n\r\n                if (!tradesResponse.isArray()) {\r\n                    futureResult.completeExceptionally(new RuntimeException(\r\n                            \"coinbase trades response was not an array!\"));\r\n                }\r\n                if (tradesResponse.isEmpty()) {\r\n                    futureResult.completeExceptionally(new IllegalArgumentException(\"coinbase trades response was empty\"));\r\n                } else {\r\n                    for (int j = 0; j \u003c tradesResponse.size(); j++) {\r\n                        JsonNode trade = tradesResponse.get(j);\r\n                        Instant time = Instant.from(ISO_INSTANT.parse(trade.get(\"time\").asText()));\r\n                        if (time.compareTo(stopAt) \u003c= 0) {\r\n                            // We have caught up with all trades until the requested stop time, so we are finished.\r\n                            futureResult.complete(tradesBeforeStopTime);\r\n                            break;\r\n                        } else {\r\n                            // Add this raw trade to the list.\r\n                            tradesBeforeStopTime.add(new Trade(tradePair,\r\n                                    DefaultMoney.ofFiat(trade.get(\"price\").asText(), tradePair.getCounterCurrency()),\r\n                                    DefaultMoney.ofCrypto(trade.get(\"size\").asText(), tradePair.getBaseCurrency()),\r\n                                    Side.getSide(trade.get(\"side\").asText()), trade.get(\"trade_id\").asLong(), time));\r\n                        }\r\n                    }\r\n                }\r\n            } catch (IOException | InterruptedException ex) {\r\n                logger.error(\"ex: \", ex);\r\n            }\r\n        }\r\n    });\r\n\r\n    return futureResult;\r\n}\r\n```\r\n\r\nNext we create a `CandleStickChartContainer` making sure to pass in `true` for the liveSyncing argument:\r\n\r\n```java\r\nExchange coinbase = new Coinbase();\r\nCandleStickChartContainer candleStickChartContainer =\r\n        new CandleStickChartContainer(\r\n                coinbase,\r\n                TradePair.of(Currency.ofCrypto(\"BTC\"), Currency.ofFiat(\"USD\")),\r\n                true // Turn live-syncing on\r\n        );\r\n```\r\n\r\n## Attribution\r\n\r\nCandleFX would not be possible without the following open source projects:\r\n\r\nThe [FontAwesome](https://fontawesome.com/) icon set for the chart toolbar.\r\n\r\nThe PopOver and ToggleSwitch controls from [ControlsFX](https://github.com/controlsfx/controlsfx).\r\n\r\nThe `StableTicksAxis` implementation from [JFXUtils](https://github.com/gillius/jfxutils).\r\n\r\nThe `FastMoney` implementation from [mikvor/money-conversion](https://github.com/mikvor/money-conversion).\r\n\r\n## TODO\r\n\r\n* Flesh out a full README example.\r\n* Add examples from more cryptocurrency exchanges.\r\n* Add example using finnhub.io (https://finnhub.io/docs/api#stock-candles).\r\n* Create subpackages (monetary, controls, etc.) and have better separation of private/public API with help of JPMS.\r\n* Create websocket interface instead of having a strong tie to one websocket library so consumers can plug in their\r\ndesired one.\r\n* Fix the ServiceLoaders for `CurrencyDataProvider`s.\r\n* Fix bugs :)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrcolow%2Fcandlefx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrcolow%2Fcandlefx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrcolow%2Fcandlefx/lists"}