DEV Community

Cover image for Java Forex Data API Tutorial: Real-Time Quotes, Historical Candles & WebSocket Push
San Si wu
San Si wu

Posted on

Java Forex Data API Tutorial: Real-Time Quotes, Historical Candles & WebSocket Push

In the fields of forex quantitative trading, currency conversion, strategy backtesting, and real-time monitoring, stable and low-latency forex market data is indispensable infrastructure. This article will teach you step-by-step how to integrate forex data APIs using Java — from API selection and environment preparation to full code implementation, exception handling, and advanced optimization. Even beginners can quickly get started and avoid 90% of common integration pitfalls.

First, a core premise: Forex data APIs mainly come in two types — REST API (suitable for low-frequency data fetching, such as scheduled rate queries) and WebSocket API (ideal for high-frequency real-time push, such as tick-level market monitoring). This guide will implement both approaches. You can choose according to your specific needs.

  • Real-time Quotes (REST API): Get the latest price, open/high/low/close, volume, etc. for a specified currency pair.
  • Historical K-Line Data (REST API): Multi-timeframe OHLC data from 1-minute to monthly charts, perfect for technical analysis and backtesting.
  • WebSocket Real-time Streaming: Millisecond-level push of quotes, ticks, depth, and K-line data, suitable for high-frequency trading and live monitoring.

All code examples are fully runnable. Production best practices (security, caching, reconnection, heartbeats, etc.) are also provided.

1. Preparation

1.1 API Selection

By 2026, mainstream forex data APIs are highly mature. However, platforms differ significantly in real-time performance, free quotas, and ease of integration. Beginners should prioritize platforms with simple interfaces, generous free tiers, and clear documentation. Below is a comparison of three high-value options. This article uses iTick as the primary example.

Comparison of Popular Forex Data APIs

iTick API

Offers millisecond-level market data, Bid/Ask depth quotes, and real-time volatility for major currency pairs. Provides up to 15 years of daily historical forex data. The free tier meets the needs of most individual quantitative developers.

Alpha Vantage

Free tier supports forex, stocks, and cryptocurrencies. Excellent for both real-time and historical data. Ideal for personal developers and small projects.

Fixer.io

Powered by European Central Bank data, supports over 170 currencies for real-time and historical rates. Free tier allows 1,000 requests per month with latency under 800ms.

Open Exchange Rates

Covers 200+ currencies and precious metals. Comprehensive but slightly more complex. Latency around 80ms in Asian sessions, rising to over 200ms during peak European/US hours.

1.2 REST API vs WebSocket: How to Choose?

  • REST API: Simple HTTP GET requests. Easy to implement, best for low-frequency scenarios.
  • WebSocket: Persistent connection with server-initiated data push. Extremely low latency, essential for high-frequency trading and real-time monitoring.

For basic exchange rate queries, REST is sufficient. For live market monitoring or high-frequency strategies, WebSocket is the preferred choice.

1.3 Development Environment Requirements

  • JDK 11 or higher (JDK 17+ recommended)
  • Maven or Gradle
  • Any Java IDE (IntelliJ IDEA, Eclipse, or VS Code)

1.4 Maven Dependencies

Add the following dependencies to your pom.xml:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Note: You can also use Java’s built-in HttpClient instead of OkHttp. Both implementations are provided in this guide.


2. Real-time Quotes (REST API)

2.1 Endpoint Specification

  • URL: GET https://api.itick.org/forex/quote
  • Required Parameters:
    • region: Market code (e.g., GB for London)
    • code: Instrument code (e.g., EURUSD)
  • Headers:
    • accept: application/json
    • token: YOUR_TOKEN
  • Key Response Fields (data object):
    • s: Symbol
    • ld: Last price
    • o/h/l: Open/High/Low
    • t: Timestamp (milliseconds)
    • v: Volume
    • ch/chp: Change & Change Percentage

2.2 Implementation with OkHttp

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

public class ITickQuoteRest {

    private static final OkHttpClient httpClient = new OkHttpClient();
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final String TOKEN = System.getenv("ITICK_TOKEN"); // Read from environment variable

    public static void main(String[] args) {
        fetchQuote("GB", "EURUSD");
    }

