Skip to Content
🚀 Spot API代码示例

代码示例

📋 目录


💻 代码示例

JavaScript 示例
const https = require('https'); const crypto = require('crypto'); const { URL } = require('url'); /** 生成随机字符串 */ function generateNonce(length = 16) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length))).join(''); } /** 对象排序(按 key 升序) */ function sortObject(obj) { if (Array.isArray(obj)) { return obj.map(sortObject); } else if (typeof obj === 'object' && obj !== null) { return Object.keys(obj) .sort() .reduce((acc, key) => { acc[key] = sortObject(obj[key]); return acc; }, {}); } return obj; } /** 扁平化对象 */ function flattenObject(obj, parentKey = '', result = {}) { if (typeof obj !== 'object' || obj === null) return result; for (const [key, value] of Object.entries(obj)) { if (value == null) continue; const newKey = parentKey ? `${parentKey}.${key}` : key; if (Array.isArray(value)) { value.forEach((v, i) => flattenObject(v, `${newKey}[${i}]`, result)); } else if (typeof value === 'object') { flattenObject(value, newKey, result); } else { if (String(value).trim() !== '') result[newKey] = String(value); } } return result; } /** 对 query 参数排序 */ function sortQueryParams(query) { if (!query) return ''; const pairs = query.split('&').filter(Boolean); const params = pairs.map(p => p.split('=')).filter(p => p.length === 2); params.sort((a, b) => a[0].localeCompare(b[0])); return params.map(([k, v]) => `${k}=${v}`).join('&'); } /** 生成签名 */ function generateSignature(method, url, body, secret, timestamp, nonce) { const urlObj = new URL(url); let query = sortQueryParams(urlObj.search.slice(1)); let bodyData = ''; if (['POST', 'PUT'].includes(method.toUpperCase()) && body) { const sortedBody = sortObject(body); const flatBody = flattenObject(sortedBody); bodyData = Object.entries(flatBody) .map(([k, v]) => `${k}=${v}`) .join('&'); } // ✅ Java 的拼接规则: // POST: (bodyData.isEmpty() ? "" : bodyData + "&") + "timestamp=xxx&nonce=xxx" // GET: (query.isEmpty() ? "&" : query + "&") + "timestamp=xxx&nonce=xxx" let signData = ''; if (method.toUpperCase() === 'GET') { signData = (query ? `${query}&` : '&') + `timestamp=${timestamp}&nonce=${nonce}`; } else { signData = (bodyData ? `${bodyData}&` : '') + `timestamp=${timestamp}&nonce=${nonce}`; } // ⚠️ 不做 URL encode const hmac = crypto.createHmac('sha256', secret); hmac.update(signData); return hmac.digest('hex'); } /** 发起 GET 请求 */ function sendGetRequest(url, headers) { return new Promise((resolve, reject) => { const req = https.get(url, { headers }, res => { let data = ''; res.on('data', chunk => (data += chunk)); res.on('end', () => { console.log('状态码:', res.statusCode); console.log('响应:', data); resolve(data); }); }); req.on('error', reject); }); } /** 示例调用 */ (async () => { const apiKey = '8aB3yCz4QpW9sT7rM0dN2uE5kH1xJ6fVtLqO8bRzP3cS9yD0mG4nF7aU'; const apiSecret = 'W7eR1tF6pA2yQ3dK5sM9xL8bG4jC0vHnUoOzTqP1rE5iN2mS7uY6aZ0l'; const url = 'https://openapi.bittap.com/stapi/v1/account'; const method = 'GET'; const timestamp = Date.now(); const nonce = generateNonce(); const sign = generateSignature(method, url, null, apiSecret, timestamp, nonce); const headers = { 'X-BT-APIKEY': apiKey, 'X-BT-SIGN': sign, 'X-BT-TS': timestamp, 'X-BT-NONCE': nonce, }; console.log('签名字符串:', sign); await sendGetRequest(url, headers); })();

Java 示例
public class OpenApiDemo { private static final String API_KEY = ""; private static final String API_SECRET = ""; private static final String BASE_URL = "https://openapi.bittap.com"; private static final String SEPARATOR = "&"; private static final String EQUALS = "="; private static final OkHttpClient CLIENT = new OkHttpClient(); private static final ObjectMapper MAPPER = new ObjectMapper(); public static void main(String[] args) throws Exception { // 批量下单 testBatchCreate(); // 撤销订单 testCancelOrder(); // 批量撤销 testBatchCancel(); // 查询挂单 testOpenOrders(); // 查询历史订单 testHisOrders(); // 查询成交记录 testTrades(); // 查询账户余额 testAccount(); } // ----------------- 接口调用 ------------------- // 1. 批量下单 public static void testBatchCreate() throws Exception { String url = BASE_URL + "/stapi/v1/batchOrders"; ObjectNode body = MAPPER.createObjectNode(); body.put("symbol", "BTC-USDT"); ArrayNode items = body.putArray("items"); ObjectNode order1 = MAPPER.createObjectNode(); order1.put("clientOrderId", UUID.randomUUID().toString().replace("-", "")); order1.put("type", "LIMIT"); order1.put("price", "100002"); order1.put("quantity", "0.0011"); order1.put("timeInForce", "GTC"); order1.put("selfTradePreventionMode", "EXPIRE_BOTH"); order1.put("side", "BUY"); items.add(order1); ObjectNode order2 = MAPPER.createObjectNode(); order2.put("clientOrderId", UUID.randomUUID().toString().replace("-", "")); order2.put("type", "LIMIT"); order2.put("price", "100002"); order2.put("quantity", "0.0011"); order2.put("timeInForce", "GTC"); order2.put("selfTradePreventionMode", "EXPIRE_BOTH"); order2.put("side", "BUY"); items.add(order2); sendSignedPost(url, body); } // 2. 撤销订单 public static void testCancelOrder() throws Exception { String url = BASE_URL + "/stapi/v1/cancelOrder"; ObjectNode body = MAPPER.createObjectNode(); body.put("clientOrderId", "00295c0dadbb47a5b5a0d3fe4ad83fc9"); sendSignedPost(url, body); } // 3. 批量撤销订单 public static void testBatchCancel() throws Exception { String url = BASE_URL + "/stapi/v1/batchCancelOrders"; ObjectNode body = MAPPER.createObjectNode(); body.put("symbol", "BTC-USDT"); sendSignedPost(url, body); } // 4. 查询挂单 public static void testOpenOrders() throws Exception { String url = BASE_URL + "/stapi/v1/openOrders"; sendSignedGet(url); } // 5. 查询历史订单 public static void testHisOrders() throws Exception { String url = BASE_URL + "/stapi/v1/hisOrders?symbol=BTC-USDT"; sendSignedGet(url); } // 6. 查询成交记录 public static void testTrades() throws Exception { String url = BASE_URL + "/stapi/v1/trades?pageNumber=1&pageSize=10"; sendSignedGet(url); } // 7. 查询账户余额 public static void testAccount() throws Exception { String url = BASE_URL + "/stapi/v1/account"; sendSignedGet(url); } // ----------------- 工具方法 ------------------- private static void sendSignedPost(String url, ObjectNode body) throws Exception { long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replace("-", ""); // 1. 排序 JSON JsonNode sortedNode = sortJsonNode(body); // 1. 扁平化 body String bodyData = convertJsonToQueryString(sortedNode); // 2. 拼接签名串 String signData = (bodyData.isEmpty() ? "" : bodyData + "&") + "timestamp=" + timestamp + "&nonce=" + nonce; String sign = hmacSha256Hex(signData, API_SECRET); String bodyJson = MAPPER.writeValueAsString(body); RequestBody requestBody = RequestBody.create( bodyJson, MediaType.parse("application/json; charset=utf-8")); Request request = new Request.Builder() .url(url) .post(requestBody) .addHeader("X-BT-APIKEY", API_KEY) .addHeader("X-BT-SIGN", sign) .addHeader("X-BT-TS", String.valueOf(timestamp)) .addHeader("X-BT-NONCE", nonce) .build(); try (Response response = CLIENT.newCall(request).execute()) { System.out.println("POST " + url); System.out.println("Response: " + response.code() + " " + (response.body() != null ? response.body().string() : "")); } } private static void sendSignedGet(String url) throws Exception { long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replace("-", ""); // 从 URL 中解析已有 query String query = ""; int idx = url.indexOf("?"); if (idx != -1) { query = url.substring(idx + 1); } query = sortQueryParams(query); String signData = (query.isEmpty() ? "&" : query + "&") + "timestamp=" + timestamp + "&nonce=" + nonce; String sign = hmacSha256Hex(signData, API_SECRET); String finalUrl = url; Request request = new Request.Builder() .url(finalUrl) .get() .addHeader("X-BT-APIKEY", API_KEY) .addHeader("X-BT-SIGN", sign) .addHeader("X-BT-TS", String.valueOf(timestamp)) .addHeader("X-BT-NONCE", nonce) .build(); try (Response response = CLIENT.newCall(request).execute()) { System.out.println("GET " + finalUrl); System.out.println("Response: " + response.code() + " " + (response.body() != null ? response.body().string() : "")); } } private static String convertJsonToQueryString(JsonNode jsonNode) { if (jsonNode == null || jsonNode.isNull()) { return ""; } MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); flattenJson(jsonNode, "", params); StringBuilder queryString = new StringBuilder(); boolean first = true; for (Map.Entry<String, String> entry : params.toSingleValueMap().entrySet()) { if (!first) { queryString.append("&"); } else { first = false; } queryString.append(entry.getKey()).append("=").append(entry.getValue()); } return queryString.toString(); } private static void flattenJson(JsonNode node, String path, MultiValueMap<String, String> params) { if (node.isObject()) { Iterator<Map.Entry<String, JsonNode>> fields = node.fields(); while (fields.hasNext()) { Map.Entry<String, JsonNode> field = fields.next(); if (field.getValue().isNull()) { continue; } String newPath = path.isEmpty() ? field.getKey() : path + "." + field.getKey(); flattenJson(field.getValue(), newPath, params); } } else if (node.isArray()) { for (int i = 0; i < node.size(); i++) { JsonNode jsonNode = node.get(i); if (jsonNode.isNull() || jsonNode.isArray() && jsonNode.isEmpty()) { continue; } String newPath = path + "[" + i + "]"; flattenJson(node.get(i), newPath, params); } } else { // 基本类型 if (node.asText().isBlank()) { return; } params.add(path, node.asText()); } } private static String hmacSha256Hex(String data, String secret) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); mac.init(keySpec); byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(raw.length * 2); for (byte b : raw) sb.append(String.format("%02x", b)); return sb.toString(); } private static JsonNode sortJsonNode(JsonNode node) { try { if (node == null || node.isNull()) return node; if (node.isObject()) { ObjectNode out = MAPPER.createObjectNode(); TreeMap<String, JsonNode> sorted = new TreeMap<>(); Iterator<Map.Entry<String, JsonNode>> fields = node.fields(); while (fields.hasNext()) { Map.Entry<String, JsonNode> f = fields.next(); sorted.put(f.getKey(), sortJsonNode(f.getValue())); } for (Map.Entry<String, JsonNode> e : sorted.entrySet()) { out.set(e.getKey(), e.getValue()); } return out; } else if (node.isArray()) { ArrayNode arr = MAPPER.createArrayNode(); for (JsonNode el : node) arr.add(sortJsonNode(el)); return arr; } else { return node; } } catch (Exception ex) { return node; } } private static String sortQueryParams(String query) { if (StringUtils.isBlank(query)) { return ""; } try { // 使用 LinkedMultiValueMap 处理相同 key 的参数 MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>(); String[] paramPairs = query.split(SEPARATOR); for (String pair : paramPairs) { String[] keyValue = pair.split(EQUALS, 2); if (keyValue.length == 2) { paramMap.add(keyValue[0], keyValue[1]); } } // 将 MultiValueMap 转换为 Map,相同 key 的值转为数组 Map<String, Object> convertedMap = new LinkedHashMap<>(); for (Map.Entry<String, List<String>> entry : paramMap.entrySet()) { String key = entry.getKey(); List<String> values = entry.getValue(); if (values.size() == 1) { convertedMap.put(key, values.get(0)); } else { convertedMap.put(key, values); } } // 使用 Jackson 将 Map 转换为 JsonNode JsonNode jsonNode = MAPPER.valueToTree(convertedMap); // 对 JsonNode 进行排序 JsonNode sortedJsonNode = sortJsonNode(jsonNode); // 将排序后的 JsonNode 转换为查询字符串 return convertJsonToQueryString(sortedJsonNode); } catch (Exception e) { return query; } } }

Python 示例
import requests import json import time import hmac import hashlib import uuid from urllib.parse import urlparse, parse_qsl, urlencode ### --- 配置 --- ### 请替换为您的 API Key 和 Secret API_KEY = "" API_SECRET = "" BASE_URL = "https://openapi.bittap.com" ### --- 工具方法 --- def sort_dict(d): """递归地按键对字典进行排序""" if not isinstance(d, dict): return d sorted_dict = {} for k in sorted(d.keys()): if isinstance(d[k], dict): sorted_dict[k] = sort_dict(d[k]) elif isinstance(d[k], list): # 递归排序列表中的字典 sorted_dict[k] = [sort_dict(i) if isinstance(i, dict) else i for i in d[k]] else: sorted_dict[k] = d[k] return sorted_dict def flatten_dict(d, parent_key='', sep='.'): """将嵌套字典扁平化""" items = [] for k, v in d.items(): new_key = parent_key + sep + k if parent_key else k if v is None: continue if isinstance(v, dict): items.extend(flatten_dict(v, new_key, sep=sep).items()) elif isinstance(v, list): for i, item in enumerate(v): # 忽略空值或空数组 if item is None or (isinstance(item, list) and not item): continue if isinstance(item, dict): items.extend(flatten_dict(item, f"{new_key}[{i}]", sep=sep).items()) else: if str(item).strip(): items.append((f"{new_key}[{i}]", str(item))) else: # 忽略空白字符串 if str(v).strip(): items.append((new_key, str(v))) return dict(items) def dict_to_query_string(d): """将字典转换为查询字符串""" return '&'.join([f"{k}={v}" for k, v in d.items()]) def sort_query_params(query_string): """按字母顺序对查询参数进行排序""" if not query_string: return "" params = parse_qsl(query_string, keep_blank_values=True) params.sort(key=lambda x: x[0]) return urlencode(params) def hmac_sha256_hex(data, secret): """生成 HMAC-SHA256 签名""" return hmac.new(secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest() ### --- 请求发送方法 --- def send_signed_post(endpoint, body): """发送带签名的 POST 请求""" url = BASE_URL + endpoint timestamp = int(time.time() * 1000) nonce = str(uuid.uuid4()).replace('-', '') # 1. 对 JSON body 进行排序 sorted_body = sort_dict(body) # 2. 扁平化 body 并转换为查询字符串 flat_body = flatten_dict(sorted_body) body_data = dict_to_query_string(flat_body) # 3. 构造签名字符串 (POST 请求) sign_data = f"{body_data}&" if body_data else "" sign_data += f"timestamp={timestamp}&nonce={nonce}" # 4. 生成签名 sign = hmac_sha256_hex(sign_data, API_SECRET) headers = { 'X-BT-APIKEY': API_KEY, 'X-BT-SIGN': sign, 'X-BT-TS': str(timestamp), 'X-BT-NONCE': nonce, 'Content-Type': 'application/json; charset=utf-8' } print(f"POST {url}") print(f"Request Body: {json.dumps(body)}") print(f"Signature String: {sign_data}") print(f"Signature: {sign}") try: response = requests.post(url, headers=headers, data=json.dumps(body)) print(f"Response: {response.status_code} {response.text}") response.raise_for_status() except requests.exceptions.RequestException as e: print(f"Error on POST {url}: {e}") def send_signed_get(endpoint): """发送带签名的 GET 请求""" url = BASE_URL + endpoint timestamp = int(time.time() * 1000) nonce = str(uuid.uuid4()).replace('-', '') parsed_url = urlparse(url) query = parsed_url.query # 1. 对查询参数进行排序 sorted_query = sort_query_params(query) # 2. 构造签名字符串 (GET 请求) # 注意: 根据示例代码,如果GET请求没有参数,签名字符串以'&'开头。 # 这与POST请求的行为不一致,但我们在此复制该行为。 sign_data = f"{sorted_query}&" if sorted_query else "&" sign_data += f"timestamp={timestamp}&nonce={nonce}" # 3. 生成签名 sign = hmac_sha256_hex(sign_data, API_SECRET) headers = { 'X-BT-APIKEY': API_KEY, 'X-BT-SIGN': sign, 'X-BT-TS': str(timestamp), 'X-BT-NONCE': nonce, } print(f"GET {url}") print(f"Signature String: {sign_data}") print(f"Signature: {sign}") try: response = requests.get(url, headers=headers) print(f"Response: {response.status_code} {response.text}") response.raise_for_status() except requests.exceptions.RequestException as e: print(f"Error on GET {url}: {e}") ### --- API 调用测试方法 --- def test_batch_create(): """1. 批量下单""" endpoint = "/stapi/v1/batchOrders" body = { "symbol": "BTC-USDT", "items": [ { "clientOrderId": str(uuid.uuid4()).replace('-', ''), "type": "LIMIT", "price": "100002", "quantity": "0.0011", "timeInForce": "GTC", "selfTradePreventionMode": "EXPIRE_BOTH", "side": "BUY" }, { "clientOrderId": str(uuid.uuid4()).replace('-', ''), "type": "LIMIT", "price": "100002", "quantity": "0.0011", "timeInForce": "GTC", "selfTradePreventionMode": "EXPIRE_BOTH", "side": "BUY" } ] } send_signed_post(endpoint, body) def test_cancel_order(): """2. 撤销订单""" endpoint = "/stapi/v1/cancelOrder" body = { "clientOrderId": "00295c0dadbb47a5b5a0d3fe4ad83fc9" } send_signed_post(endpoint, body) def test_batch_cancel(): """3. 批量撤销订单""" endpoint = "/stapi/v1/batchCancelOrders" body = { "symbol": "BTC-USDT" } send_signed_post(endpoint, body) def test_open_orders(): """4. 查询挂单""" endpoint = "/stapi/v1/openOrders" send_signed_get(endpoint) def test_his_orders(): """5. 查询历史订单""" endpoint = "/stapi/v1/hisOrders?symbol=BTC-USDT" send_signed_get(endpoint) def test_trades(): """6. 查询成交记录""" endpoint = "/stapi/v1/trades?pageNumber=1&pageSize=10" send_signed_get(endpoint) def test_account(): """7. 查询账户余额""" endpoint = "/stapi/v1/account" send_signed_get(endpoint) ### --- 主执行逻辑 --- if __name__ == "__main__": if not API_KEY or not API_SECRET: print("错误: 请在代码中设置您的 API_KEY 和 API_SECRET。") else: print("--- 1. 测试批量下单 ---") test_batch_create() print("\n--- 2. 测试撤销订单 ---") test_cancel_order() print("\n--- 3. 测试批量撤销 ---") test_batch_cancel() print("\n--- 4. 测试查询挂单 ---") test_open_orders() print("\n--- 5. 测试查询历史订单 ---") test_his_orders() print("\n--- 6. 测试查询成交记录 ---") test_trades() print("\n--- 7. 测试查询账户余额 ---") test_account()

🔍 常见问题

Q: 如何生成随机字符串?

A: 可以使用以下方法:

  • JavaScript: 使用 crypto.randomUUID() 或自定义函数
  • Java: 使用 UUID.randomUUID().toString().replace("-", "")
Q: 如何处理嵌套对象?

A: 使用递归函数将嵌套对象扁平化,使用点号分隔层级关系。

Q: 数组参数如何表示?

A: 数组参数使用方括号索引表示,如 items[0].price=100&items[1].price=200

Q: 签名验证失败怎么办?

A: 请检查:

  1. 参数排序是否正确
  2. 时间戳是否在有效期内
  3. nonce 是否重复使用
  4. API Secret 是否正确
Q: 支持哪些编程语言?

A: 我们提供了 JavaScript 和 Java 的完整示例,其他语言可以参考签名算法自行实现。


📚 依赖说明

JavaScript 依赖
{ "dependencies": { "crypto": "^1.0.1" } }
Java 依赖
<dependencies> <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.11.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>6.0.11</version> </dependency> </dependencies>

🚀 快速开始

  1. 选择编程语言: 根据您的项目需求选择 JavaScript 或 Java
  2. 配置环境: 安装必要的依赖包
  3. 设置密钥: 配置您的 API Key 和 Secret
  4. 运行示例: 执行示例代码验证签名算法
  5. 集成到项目: 将签名逻辑集成到您的实际项目中

📖 更多资源

最后更新于: