WeChart Adapter API
標準化 Adapter 介面規格文件 — 對接任意後端所需實作的方法。
Live Demo
在瀏覽器直接試用 WeChart,連接任意支援 Adapter 規格的後端:
# 使用預設 Demo 後端(walltrade.cc)
https://wechart.cc/demo
# 連接自己的後端
https://wechart.cc/demo?api=https://你的後端網址
Demo 帳號(walltrade.cc):testbot99 / admin123
概覽
WeChart 透過 Adapter 介面與後端通信,將前端 UI 邏輯與後端 API 完全解耦。
實作 Adapter 的所有必要方法後,傳入 TradingApi 即可讓 WeChart 對接你的後端。
方法一覽
| 方法 | 分類 | 必要 | 說明 |
|---|---|---|---|
login | 認證 | 必要 | 用戶登入,回傳 token |
getMe | 認證 | 必要 | 驗證 token,獲取用戶資訊 |
logout | 認證 | 必要 | 登出 |
getBalance | 帳戶 | 必要 | 獲取餘額 |
getSymbols | 市場 | 必要 | 獲取交易對列表 |
getTicker | 市場 | 必要 | 獲取即時行情 |
getKlines | 市場 | 必要 | 獲取 K 線數據 |
getOrderbookConfig | 訂單簿 | 必要 | 獲取訂單簿配置(可回預設值) |
getOrders | 交易 | 必要 | 獲取訂單列表 |
createOrder | 交易 | 必要 | 建立訂單 |
cancelOrder | 交易 | 必要 | 撤銷訂單 |
cancelAllLiquidity | 做市 | 可選 | 批量撤銷流動性訂單 |
快速開始
最簡單的方式:繼承或參考 SimTradingAdapter 的實作,替換 API 端點即可。
// 最小可用 Adapter 範例
class MyAdapter {
constructor({ apiBase, getToken }) {
this._apiBase = apiBase;
this._getToken = getToken;
}
_headers() {
const token = this._getToken();
const h = { 'Content-Type': 'application/json' };
if (token) h['Authorization'] = 'Bearer ' + token;
return h;
}
async login(username, password) {
const r = await fetch(this._apiBase + '/auth/login', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({ username, password })
});
if (!r.ok) throw Object.assign(new Error('Login failed'), { status: r.status });
return r.json();
// 必須回傳: { token, user: { id, username, role, featureFlags } }
}
// ... 實作其他方法
}
// 傳入 TradingApi
const api = new TradingApi({ adapter: new MyAdapter({
apiBase: 'https://your-api.example.com',
getToken: () => localStorage.getItem('token')
}) });
login
用戶登入,驗證帳號密碼,回傳 JWT token 及用戶資訊。
回傳
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "user_123",
"username": "trader01",
"role": "trader", // "trader" | "broker" | "admin"
"featureFlags": 256 // 位元旗標,控制功能顯示
}
}
getMe
以 token 換取完整用戶資訊,頁面初始化時用於驗證登入狀態。
回傳
{
"id": "user_123",
"username": "trader01",
"role": "broker",
"featureFlags": 256,
"brokerInfo": {
"brokerId": "broker_abc" // 僅 broker/admin 角色才有
}
}
logout
登出當前用戶,使伺服器端 session 失效。若後端不支援,可回傳已 resolved 的 Promise。
getBalance
獲取當前用戶的帳戶餘額資訊。
回傳
{
"balance": 10000.00, // 總餘額
"availableBalance": 8500.00, // 可用餘額
"frozenMargin": 1500.00 // 凍結保證金
}
getSymbols
獲取所有可交易的交易對列表。
Symbol 物件
| 欄位 | 類型 | 說明 |
|---|---|---|
symbol | string | 交易對代碼,如 BTCUSDT |
name | string | 顯示名稱,如 Bitcoin |
syncStatus | string | 行情同步狀態:"live" / "paused" |
minOrderValue | number | 最小下單金額(USDT) |
maxLeverage | number | 最大可用槓桿倍數 |
回傳範例
[
{ "symbol": "BTCUSDT", "name": "Bitcoin", "syncStatus": "live", "minOrderValue": 10, "maxLeverage": 100 },
{ "symbol": "ETHUSDT", "name": "Ethereum", "syncStatus": "live", "minOrderValue": 5, "maxLeverage": 50 }
]
getTicker
獲取指定交易對的即時行情快照。
回傳
{
"symbol": "BTCUSDT",
"lastPrice": 67500.00,
"priceChangePercent": 2.35, // 24h 漲跌幅 %
"highPrice": 68200.00, // 24h 最高價
"lowPrice": 65800.00, // 24h 最低價
"volume": 12345.67 // 24h 成交量(base asset)
}
getKlines
獲取 K 線(OHLCV)數據。
參數
| 參數 | 類型 | 說明 |
|---|---|---|
symbol | string | 交易對,如 BTCUSDT |
interval | string | 時框:1m / 5m / 15m / 1h / 4h / 1d |
limit | number | 回傳條數,最多 1000,預設 500 |
Kline 物件
[
{
"time": 1716100000, // Unix 秒(注意:秒,不是毫秒)
"open": 67000.00,
"high": 67800.00,
"low": 66500.00,
"close": 67500.00,
"volume": 123.45
},
// ...
]
time 必須是 Unix 秒,不是毫秒。WeChart 圖表庫預期秒級時間戳。getOrderbookConfig
獲取指定交易對的訂單簿顯示配置。若後端無此端點,可直接回傳預設值。
回傳
{
"orderBookDepth": 20, // 顯示幾檔買賣盤
"priceTickSize": 0.1, // 價格精度(合併顯示用)
"spreadMode": "fixed", // "fixed" | "percent"
"minSpread": 0.5 // 最小點差(fixed=USDT, percent=百分比)
}
{ orderBookDepth: 20, priceTickSize: 0.1, spreadMode: 'fixed', minSpread: 0 }
getOrders
獲取訂單列表,支援按狀態、交易對、數量篩選。
params 參數
| 欄位 | 類型 | 必要 | 說明 |
|---|---|---|---|
status | string | 可選 | "open" / "closed" / "cancelled" |
limit | number | 可選 | 回傳數量上限,預設 50 |
symbol | string | 可選 | 篩選指定交易對 |
回傳
{
"orders": [ /* Order 物件陣列,見下方 Order 規格 */ ]
}
createOrder
建立新訂單,回傳已建立的 Order 物件。
data 欄位
| 欄位 | 類型 | 必要 | 說明 |
|---|---|---|---|
symbol | string | 必要 | 交易對,如 BTCUSDT |
side | string | 必要 | "buy" 或 "sell" |
type | string | 必要 | "market" / "limit" / "stop" / "liquidity" |
quantity | number | 必要 | 下單數量(base asset) |
price | number | 可選 | 限價單指定價格 |
stopPrice | number | 可選 | 止損單觸發價格 |
leverage | number | 可選 | 槓桿倍數,預設 1 |
takeProfit | number | 可選 | 止盈價格 |
stopLoss | number | 可選 | 止損價格 |
meta | object | 可選 | 自定義元數據,如 { pairId: "uuid" } |
liquidityConfig | object | 可選 | 流動性訂單配置,見下方說明 |
priceMode | string | 可選 | 價格模式:"fixed" 或 "percent" |
pricePercent | number | 可選 | 相對市價的百分比偏移(priceMode=percent 時使用) |
liquidityConfig 欄位
{
"levelOffset": 1, // 深度層級 1-19,1 為最緊點差
"pricePercent": 0.5 // 距市價的百分比(可選)
}
cancelOrder
撤銷指定 ID 的訂單。
cancelAllLiquidity
批量撤銷所有流動性訂單。symbol 為可選,不傳則撤銷所有交易對的流動性訂單。
此方法為可選,若後端不支援,可省略或回傳 { cancelled: 0 }。
回傳
{
"cancelled": 4 // 實際撤銷的訂單數量
}
Order 物件
所有涉及訂單的方法(getOrders、createOrder)都使用統一的 Order 物件格式。
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | 訂單唯一 ID |
symbol | string | 交易對 |
side | string | "buy" / "sell" |
type | string | "market" / "limit" / "stop" / "liquidity" |
status | string | "open" / "filled" / "partial" / "cancelled" |
quantity | number | 下單數量 |
filledQuantity | number | 已成交數量 |
closedQuantity | number | 已平倉數量 |
price | number | 掛單價格(市價單為 null) |
averagePrice | number | 平均成交價格 |
leverage | number | 使用槓桿倍數 |
isLiquidityOrder | boolean | 是否為流動性做市訂單 |
isHedgeOrder | boolean | 是否為對沖訂單 |
liquidityLevelOffset | number | 流動性深度層級(1–19) |
liquidityPricePercent | number | 流動性價格百分比偏移 |
pnl | number | 已實現盈虧(平倉後) |
createdAt | string | 建立時間 ISO 8601,如 "2024-05-20T10:00:00Z" |
完整範例
{
"id": "ord_abc123",
"symbol": "BTCUSDT",
"side": "buy",
"type": "limit",
"status": "open",
"quantity": 0.1,
"filledQuantity": 0,
"closedQuantity": 0,
"price": 67000.00,
"averagePrice": null,
"leverage": 10,
"isLiquidityOrder": false,
"isHedgeOrder": false,
"liquidityLevelOffset": null,
"liquidityPricePercent": null,
"pnl": null,
"createdAt": "2024-05-20T10:00:00Z"
}
參考實作 — SimTradingAdapter
以下是 sim-trading-pro 預設的 SimTradingAdapter 完整實作,
可作為自訂 Adapter 的開發參考。
// ════════════════════════════════════════════════════════════════════════════
// SimTradingAdapter — sim-trading-pro 後端的 Adapter 實作
// 日後對接其他平台:實作相同方法的新 Adapter 傳進 TradingApi 即可
// ════════════════════════════════════════════════════════════════════════════
function SimTradingAdapter(opts) {
this._apiBase = (opts.apiBase || '').replace(/\/$/, '');
this._getToken = opts.getToken || function() { return null; };
this._onUnauthorized = opts.onUnauthorized || null;
this._onError = opts.onError || null;
}
SimTradingAdapter.prototype._url = function(path) {
return this._apiBase + (path.startsWith('/') ? path : '/' + path);
};
SimTradingAdapter.prototype._headers = function() {
var token = this._getToken();
var h = { 'Content-Type': 'application/json' };
if (token) h['Authorization'] = 'Bearer ' + token;
return h;
};
SimTradingAdapter.prototype._fetch = function(method, path, body) {
var self = this;
var url = this._url(path);
var opts = { method: method, headers: this._headers() };
if (body !== undefined && body !== null) opts.body = JSON.stringify(body);
return fetch(url, opts).then(function(r) {
if (r.status === 401) {
if (self._onUnauthorized) self._onUnauthorized();
throw Object.assign(new Error('Unauthorized'), { status: 401 });
}
return r.json().then(function(data) {
if (!r.ok) {
var msg = (data && (data.message || data.error)) || ('HTTP ' + r.status);
if (self._onError) self._onError(msg);
throw Object.assign(new Error(msg), { status: r.status, data: data });
}
return data;
});
}).catch(function(e) {
if (e.status) throw e; // 已處理的 HTTP 錯誤直接往上丟
if (self._onError) self._onError('網絡錯誤:' + e.message);
throw e;
});
};
// ─── Auth ──────────────────────────────────────────────────────────────────
SimTradingAdapter.prototype.login = function(username, password) {
return this._fetch('POST', '/api/auth/login', { username: username, password: password });
};
SimTradingAdapter.prototype.getMe = function(token) {
var url = this._url('/api/auth/me');
return fetch(url, { headers: { Authorization: 'Bearer ' + token } })
.then(function(r) {
if (r.status === 401) throw Object.assign(new Error('Unauthorized'), { status: 401 });
return r.json();
});
};
SimTradingAdapter.prototype.logout = function() {
return this._fetch('POST', '/api/auth/logout', {}).catch(function() {});
};
// ─── Account ───────────────────────────────────────────────────────────────
SimTradingAdapter.prototype.getBalance = function() {
return this._fetch('GET', '/api/accounts/balance');
};
// ─── Market ────────────────────────────────────────────────────────────────
SimTradingAdapter.prototype.getSymbols = function() {
return this._fetch('GET', '/api/symbols/enabled');
};
SimTradingAdapter.prototype.getTicker = function(symbol) {
return this._fetch('GET', '/api/market/ticker?symbol=' + encodeURIComponent(symbol));
};
SimTradingAdapter.prototype.getKlines = function(symbol, interval, limit) {
var qs = '?symbol=' + encodeURIComponent(symbol) + '&interval=' + interval + '&limit=' + (limit || 500);
return this._fetch('GET', '/api/market/klines' + qs);
};
SimTradingAdapter.prototype.getOrderbookConfig = function(symbol) {
return this._fetch('GET', '/api/symbols/' + encodeURIComponent(symbol) + '/orderbook-config');
};
// ─── Trading ───────────────────────────────────────────────────────────────
SimTradingAdapter.prototype.getOrders = function(params) {
var qs = Object.keys(params || {}).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
}).join('&');
return this._fetch('GET', '/api/trading/orders' + (qs ? '?' + qs : ''));
};
SimTradingAdapter.prototype.createOrder = function(data) {
return this._fetch('POST', '/api/trading/orders', data);
};
SimTradingAdapter.prototype.cancelOrder = function(orderId) {
return this._fetch('DELETE', '/api/trading/orders/' + orderId);
};
SimTradingAdapter.prototype.cancelAllLiquidity = function(symbol) {
var qs = symbol ? '?symbol=' + encodeURIComponent(symbol) : '';
return this._fetch('DELETE', '/api/liquidity/cancel-all' + qs);
};
錯誤處理
Adapter 在請求失敗時應拋出帶有 status 屬性的 Error 物件:
401 未授權
必須拋出帶有 { status: 401 } 的錯誤,WeChart 會自動觸發重新登入流程。
// 401 錯誤格式
throw Object.assign(new Error('Unauthorized'), { status: 401 });
其他 HTTP 錯誤
帶上 HTTP 狀態碼和錯誤描述,方便調試:
// 一般 HTTP 錯誤格式
const msg = data?.message || data?.error || 'HTTP ' + response.status;
throw Object.assign(new Error(msg), {
status: response.status,
data: data // 原始回應 body(可選)
});
網絡錯誤
fetch 本身拋出的網絡錯誤(如無網絡連線)沒有 status 屬性,應直接往上拋或包裝後拋出:
try {
const r = await fetch(url, opts);
// ...
} catch (e) {
if (e.status) throw e; // 已處理的 HTTP 錯誤,直接往上丟
// 網絡錯誤
throw new Error('網絡錯誤:' + e.message);
}
error.status === 401 判斷是否需要重新登入。
確保所有 401 錯誤都正確設置了 status 屬性。