    public static void fetchQuote(String region, String code) {
        String url = String.format("https://api.itick.org/forex/quote?region=%s&code=%s", region, code);

        Request request = new Request.Builder()
                .url(url)
                .addHeader("accept", "application/json")
                .addHeader("token", TOKEN)
                .get()
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                System.err.println("HTTP Error: " + response.code());
                return;
            }
            String body = response.body() != null ? response.body().string() : "";
            parseAndPrint(body);
        } catch (IOException e) {
            System.err.println("Request Exception: " + e.getMessage());
        }
    }

    private static void parseAndPrint(String jsonBody) throws IOException {
        JsonNode root = objectMapper.readTree(jsonBody);
        if (root.path("code").asInt() != 0) {
            System.err.println("API Error: " + root.path("msg").asText());
            return;
        }
        JsonNode data = root.path("data");
        System.out.printf("Symbol: %s%n", data.path("s").asText());
        System.out.printf("Last Price: %.5f%n", data.path("ld").asDouble());
        System.out.printf("Open: %.5f  High: %.5f  Low: %.5f%n",
                data.path("o").asDouble(), data.path("h").asDouble(), data.path("l").asDouble());
        System.out.printf("Change: %.5f (%.2f%%)%n", 
                data.path("ch").asDouble(), data.path("chp").asDouble());
        System.out.printf("Timestamp: %d%n", data.path("t").asLong());
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Historical K-Line Data (REST API)

3.1 Endpoint Specification

  • URL: GET https://api.itick.org/forex/kline
  • Required Parameters:
    • region, code
    • kType: 1–10 (1min, 5min, 15min, 30min, 1H, 2H, 4H, Daily, Weekly, Monthly)
    • limit: Number of candles to return
  • Optional: et – End timestamp (ms)
  • Response: data is an array of OHLCV objects.

3.2 Complete Java Implementation

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

public class ITickKlineRest {

    private static final OkHttpClient httpClient = new OkHttpClient();
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final String TOKEN = System.getenv("ITICK_TOKEN");

    public static void main(String[] args) {
        // Fetch latest 10 5-minute K-lines for EURUSD
        fetchKline("GB", "EURUSD", 2, 10, null);
    }

    public static void fetchKline(String region, String code, int kType, int limit, Long et) {
        StringBuilder urlBuilder = new StringBuilder(String.format(
                "https://api.itick.org/forex/kline?region=%s&code=%s&kType=%d&limit=%d",
                region, code, kType, limit));

        if (et != null) {
            urlBuilder.append("&et=").append(et);
        }

        Request request = new Request.Builder()
                .url(urlBuilder.toString())
                .addHeader("accept", "application/json")
                .addHeader("token", TOKEN)
                .get()
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                System.err.println("HTTP Error: " + response.code());
                return;
            }
            String body = response.body() != null ? response.body().string() : "";
            parseKline(body);
        } catch (Exception e) {
            System.err.println("Request Exception: " + e.getMessage());
        }
    }

    private static void parseKline(String jsonBody) throws IOException {
        JsonNode root = objectMapper.readTree(jsonBody);
        if (root.path("code").asInt() != 0) {
            System.err.println("API Error: " + root.path("msg").asText());
            return;
        }
        JsonNode dataArray = root.path("data");
        for (JsonNode item : dataArray) {
            long ts = item.path("t").asLong();
            double open = item.path("o").asDouble();
            double high = item.path("h").asDouble();
            double low = item.path("l").asDouble();
            double close = item.path("c").asDouble();
            double volume = item.path("v").asDouble();
            double turnover = item.path("tu").asDouble();

            System.out.printf("Timestamp %d | O:%.5f H:%.5f L:%.5f C:%.5f Vol:%.2f Turnover:%.2f%n",
                    ts, open, high, low, close, volume, turnover);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. WebSocket Real-time Market Data Streaming

For scenarios requiring ultra-low latency and continuous updates, WebSocket is the best solution.

4.1 Interface Specification

  • Connection URL: wss://api.itick.org/forex
  • Authentication: Pass token via header during handshake
  • Interaction Flow: Connect → Auto-auth → Subscribe → Heartbeat → Data push

4.2 Complete Java WebSocket Client

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocket.Listener;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ITickWebSocketClient implements Listener {

    private static final String WS_URL = "wss://api.itick.org/forex";
    private static final String TOKEN = System.getenv("ITICK_TOKEN");

    private WebSocket webSocket;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
    private volatile boolean authenticated = false;

    public void connect() {
        HttpClient client = HttpClient.newHttpClient();
        CompletableFuture<WebSocket> wsFuture = client.newWebSocketBuilder()
                .header("token", TOKEN)
                .buildAsync(URI.create(WS_URL), this);
        webSocket = wsFuture.join();
        System.out.println("WebSocket connecting...");
    }

    @Override
    public void onOpen(WebSocket webSocket) {
        System.out.println("Connection opened");
        webSocket.request(1);
    }

    @Override
    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
        String message = data.toString();
        System.out.println("Received: " + message);

        try {
            JsonNode root = objectMapper.readTree(message);

            if (root.has("code") && root.get("code").asInt() == 1 
                    && "Connected Successfully".equals(root.path("msg").asText())) {
                System.out.println("Handshake successful, waiting for authentication...");
            }
            else if (root.has("resAc") && "auth".equals(root.get("resAc").asText())) {
                if (root.get("code").asInt() == 1) {
                    System.out.println("Authentication successful");
                    authenticated = true;
                    subscribe();
                    startHeartbeat();
                } else {
                    System.err.println("Authentication failed: " + root.path("msg").asText());
                    close();
                }
            }
            else if (root.has("resAc") && "subscribe".equals(root.get("resAc").asText())) {
                if (root.get("code").asInt() == 1) {
                    System.out.println("Subscription successful");
                } else {
                    System.err.println("Subscription failed: " + root.path("msg").asText());
                }
            }
            else if (root.has("data")) {
                JsonNode dataNode = root.get("data");
                String type = dataNode.has("type") ? dataNode.get("type").asText() : "unknown";
                String symbol = dataNode.has("s") ? dataNode.get("s").asText() : "unknown";
                handleMarketData(type, symbol, dataNode);
            }
            else if (root.has("resAc") && "pong".equals(root.get("resAc").asText())) {
                long echo = root.path("data").path("params").asLong();
                System.out.println("Received pong, echo=" + echo);
            }
        } catch (Exception e) {
            System.err.println("Parse error: " + e.getMessage());
        }

        webSocket.request(1);
        return CompletableFuture.completedFuture(null);
    }

    private void handleMarketData(String type, String symbol, JsonNode data) {
        System.out.printf("[%s] %s : %s%n", type, symbol, data.toString());
        // Place your business logic here (caching, strategy, alerts, etc.)
    }

    private void subscribe() {
        ObjectNode sub = objectMapper.createObjectNode();
        sub.put("ac", "subscribe");
        sub.put("params", "EURUSD$GB,GBPUSD$GB");   // Format: SYMBOL$REGION
        sub.put("types", "quote,tick");             // You can add depth, kline@1 etc.
        sendMessage(sub);
    }

    private void startHeartbeat() {
        heartbeatExecutor.scheduleAtFixedRate(() -> {
            if (webSocket != null && !webSocket.isInputClosed()) {
                ObjectNode ping = objectMapper.createObjectNode();
                ping.put("ac", "ping");
                ping.put("params", System.currentTimeMillis());
                sendMessage(ping);
                System.out.println("Sent ping");
            }
        }, 30, 30, TimeUnit.SECONDS);
    }

    private void sendMessage(ObjectNode msg) {
        try {
            String json = objectMapper.writeValueAsString(msg);
            webSocket.sendText(json, true);
        } catch (Exception e) {
            System.err.println("Failed to send message: " + e.getMessage());
        }
    }

    private void close() {
        if (webSocket != null) {
            webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Client closed");
        }
        heartbeatExecutor.shutdown();
    }

    @Override
    public void onError(WebSocket webSocket, Throwable error) {
        System.err.println("Error occurred: " + error.getMessage());
        reconnect();
    }

    @Override
    public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
        System.out.println("Connection closed: " + reason);
        reconnect();
        return CompletableFuture.completedFuture(null);
    }

    private void reconnect() {
        try {
            Thread.sleep(5000);
            System.out.println("Attempting to reconnect...");
            connect();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        ITickWebSocketClient client = new ITickWebSocketClient();
        client.connect();

        try {
            Thread.sleep(Long.MAX_VALUE); // Keep main thread alive
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Production Best Practices

5.1 Token Security

  • Never hard-code tokens. Use environment variables (System.getenv).
  • In Kubernetes, use Secrets. Rotate tokens regularly.

5.2 Caching Strategy

Use Caffeine or Redis for real-time quotes (1–2 second TTL). Historical data can be cached longer.

5.3 Retry & Fault Tolerance

Implement exponential backoff retry for REST calls.

5.4 Rate Limiting

Respect platform limits. One WebSocket connection supports up to 500 subscriptions.

5.5 Monitoring & Logging

Log connection status, authentication, subscription results, and latency.


6. Common Issues & Solutions

Q1: REST returns 401

→ Token missing or invalid. Check environment variable.

Q2: No auth response on WebSocket

→ Token error. Connection is dropped immediately.

Q3: Subscription returns “cannot be resolved action”

→ JSON format or ac field error.

Q4: Frequent disconnections

→ Missing heartbeats or blocking handler thread.

Q5: Empty K-line array

→ Invalid timestamp or outside trading hours.


7. Conclusion

This guide has covered three core ways to access forex data in Java:

  • REST for real-time quotes
  • REST for historical K-line data
  • WebSocket for high-performance real-time streaming

Using the complete code examples and best practices provided, you can quickly build a robust forex data acquisition system suitable for both personal projects and production trading environments.

References:

Top comments (0)