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 對接你的後端。

WeChart UI
TradingApi(持有 Adapter)
Adapter 實作
後端 API
ℹ️
所有 Adapter 方法都應回傳 Promise。 標記為 必要 的方法必須實作; 標記為 可選 的方法可回傳 null 或空值。

方法一覽

方法分類必要說明
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

必要
login(username: string, password: string) → Promise<{ token, user }>

用戶登入,驗證帳號密碼,回傳 JWT token 及用戶資訊。

回傳

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "user_123",
    "username": "trader01",
    "role": "trader",           // "trader" | "broker" | "admin"
    "featureFlags": 256         // 位元旗標,控制功能顯示
  }
}

getMe

必要
getMe(token: string) → Promise<{ id, username, role, featureFlags, brokerInfo }>

以 token 換取完整用戶資訊,頁面初始化時用於驗證登入狀態。

回傳

{
  "id": "user_123",
  "username": "trader01",
  "role": "broker",
  "featureFlags": 256,
  "brokerInfo": {
    "brokerId": "broker_abc"    // 僅 broker/admin 角色才有
  }
}

logout

必要
logout() → Promise<void>

登出當前用戶,使伺服器端 session 失效。若後端不支援,可回傳已 resolved 的 Promise。


getBalance

必要
getBalance() → Promise<{ balance, availableBalance, frozenMargin }>

獲取當前用戶的帳戶餘額資訊。

回傳

{
  "balance": 10000.00,           // 總餘額
  "availableBalance": 8500.00,   // 可用餘額
  "frozenMargin": 1500.00        // 凍結保證金
}

getSymbols

必要
getSymbols() → Promise<Array<Symbol>>

獲取所有可交易的交易對列表。

Symbol 物件

欄位類型說明
symbolstring交易對代碼,如 BTCUSDT
namestring顯示名稱,如 Bitcoin
syncStatusstring行情同步狀態:"live" / "paused"
minOrderValuenumber最小下單金額(USDT)
maxLeveragenumber最大可用槓桿倍數

回傳範例

[
  { "symbol": "BTCUSDT", "name": "Bitcoin", "syncStatus": "live", "minOrderValue": 10, "maxLeverage": 100 },
  { "symbol": "ETHUSDT", "name": "Ethereum", "syncStatus": "live", "minOrderValue": 5, "maxLeverage": 50 }
]

getTicker

必要
getTicker(symbol: string) → Promise<Ticker>

獲取指定交易對的即時行情快照。

回傳

{
  "symbol": "BTCUSDT",
  "lastPrice": 67500.00,
  "priceChangePercent": 2.35,    // 24h 漲跌幅 %
  "highPrice": 68200.00,         // 24h 最高價
  "lowPrice": 65800.00,          // 24h 最低價
  "volume": 12345.67             // 24h 成交量(base asset)
}

getKlines

必要
getKlines(symbol: string, interval: string, limit: number) → Promise<Array<Kline>>

獲取 K 線(OHLCV)數據。

參數

參數類型說明
symbolstring交易對,如 BTCUSDT
intervalstring時框:1m / 5m / 15m / 1h / 4h / 1d
limitnumber回傳條數,最多 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

必要
getOrderbookConfig(symbol: string) → Promise<OrderbookConfig>

獲取指定交易對的訂單簿顯示配置。若後端無此端點,可直接回傳預設值。

回傳

{
  "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

必要
getOrders(params: object) → Promise<{ orders: Order[] }>

獲取訂單列表,支援按狀態、交易對、數量篩選。

params 參數

欄位類型必要說明
statusstring可選"open" / "closed" / "cancelled"
limitnumber可選回傳數量上限,預設 50
symbolstring可選篩選指定交易對

回傳

{
  "orders": [ /* Order 物件陣列,見下方 Order 規格 */ ]
}

createOrder

必要
createOrder(data: object) → Promise<Order>

建立新訂單,回傳已建立的 Order 物件。

data 欄位

欄位類型必要說明
symbolstring必要交易對,如 BTCUSDT
sidestring必要"buy""sell"
typestring必要"market" / "limit" / "stop" / "liquidity"
quantitynumber必要下單數量(base asset)
pricenumber可選限價單指定價格
stopPricenumber可選止損單觸發價格
leveragenumber可選槓桿倍數,預設 1
takeProfitnumber可選止盈價格
stopLossnumber可選止損價格
metaobject可選自定義元數據,如 { pairId: "uuid" }
liquidityConfigobject可選流動性訂單配置,見下方說明
priceModestring可選價格模式:"fixed""percent"
pricePercentnumber可選相對市價的百分比偏移(priceMode=percent 時使用)

liquidityConfig 欄位

{
  "levelOffset": 1,       // 深度層級 1-19,1 為最緊點差
  "pricePercent": 0.5     // 距市價的百分比(可選)
}

cancelOrder

必要
cancelOrder(orderId: string) → Promise<void>

撤銷指定 ID 的訂單。


cancelAllLiquidity

可選
cancelAllLiquidity(symbol?: string) → Promise<{ cancelled: number }>

批量撤銷所有流動性訂單。symbol 為可選,不傳則撤銷所有交易對的流動性訂單。 此方法為可選,若後端不支援,可省略或回傳 { cancelled: 0 }

回傳

{
  "cancelled": 4    // 實際撤銷的訂單數量
}

Order 物件

所有涉及訂單的方法(getOrderscreateOrder)都使用統一的 Order 物件格式。

欄位類型說明
idstring訂單唯一 ID
symbolstring交易對
sidestring"buy" / "sell"
typestring"market" / "limit" / "stop" / "liquidity"
statusstring"open" / "filled" / "partial" / "cancelled"
quantitynumber下單數量
filledQuantitynumber已成交數量
closedQuantitynumber已平倉數量
pricenumber掛單價格(市價單為 null)
averagePricenumber平均成交價格
leveragenumber使用槓桿倍數
isLiquidityOrderboolean是否為流動性做市訂單
isHedgeOrderboolean是否為對沖訂單
liquidityLevelOffsetnumber流動性深度層級(1–19)
liquidityPricePercentnumber流動性價格百分比偏移
pnlnumber已實現盈虧(平倉後)
createdAtstring建立時間 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);
}
⚠️
WeChart 通過 error.status === 401 判斷是否需要重新登入。 確保所有 401 錯誤都正確設置了 status 屬性。