Using WebSockets to Convert BTC to USD and Reais (BRL)
Using WebSockets to Convert BTC to USD and Reais (BRL)
使用 WebSockets 实现 BTC 到美元 (USD) 和雷亚尔 (BRL) 的实时转换
If you need real-time BTC conversion (USD and BRL), polling an API every few seconds is usually not enough. A better approach is streaming quotes with WebSockets and calculating conversions as events arrive. 如果你需要实时的 BTC 转换(美元和雷亚尔),每隔几秒轮询一次 API 通常是不够的。更好的方法是使用 WebSockets 流式传输报价,并在事件到达时计算转换结果。
Why WebSockets for BTC conversion?
为什么 BTC 转换要使用 WebSockets?
With WebSockets, your app keeps one open connection and receives new prices instantly. 通过 WebSockets,你的应用程序只需保持一个连接,即可即时接收最新的价格。
Benefits:
- Lower latency than polling
- Fewer HTTP requests
- Better user experience for real-time values
优点:
- 比轮询具有更低的延迟
- 更少的 HTTP 请求
- 为实时数值提供更好的用户体验
Trade-offs:
- You must handle reconnects
- Need heartbeat/health checks
- Must validate and normalize incoming messages
权衡:
- 必须处理重连逻辑
- 需要心跳/健康检查
- 必须验证并标准化传入的消息
Real-time conversion model
实时转换模型
For BTC conversion, a common model is:
- Stream BTC/USD
- Stream USD/BRL
- Calculate BTC/BRL = BTC/USD × USD/BRL
对于 BTC 转换,一种常见的模型是:
- 流式传输 BTC/USD
- 流式传输 USD/BRL
- 计算 BTC/BRL = BTC/USD × USD/BRL
This avoids waiting for a separate BTC/BRL endpoint and keeps conversion logic transparent. 这避免了等待单独的 BTC/BRL 接口,并保持了转换逻辑的透明度。
What is a “tick”?
什么是“Tick”(报价跳动)?
A tick is one market update event. Example: BTCUSD changed to 64210.50 at timestamp t. Tick 是指一次市场更新事件。例如:BTCUSD 在时间戳 t 变为 64210.50。
In this article, each tick has:
- pair: market identifier (BTCUSD, USDBRL)
- price: latest value for that pair
- ts: event timestamp
在本文中,每个 tick 包含:
- pair:市场标识符(BTCUSD, USDBRL)
- price:该交易对的最新价格
- ts:事件时间戳
Why this matters: conversion state should always be derived from the latest ticks. 重要性: 转换状态应始终根据最新的 tick 推导得出。
Minimal WebSocket client (TypeScript)
极简 WebSocket 客户端 (TypeScript)
This client only transports responsibilities:
- Connect
- Receive messages
- Parse and normalize into a consistent shape
- Notify listeners
- Reconnect on disconnect
该客户端仅负责传输职责:
- 连接
- 接收消息
- 解析并标准化为统一格式
- 通知监听器
- 断开后重连
type MarketTick = {
pair: string; // e.g. "BTCUSD" or "USDBRL"
price: number;
ts: number;
};
class WsFeedClient {
private ws?: WebSocket;
private listeners: Array<(tick: MarketTick) => void> = [];
constructor(private readonly url: string) {}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => console.log("[ws] connected");
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(String(event.data));
// Normalize external payload into internal contract
const tick: MarketTick = {
pair: String(data.pair),
price: Number(data.price),
ts: Number(data.ts),
};
// Basic guard
if (!tick.pair || Number.isNaN(tick.price) || Number.isNaN(tick.ts)) return;
this.listeners.forEach((listener) => listener(tick));
} catch {
console.warn("[ws] invalid payload");
}
};
this.ws.onclose = () => {
console.warn("[ws] disconnected, reconnecting...");
setTimeout(() => this.connect(), 1500);
};
this.ws.onerror = () => {
this.ws?.close();
};
}
onTick(listener: (tick: MarketTick) => void) {
this.listeners.push(listener);
}
}
Why normalize messages early? External providers may use different field names and formats. If raw payloads leak into business logic, maintenance gets harder quickly. So we normalize at the edge (MarketTick) and keep the rest of the system provider-agnostic.
为什么要尽早标准化消息? 外部提供商可能使用不同的字段名称和格式。如果原始数据负载泄露到业务逻辑中,维护难度会迅速增加。因此,我们在边缘(MarketTick)进行标准化,使系统的其余部分与特定提供商解耦。
Conversion engine (BTC → USD and BRL)
转换引擎 (BTC → USD 和 BRL)
This component stores the latest required rates and computes current outputs. 该组件存储所需的最新汇率并计算当前的输出结果。
type ConversionSnapshot = {
btcInUsd: number;
btcInBrl: number | null;
updatedAt: number;
};
class BtcConverter {
private btcUsd?: number;
private usdBrl?: number;
private lastTs?: number;
update(tick: { pair: string; price: number; ts: number }) {
if (tick.pair === "BTCUSD") this.btcUsd = tick.price;
if (tick.pair === "USDBRL") this.usdBrl = tick.price;
this.lastTs = tick.ts;
}
getSnapshot(): ConversionSnapshot | null {
// Can't compute anything without BTCUSD
if (this.btcUsd == null) return null;
return {
btcInUsd: this.btcUsd,
btcInBrl: this.usdBrl == null ? null : this.btcUsd * this.usdBrl,
updatedAt: this.lastTs ?? Date.now(),
};
}
}
What is a “snapshot”? A snapshot is the current computed view of your conversion state. It is useful because:
- UI/services consume one stable object
- Internal partial state stays hidden
- Logging and caching become simpler
什么是“快照”? 快照是转换状态当前计算出的视图。它很有用,因为:
- UI/服务只需消费一个稳定的对象
- 内部的部分状态保持隐藏
- 日志记录和缓存变得更简单
Instead of exposing raw tick flow everywhere, you expose a single coherent result. 与其到处暴露原始的 tick 流,不如只暴露一个连贯的结果。
Wiring everything together
整合所有组件
Event flow: WebSocket tick → normalize → update converter → emit snapshot 事件流:WebSocket tick → 标准化 → 更新转换器 → 发出快照
const client = new WsFeedClient(process.env.MARKET_WS_URL!);
const converter = new BtcConverter();
client.onTick((tick) => {
converter.update(tick);
const snapshot = converter.getSnapshot();
if (!snapshot) return;
console.log("BTC in USD:", snapshot.btcInUsd);
if (snapshot.btcInBrl != null) {
console.log("BTC in BRL:", snapshot.btcInBrl);
} else {
console.log("BTC in BRL: waiting for USDBRL tick...");
}
});
client.connect();
Satlance example (high-level)
Satlance 示例(高层级)
In Satlance, this is implemented with clear separation of concerns:
- WebSocket client layer: manages connection lifecycle and message intake
- Broadcaster layer: distributes normalized price events internally
- Domain layer: consumes those events for conversion and product features
在 Satlance 中,这是通过清晰的关注点分离来实现的:
- WebSocket 客户端层:管理连接生命周期和消息接收
- 广播层:在内部发布标准化的价格事件
- 领域层:消费这些事件以实现转换和产品功能
This design keeps real-time transport logic isolated from business rules, making the system easier to test, evolve, and operate. 这种设计将实时传输逻辑与业务规则隔离开来,使系统更易于测试、演进和运维。