槓桿全球贏家 Web API 使用指南
本頁將 PDF 規格文件(v4.12)翻譯成可直接執行的 Python 範例,涵蓋全部 14 個 API 功能。不懂程式也能對照使用。
申請 API Key
使用 API 前,需先向元大期貨申請 API Key。申請完成後會以簡訊通知,簡訊內含 API Key 與 API 密碼,即為程式中填入的帳號資訊。
申請完成後的簡訊範例
簡訊欄位對照表
| 簡訊欄位 | 對應程式變數 | 填入位置 |
|---|---|---|
| API Key(藍色) ABCDEF0ECB394776ADDBA88CB3254321 |
API_KEY | ltm_client.py 第7行 |
| API密碼(綠色) Ss12345# |
PASSWORD | ltm_client.py 第8行 |
| 帳號 1041234 |
ACCOUNT | ltm_client.py 第9行 |
架構說明
這個 API 使用兩個同時運作的通道,與一般只有 REST 的 API 不同,請務必理解:
(送出指令)
(接收回應)
API 端點
| 用途 | 位址 |
|---|---|
| REST 請求(登入、下單、查詢) | https://webltm.yuantafutures.com.tw/WEBTRADER/rest |
| WebSocket(接收報價與回報) | wss://webltm.yuantafutures.com.tw/WEBTRADER/ws |
正確連線流程
安裝設定
error 120: Signature is required。
必須提供 .pfx 憑證檔案,透過 RSA-SHA256 簽名才能成功登入。詳見 1.1 REST 登入 說明。
安裝必要套件
# 純 Python 腳本用(ltm_client.py)
pip install requests websocket-client cryptography
# 本機代理伺服器用(server.py + 瀏覽器前台)
pip install flask flask-socketio requests websocket-client cryptography
下載完整範例檔
請到本頁底部 完整 Python 檔案 區域,複製整份程式碼存成 ltm_client.py。
修改帳號設定(程式最上方)
# ================================================
# 【必填】請把以下資訊改成你自己的帳號
# ================================================
API_KEY = "ABCDEF0ECB394776ADDBA88CB3254321" # 從元大後台取得的 API Key
PASSWORD = "Ss12345#" # API Key 對應的密碼
ACCOUNT = "1041234" # 交易帳戶號碼
PFX_PATH = r"C:\路徑\你的憑證.pfx" # CA 憑證路徑(必填)
PFX_PASS = "憑證密碼" # .pfx 開啟密碼(沒有則留空)
本機代理伺服器設定(瀏覽器前台)
因為瀏覽器無法直接建立 WSS WebSocket 到元大伺服器(跨域限制),本專案提供一個 server.py 本機代理,讓瀏覽器操作 API。
python server.py,看到「Running on http://127.0.0.1:5000」表示成功http://localhost:5000,輸入帳號密碼即可登入交易前台# ══════════════════════════════════════════════════════════
# ★ 使用前必填:CA 憑證設定
#
# PFX_PATH : .pfx 檔案的完整路徑(Windows 路徑前加 r)
# PFX_PASSWORD : 開啟 .pfx 的密碼(沒有密碼則留空字串 "")
# ══════════════════════════════════════════════════════════
PFX_PATH = r"C:\Users\你的名字\Desktop\憑證\9813377.pfx"
PFX_PASSWORD = "你的憑證密碼"
快速開始(5分鐘跑起來)
from ltm_client import LTMClient
# 建立客戶端
client = LTMClient()
# 1. 登入(REST + WebSocket 一起完成)
client.start()
# 2. 查詢帳戶餘額
acct = client.get_account_info()
bal = acct.get("AcctVal", {}).get("bal", 0)
print(f"帳戶餘額: {bal:,.2f}")
# 3. 查詢目前部位
pos = client.get_positions()
for p in pos.get("PositionAry", []):
print(f" 商品: {p['sym']} 方向: {'買' if p['type']==1 else '賣'} 數量: {p['qty']}")
# 4. 訂閱 EUR/USD 即時報價(報價會自動印出)
client.subscribe_quote(["EUR/USD"])
import time; time.sleep(10) # 等 10 秒看報價
# 5. 下一張 EUR/USD 市價買單(1手 = 100000)
result = client.place_market_order("EUR/USD", side="1", qty=100000)
print("下單回應:", result)
# 保持程式執行(持續接收 WebSocket 推播)
client.run_forever()
⚠️ 下單前必讀:10 個常見錯誤
以下是使用此 API 最容易踩到的坑,建議在實際下單前逐條確認。
① qty 是「合約單位」,不是「手數」
送出數量的欄位 qty 填的是合約數量,換算方式依商品類型而不同。最常見的錯誤是把外匯的手數直接填入,或是把黃金的手數搞錯。
| 商品類型 | 手數 | 應填 qty 值 | 備註 |
|---|---|---|---|
| 外匯 FX EUR/USD、GBP/USD USD/JPY 等 |
1 手 | 100000 | 標準手(Standard Lot) |
| 0.1 手 | 10000 | 迷你手(Mini Lot) | |
| 0.01 手 | 1000 | 微型手(Micro Lot) | |
| 黃金 XAU/USD |
1 手 | 100 | = 100 盎司 |
| 0.1 手 | 10 | = 10 盎司 | |
| 0.01 手 | 1 | = 1 盎司(最小單位) | |
| 白銀 XAG/USD |
1 手 | 5000 | = 5000 盎司 |
| 0.1 手 | 500 | = 500 盎司 | |
| 0.01 手 | 50 | = 50 盎司(最小單位) | |
| 石油(原油) 西德州 XTI/USD 布蘭特 XBR/USD |
1 手 | 1000 | = 1000 桶 |
| 0.1 手 | 100 | = 100 桶 | |
| 0.01 手 | 10 | = 10 桶(最小單位) | |
| CFD 指數 US500、US30、NAS100 等 |
1 口 | 1 | 以各商品 contractSz 為準,建議先呼叫 2.2 查詢商品列表確認 |
qtyDec(數量小數位)和 contractSz(合約規格),即可確認正確的換算比例。
② 停損停利單的方向必須與開倉「相反」
停損停利(type=U)本質上是「預掛的平倉單」,所以方向要反向填寫:
| 開倉方向 | 停損停利單 side 要填 |
|---|---|
| 買進(side=1) | 2(賣出) |
| 賣出(side=2) | 1(買進) |
填錯方向伺服器不一定會報錯,但停損停利不會正確觸發,造成無保護的持倉。
③ 停損停利的 px 和 px2 各代表不同意義
Threshold 型(type=U)的訂單,px 和 px2 的意思不直觀:
| 欄位 | 代表意義 | 範例(EUR/USD 買在 1.0850) |
|---|---|---|
| px | 下限觸發價(即停損) 價格跌破此值時觸發 |
填 1.0800(停損在下方) |
| px2 | 上限觸發價(即停利) 價格漲過此值時觸發 |
填 1.0900(停利在上方) |
賣出部位則相反:px 為上方停損,px2 為下方停利。
④ 市價單的 px 固定填 "0",不要填市場價
下市價單(type=1)時,px 欄位必須填字串 "0",不能填目前的市場報價,否則伺服器會拒絕。
# ✅ 正確:市價單 px 填 "0"
"px": "0", "type": "1"
# ❌ 錯誤:不要填目前報價
"px": "1.0850", "type": "1" # 伺服器會拒絕
⑤ tktType 填錯會收到 Error 108
tktType 指定這張單是「開倉」還是「平倉」,填錯會被伺服器拒絕並回傳 error 108: Incorrect To Open Or To Close。
| tktType 值 | 用途 | 同時要填 |
|---|---|---|
| "0" | 開倉(新建部位) | tktNo="", refTktNo="" |
| "1" | 平倉 / 停損停利單 | tktNo=持倉票號, refTktNo=持倉票號 |
⑥ 修改或撤銷停損停利,需先查到 clOrdId
修改停損停利(OrdCxlRepReq)或撤單(OrdCxlReq)時,必須提供現有委託的 clOrdId(委託單 ID)。這個值無法自己決定,需先查詢:
⑦ REST 登入密碼要 SHA-256 加密;WebSocket 登入用明文
| 登入方式 | 密碼格式 |
|---|---|
| REST Login | SHA-256 雜湊後的字串(程式已自動處理) |
| WebSocket Login | 明文密碼(直接填原始密碼) |
使用本頁提供的 LTMClient 類別時,兩種格式都已自動處理,無需手動轉換。
⑧ txTime 使用 UTC 時間,台灣需減 8 小時
txTime 欄位使用 UTC(格林威治標準時間),格式為 YYYYMMDD-HH:MM:SS.mmm。台灣時間(UTC+8)需減去 8 小時。
from datetime import datetime, timezone
# ✅ 正確:使用 UTC 時間
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
# ❌ 錯誤:用本地時間會快 8 小時
now = datetime.now().strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
⑨ WebSocket 每條訊息都必須立即回 ACK
從 WebSocket 收到任何訊息,必須立刻回傳 ACK,包含報價、回報、帳戶更新等所有類型。如果沒有回 ACK,伺服器會認為連線異常並可能切斷連線。
def on_message(ws, message):
data = json.loads(message)
# 每條訊息收到後,第一件事就是回 ACK
ws.send(json.dumps({"MT": "Ack"}))
# 之後再處理訊息內容...
本頁提供的 LTMClient 已在 on_message 中自動處理 ACK,無需額外設定。
⑩ 商品代碼格式與 secType 必須對應正確
XAU/USD(黃金)
XAG/USD(白銀)
XTI/USD(西德州原油)
XBR/USD(布蘭特原油)
US30(道瓊)
NAS100(那斯達克)
| 商品類型 | secType 填 | 商品代碼格式 | 範例 |
|---|---|---|---|
| 外匯 | "FOR" | 有斜線 CUR1/CUR2 | EUR/USD、GBP/USD、USD/JPY |
| 黃金/白銀 | "FOR" | 有斜線 CUR1/CUR2 | XAU/USD、XAG/USD |
| 石油(原油) | "FOR" | 有斜線 CUR1/CUR2 | XTI/USD(西德州)、XBR/USD(布蘭特) |
| CFD 指數 | "CFD" | 無斜線,直接代碼 | US500、US30、NAS100 |
| 遠期外匯 | "FORWARD" | 有斜線 CUR1/CUR2 | EUR/USD(搭配 tenor 參數) |
XAUUSD ✗ → 應為 XAU/USD ✓EURUSD ✗ → 應為 EUR/USD ✓XTIUSD ✗ → 應為 XTI/USD ✓斜線省略伺服器會找不到商品,回傳找不到 symbol 的錯誤。
1.1 REST 登入(CA SignedReq)
此伺服器啟用 CA 驗證,必須使用 SignedReq 模式登入。流程:用 .pfx 私鑰對登入訊息做 RSA-SHA256 簽名 → POST SignedReq → 取回加密 Token → RSA 解密得到真正 Token。
signerId 欄位必須填交易帳戶號碼(如 9814884),填錯會回傳 error 121 "Certificate is missing"。
SignedReq 請求 JSON
{
"MT": "SignedReq",
"rawReq": "{\"MT\":\"Login\",\"UserInfo\":{\"login\":\"API_KEY\",\"password\":\"base64(SHA256(pwd))\"}}",
"signerId": "9814884",
"password": "明文密碼",
"signature": "base64(RSA-SHA256簽名)"
}
欄位說明
| 欄位 | 必填 | 說明 |
|---|---|---|
| MT | 必填 | 固定填 "SignedReq" |
| rawReq | 必填 | 原始 Login JSON 字串,密碼用 base64(SHA256(pwd)) |
| signerId | 必填 | 交易帳戶號碼(不是 API Key,不是憑證 CN) |
| password | 必填 | API 密碼(明文) |
| signature | 必填 | 用 .pfx 私鑰對 rawReq 做 RSA-SHA256 簽名後 base64 |
回應:伺服器返回加密 Token,需用私鑰解密
Tok 是 base64(RSA加密後的token),約 344 字元。必須用 .pfx 私鑰 RSA 解密,得到真正的 session token(16~32 字元十六進位字串),再用此 token 登入 WebSocket。
Python 完整 CA 登入範例
import requests, hashlib, base64, json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.serialization import pkcs12
REST_URL = "https://webltm.yuantafutures.com.tw/WEBTRADER/rest"
API_KEY = "ABCDEF0ECB394776ADDBA88CB3254321"
PASSWORD = "Ss12345#"
ACCOUNT = "1041234" # 交易帳戶號碼(signerId 用這個)
PFX_PATH = r"C:\路徑\憑證.pfx"
PFX_PASS = "憑證密碼"
def ca_login():
# 1. 載入 .pfx
with open(PFX_PATH, "rb") as f:
pfx_bytes = f.read()
private_key, cert, _ = pkcs12.load_key_and_certificates(
pfx_bytes, PFX_PASS.encode() if PFX_PASS else None
)
# 2. 建構 rawReq(密碼用 base64(SHA256))
pw_b64 = base64.b64encode(hashlib.sha256(PASSWORD.encode()).digest()).decode()
raw_obj = {"MT": "Login", "UserInfo": {"login": API_KEY, "password": pw_b64}}
raw_req = json.dumps(raw_obj, separators=(",", ":"))
# 3. RSA-SHA256 簽名
sig_b64 = base64.b64encode(
private_key.sign(raw_req.encode(), asym_padding.PKCS1v15(), hashes.SHA256())
).decode()
# 4. POST SignedReq
resp = requests.post(REST_URL, json={
"MT": "SignedReq",
"rawReq": raw_req,
"signerId": ACCOUNT, # ← 帳戶號碼,不是 API Key
"password": PASSWORD,
"signature": sig_b64,
}, timeout=10)
data = resp.json()
if data.get("MT") == "Logout" or "ErrInfo" in data:
raise RuntimeError(f"登入失敗: {data}")
# 5. RSA 解密 Token
enc_tok = base64.b64decode(data["Tok"] + "==")
token = private_key.decrypt(enc_tok, asym_padding.PKCS1v15()).decode()
print(f"✅ CA 登入成功,token={token[:12]}...")
return token
token = ca_login()
1.2 WebSocket 登入
REST 登入成功後,立即用取得的 Token 建立 WebSocket 連線並完成登入。之後所有回報都從這條連線推送。
請求 JSON(透過 WebSocket 送出)
{
"MT": "Login",
"UserInfo": {
"login": "ABCDEF0ECB394776ADDBA88CB3254321",
"password": "Ss12345#"
},
"Tok": "525197be1b9c44c98a2aef5d39f66ce9"
}
欄位說明
| 欄位 | 必填 | 說明 |
|---|---|---|
| MT | 必填 | 固定填 "Login" |
| UserInfo.login | 必填 | API Key |
| UserInfo.password | 必填 | 密碼(WebSocket 登入可用明文) |
| Tok | 必填 | REST 登入拿到的 Token |
Python 範例(REST + WebSocket 完整登入流程)
import websocket
import json, threading, time
WS_URL = "wss://webltm.yuantafutures.com.tw/WEBTRADER/ws"
API_KEY = "ABCDEF0ECB394776ADDBA88CB3254321"
PASSWORD = "Ss12345#"
ws_app = None # 全域 WebSocket 連線
def start_websocket(token):
global ws_app
def on_open(ws):
print("🔌 WebSocket 已連線,正在登入...")
login_msg = {
"MT": "Login",
"UserInfo": {"login": API_KEY, "password": PASSWORD},
"Tok": token
}
ws.send(json.dumps(login_msg))
def on_message(ws, message):
data = json.loads(message)
# 每收到一條訊息,必須立即回傳 ACK
ws.send(json.dumps({"MT": "Ack"}))
mt = data.get("MT", "")
if mt == "Login":
print("✅ WebSocket 登入成功")
elif mt == "ExecRp":
# 下單後的執行回報
print(f"📋 執行回報:\n{json.dumps(data, ensure_ascii=False, indent=2)}")
elif mt == "Q":
# 即時報價
q = data.get("Q", {})
print(f"💹 {q.get('s')} 買={q.get('b')} 賣={q.get('a')}")
elif mt == "GetAcctInfo":
bal = data.get("AcctVal", {}).get("bal")
print(f"💰 帳戶餘額: {bal}")
def on_error(ws, error):
print(f"❌ WebSocket 錯誤: {error}")
def on_close(ws, code, msg):
print("🔌 WebSocket 已斷線")
ws_app = websocket.WebSocketApp(
WS_URL,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
# 在背景執行緒中運行,不阻塞主程式
t = threading.Thread(target=ws_app.run_forever, kwargs={"ping_interval": 30})
t.daemon = True
t.start()
time.sleep(2) # 等待連線完成
return ws_app
# 使用方式
token = rest_login() # 先 REST 登入
ws = start_websocket(token) # 再建立 WebSocket
1.3 取得伺服器列表
查詢所有可用的接入伺服器,選擇延遲最低的那台,可提升交易速度。
Python 範例
def get_access_server_info(token):
payload = {
"MT": "GetAccessServerInfo",
"Tok": token
}
resp = requests.post(REST_URL, json=payload, timeout=10)
return resp.json()
# 伺服器列表會從 WebSocket 推送過來
result = get_access_server_info(token)
print(result)
1.4 Echo 測試
送一個訊息給伺服器,伺服器原樣回傳。可用來測試連線是否正常,或計算來回延遲時間。
from datetime import datetime
def echo_test(token):
now = datetime.utcnow().strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "Echo",
"Tok": token,
"SendTime": now # 自訂欄位,伺服器會原樣回傳
}
resp = requests.post(REST_URL, json=payload, timeout=10)
return resp.json()
echo_test(token)
2.1 帳戶資訊
查詢帳戶的餘額、保證金、盈虧等財務資訊。
回應欄位說明(重要欄位)
| 欄位路徑 | 說明 | 範例 |
|---|---|---|
| AcctVal.bal | 目前帳戶淨值 | 1000000.00 |
| AcctVal.bodBal | 今日開盤餘額 | 998000.00 |
| AcctVal.clsPL | 今日已實現盈虧 | -197.51 |
| AcctVal.rqdMrgn | 所需保證金 | 75125.55 |
| AcctVal.mntnMrgn | 維持保證金 | 37500.00 |
| AcctSetting.mrgnRt | 初始保證金比率 | 0.04(即4%) |
| AcctVal.numTickets | 目前持倉單數 | 3 |
def get_account_info(token, account):
payload = {
"MT": "GetAcctInfo",
"Tok": token,
"UserInfo": {"login": account}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
# 解析重要數字
acct_val = data.get("AcctVal", {})
print(f"帳戶: {account}")
print(f" 淨值: {acct_val.get('bal', 0):,.2f}")
print(f" 已實現盈虧: {acct_val.get('clsPL', 0):,.2f}")
print(f" 所需保證金: {acct_val.get('rqdMrgn', 0):,.2f}")
print(f" 持倉張數: {acct_val.get('numTickets', 0)}")
return data
account_data = get_account_info(token, ACCOUNT)
2.2 商品列表
查詢所有可交易商品,包含外匯(FX)、CFD 指數、金屬等,以及其規格(小數位、點值等)。
商品類型(type 欄位)
| type 值 | 商品類型 | 範例 |
|---|---|---|
| 3 | 外匯(FX) | EUR/USD, GBP/JPY |
| 5 | 金屬 | XAU/USD(黃金) |
| 7 | CFD 指數/股票 | US500, US30 |
def get_symbols(token):
payload = {"MT": "SymListReq", "Tok": token}
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
symbols = data.get("CfgSymAry", [])
print(f"共 {len(symbols)} 個可交易商品:")
for s in symbols[:10]: # 只印前10個
type_name = {3: "外匯", 5: "金屬", 7: "CFD"}.get(s.get("type"), "?")
print(f" {s['sym']:<15} {type_name} 買={s.get('bodBid')} 賣={s.get('bodAsk')}")
return symbols
symbols = get_symbols(token)
2.3 部位查詢
查詢目前持倉中的所有部位(未平倉的交易)。
回應欄位說明
| 欄位 | 說明 | 值域 |
|---|---|---|
| sym | 商品代碼 | EUR/USD |
| qty | 數量 | 正數=買方向,負數=賣方向 |
| openAvgPx | 平均開倉價 | 1.08523 |
| type | 方向 | 1=買開, 2=賣開, 3=買平, 4=賣平 |
| clsPL | 已平倉盈虧 | -197.51 |
def get_positions(token, account):
payload = {
"MT": "NetPosReq",
"Tok": token,
"UserInfo": {"login": account}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
positions = data.get("PositionAry", [])
if not positions:
print("目前無持倉")
return data
print(f"目前持倉(共 {len(positions)} 筆):")
for p in positions:
direction = {1: "買開", 2: "賣開", 3: "買平", 4: "賣平"}.get(p.get("type"), "?")
print(f" {p.get('sym'):<15} {direction} 數量={p.get('qty')} 均價={p.get('openAvgPx')}")
return data
get_positions(token, ACCOUNT)
3.1 即時報價訂閱
透過 WebSocket 訂閱商品的即時買賣價(L1)或市場深度(L2)。訂閱後伺服器會持續推送報價更新。
quote_type 說明
| 值 | 說明 |
|---|---|
| 1 | L1:基本買賣價(Bid/Ask),一般使用這個 |
| 2 | L2:市場深度(多筆掛單價位) |
| 3 | L1 + L2 全部 |
error 5: Unsupported message type。
def subscribe_quote(token, symbols, unsubscribe=False):
"""
訂閱或取消訂閱即時報價
symbols: 商品代碼列表,例如 ["EUR/USD", "GBP/USD", "XAU/USD"]
unsubscribe: True = 取消訂閱
注意:QReq 走 REST(不是 WebSocket),報價更新從 WebSocket 接收
"""
payload = {
"MT": "QReq",
"Tok": token,
"QReq": {
"sym": symbols,
"sub_type": 2 if unsubscribe else 1, # 1=訂閱, 2=取消
"quote_type": 1 # 必填:1=L1基本報價
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
action = "取消訂閱" if unsubscribe else "訂閱"
print(f"✅ 已{action}: {', '.join(symbols)} 回應: {result.get('MT')}")
return result
# 使用範例(REST 送出,WebSocket 接收報價推播)
subscribe_quote(token, ["EUR/USD", "GBP/USD", "XAU/USD"])
# 報價會從 on_message 的 WebSocket 接收,格式如下:
# {"MT": "Q", "Q": {"s": "EUR/USD", "b": 1.08525, "a": 1.08528, "t": "20241118-10:30:00.123"}}
# s=商品 b=買價(Bid) a=賣價(Ask) t=時間
3.2 歷史 K 線
查詢指定商品的歷史 K 線資料(日線或分鐘線)。
欄位說明
| 欄位 | 必填 | 說明 |
|---|---|---|
| sym | 必填 | 商品代碼(如 EUR/USD) |
| type | 必填 | 0=日線,1=分鐘線 |
| days | 必填 | 往前幾天的資料 |
| endTime | 必填 | 結束時間(格式: YYYYMMDD-HH:MM:SS,UTC 時間) |
| groupSize | 選填 | 分鐘線合併(如 5 = 5分鐘K線),僅 type=1 有效 |
| reqId | 必填 | 自訂請求編號,回應中會帶回 |
from datetime import datetime, timezone
def get_chart_history(token, symbol, chart_type=1, days=2, group_size=5):
"""
取得歷史K線
chart_type: 0=日線 1=分鐘線
days: 往前幾天
group_size: 幾根合一(分鐘線用,5=5分鐘K)
"""
end_time = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S")
payload = {
"MT": "ChartHisReq",
"Tok": token,
"ChartHisReq": {
"sym": symbol,
"type": chart_type,
"days": days,
"reqId": 1,
"endTime": end_time,
"groupSize": group_size # 選填,只在分鐘線有效
}
}
resp = requests.post(REST_URL, json=payload, timeout=15)
data = resp.json()
candles = data.get("ChartDataAry", [])
print(f"{symbol} {group_size}分鐘K(最近{days}天,共{len(candles)}根)")
for c in candles[-5:]: # 印最後5根
print(f" {c['t']} 開={c['o']} 高={c['h']} 低={c['l']} 收={c['c']}")
return data
# 取得 EUR/USD 的 5 分鐘 K 線
get_chart_history(token, "EUR/USD", chart_type=1, days=2, group_size=5)
# 取得 EUR/USD 的日線
get_chart_history(token, "EUR/USD", chart_type=0, days=30, group_size=1)
4.1 查詢掛單(未成交委託)
查詢目前還未成交的限價單或停損單。
回應重要欄位
| 欄位 | 說明 |
|---|---|
| sym | 商品代碼 |
| ordtype | 委託類型:1=市價, 2=限價, 3=停損, U=停損停利 |
| side | 方向:1=買, 2=賣 |
| px | 委託價格 |
| ordQty | 委託數量 |
| ordstatus | 狀態:0=掛單中, 1=部分成交, 2=全部成交 |
| clOrdId | 委託單ID(撤單時需要這個值) |
| tktId | 對應的持倉票號 |
def get_open_orders(token, account):
payload = {
"MT": "OpenOrdReq",
"Tok": token,
"UserInfo": {"login": account}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
orders = data.get("OpenOrdAry", [])
if not orders:
print("目前無掛單")
return data
type_map = {"1": "市價", "2": "限價", "3": "停損", "U": "停損停利"}
side_map = {"1": "買", "2": "賣"}
print(f"目前掛單(共 {len(orders)} 筆):")
for o in orders:
print(f" 委託ID={o.get('clOrdId')} {o.get('sym')} "
f"{side_map.get(o.get('side',''),'?')} "
f"{type_map.get(o.get('ordtype',''),'?')} "
f"價={o.get('px')} 量={o.get('ordQty')}")
return data
get_open_orders(token, ACCOUNT)
4.2 查詢成交紀錄
查詢已成交(含已平倉)的歷史交易紀錄。
"YYYY/MM/DD HH:mm:ss",時間為當地時間。
def get_tickets(token, account,
include_open=True,
include_closed=True,
from_date="2025/01/01 00:00:00",
to_date="2025/12/31 23:59:59"):
"""
查詢成交紀錄
include_open: 是否包含未平倉的持倉
include_closed: 是否包含已平倉的紀錄
"""
payload = {
"MT": "GetTickets",
"Tok": token,
"UserInfo": {"login": account},
"GetTickets": {
"inclOp": include_open,
"inclCls": include_closed,
"clsFrom": from_date,
"clsTo": to_date
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
tickets = data.get("TicketAry", [])
print(f"成交紀錄(共 {len(tickets)} 筆):")
for t in tickets:
status = {0: "持倉中", 1: "部分平倉", 2: "已平倉"}.get(t.get("status"), "?")
print(f" TktID={t.get('tktId')} {t.get('sym')} "
f"{'買' if t.get('side')=='B' else '賣'} "
f"開={t.get('opPx')} 量={t.get('opQty') or t.get('clsQty')} "
f"狀態={status}")
return data
# 查詢本月所有紀錄
get_tickets(token, ACCOUNT,
from_date="2025/06/01 00:00:00",
to_date="2025/06/30 23:59:59")
4.3 下市價單
以當前市場價格立即成交。買賣方向:1 = 買進,2 = 賣出。FX 數量:100000 = 1 手。
關鍵參數
| 欄位 | 市價單設定 | 說明 |
|---|---|---|
| type | "1" | 1=市價單 |
| px | "0" | 市價單固定填 0 |
| tktType | "0" | 0=開倉 |
| side | "1" 或 "2" | 1=買, 2=賣 |
| secType | "FOR" | FOR=外匯, CFD=指數, FORWARD=遠期 |
| qty | "100000" | FX:100000=1手, CFD視商品而定 |
from datetime import datetime, timezone
def place_market_order(token, account, symbol, side, qty,
sec_type="FOR", comment=""):
"""
下市價單
symbol: 商品代碼,例如 "EUR/USD"
side: "1"=買進 "2"=賣出
qty: 數量(外匯:100000=1手)
sec_type: "FOR"=外匯 "CFD"=指數 "FORWARD"=遠期
"""
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdReq",
"Tok": token,
"OrdReq": {
"acct": account,
"sym": symbol,
"secType": sec_type,
"side": str(side), # "1"=買 "2"=賣
"qty": str(qty),
"px": "0", # 市價單固定為 0
"type": "1", # 1=市價單
"tif": "1", # 1=GTC(直到取消)
"sl": "0", # 停損(0=不設)
"tp": "0", # 停利(0=不設)
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "u",
"px2": "0",
"txTime": now,
"handlInst":"1",
"prnAgc": "false",
"slpg": "0",
"tktType": "0", # 0=開倉
"tktNo": "",
"refTktNo": "",
"comment": comment
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"下單送出: {symbol} {'買' if str(side)=='1' else '賣'} {qty}")
print(f" 回應: {result}")
# 實際成交結果會從 WebSocket ExecRp 推送過來
return result
# 買進 1 手 EUR/USD
place_market_order(token, ACCOUNT, "EUR/USD", side="1", qty=100000)
# 賣出 1 手 GBP/USD
place_market_order(token, ACCOUNT, "GBP/USD", side="2", qty=100000)
4.4 下限價單
指定價格,當市場達到該價格時才成交。買進通常設低於現價,賣出通常設高於現價。
def place_limit_order(token, account, symbol, side, qty, price,
sec_type="FOR"):
"""
下限價單
price: 希望成交的目標價格
"""
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdReq",
"Tok": token,
"OrdReq": {
"acct": account,
"sym": symbol,
"secType": sec_type,
"side": str(side),
"qty": str(qty),
"px": str(price), # 限價單:填目標價格
"type": "2", # 2=限價單
"tif": "1",
"sl": "0",
"tp": "0",
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "u",
"px2": "0",
"txTime": now,
"handlInst":"1",
"prnAgc": "false",
"slpg": "0",
"tktType": "0",
"tktNo": "",
"refTktNo": ""
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"限價單送出: {symbol} {'買' if str(side)=='1' else '賣'} @ {price}")
return result
# 以 1.0800 限價買入 EUR/USD 1手
place_limit_order(token, ACCOUNT, "EUR/USD", side="1", qty=100000, price=1.0800)
4.5 市價單含停損停利
下市價單的同時一併設定停損(SL)和停利(TP)價格。
def place_market_order_with_sltp(token, account, symbol, side, qty,
sl_price, tp_price, sec_type="FOR"):
"""
含停損停利的市價單
sl_price: 停損價格(0=不設)
tp_price: 停利價格(0=不設)
"""
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdReq",
"Tok": token,
"OrdReq": {
"acct": account,
"sym": symbol,
"secType": sec_type,
"side": str(side),
"qty": str(qty),
"px": "0", # 市價
"type": "1", # 市價單
"tif": "1",
"sl": str(sl_price), # 停損價
"tp": str(tp_price), # 停利價
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "u",
"px2": "0",
"txTime": now,
"handlInst":"1",
"prnAgc": "false",
"slpg": "-1", # 含SL/TP時設 -1
"tktType": "0",
"tktNo": "",
"refTktNo": ""
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
return resp.json()
# 買進 GBP/USD,停損 1.20,停利 1.30
place_market_order_with_sltp(
token, ACCOUNT,
symbol="GBP/USD",
side="1", # 買
qty=100000,
sl_price=1.20, # 跌到 1.20 停損
tp_price=1.30 # 漲到 1.30 停利
)
4.6 為現有部位新增停損停利
已開倉但未設停損停利的單子,用這個方法補設。需要提供持倉票號(tktId)。
def add_sltp(token, account, ticket_id, symbol, open_side, qty,
sl_price, tp_price, sec_type="FOR"):
"""
為持倉新增停損停利(type=U,Threshold 模式)
ticket_id: 持倉的票號(從 get_tickets 取得 tktId)
open_side: 原始開倉方向 ("1"=買 "2"=賣)
sl_price: 停損價格(U型單的 px)
tp_price: 停利價格(U型單的 px2)
"""
# 停損停利單方向與開倉相反
close_side = "2" if str(open_side) == "1" else "1"
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdReq",
"Tok": token,
"OrdReq": {
"acct": account,
"sym": symbol,
"secType": sec_type,
"side": close_side, # 與開倉相反
"qty": str(qty),
"px": str(sl_price), # U型:px = 停損價
"type": "U", # U = Threshold(停損停利)
"tif": "1",
"sl": "0",
"tp": "0",
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "u",
"px2": str(tp_price), # U型:px2 = 停利價
"txTime": now,
"handlInst":"1",
"prnAgc": "true",
"slpg": "0",
"tktType": "1", # 1=平倉單
"tktNo": str(ticket_id),
"refTktNo": str(ticket_id)
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"新增停損停利: 票號={ticket_id} 停損={sl_price} 停利={tp_price}")
return result
# 票號 22130 是買進的倉,新增停損=1.05,停利=1.06
add_sltp(token, ACCOUNT,
ticket_id=22130,
symbol="EUR/USD",
open_side="1", # 原本是買
qty=100000,
sl_price=1.05,
tp_price=1.06)
4.7 修改停損停利
修改已設定的停損停利價格。需要先查到現有停損停利單的委託ID(clOrdId)。
clOrdId 就是這裡的 org_order_id。
def modify_sltp(token, account, org_order_id, ticket_id,
symbol, open_side, qty, new_sl, new_tp,
sec_type="FOR"):
"""
修改停損停利(Cancel + Replace)
org_order_id: 原始停損停利單的委託ID(從查詢掛單取得的 clOrdId)
ticket_id: 對應的持倉票號
new_sl: 新的停損價
new_tp: 新的停利價
"""
close_side = "2" if str(open_side) == "1" else "1"
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdCxlRepReq",
"Tok": token,
"OrdCxlRepReq": {
"acct": account,
"orgClOrdId": str(org_order_id), # 要被替換的舊委託ID
"sym": symbol,
"secType": sec_type,
"side": close_side,
"qty": str(qty),
"px": str(new_sl), # 新停損價
"type": "U",
"tif": "1",
"sl": "0",
"tp": "0",
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "u",
"px2": str(new_tp), # 新停利價
"txTime": now,
"handlInst": "1",
"prnAgc": "false",
"slpg": "0",
"tktType": "1",
"tktNo": str(ticket_id),
"refTktNo": str(ticket_id)
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"修改停損停利: 委託={org_order_id} 新停損={new_sl} 新停利={new_tp}")
return result
# 範例:把票號22130的停損從1.05改到1.04,停利從1.06改到1.07
modify_sltp(token, ACCOUNT,
org_order_id=14564, # 從查詢掛單取得的 clOrdId
ticket_id=22130,
symbol="EUR/USD",
open_side="1",
qty=100000,
new_sl=1.04,
new_tp=1.07)
4.8 撤銷掛單
撤銷尚未成交的限價單或停損單。需要知道委託單的 ID(clOrdId)。
def cancel_order(token, account, order_id, symbol, side, sec_type="FOR"):
"""
撤銷掛單
order_id: 要撤銷的委託ID(從查詢掛單取得的 clOrdId)
"""
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdCxlReq",
"Tok": token,
"OrdCxlReq": {
"acct": account,
"clOrdId": str(order_id), # 要撤銷的委託ID
"sym": symbol,
"secType": sec_type,
"side": str(side),
"txTime": now
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"撤單送出: 委託ID={order_id} {symbol}")
return result
# 撤銷委託ID=883 的掛單
cancel_order(token, ACCOUNT, order_id=883, symbol="EUR/USD", side="1")
4.9 平倉(市價關閉持倉)
對有設停損停利的倉位,先取消原本的停損停利單,同時送出市價平倉單。需提供停損停利委託的 ID 和持倉票號。
tktType="1",tktNo 填票號)。
def close_position(token, account, org_sltp_order_id,
ticket_id, symbol, open_side, qty,
sec_type="FOR"):
"""
市價平倉(同時取消停損停利)
org_sltp_order_id: 現有停損停利單的委託ID(從查詢掛單取得)
ticket_id: 持倉票號
open_side: 原始開倉方向("1"=買 "2"=賣)
"""
close_side = "2" if str(open_side) == "1" else "1"
now = datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
payload = {
"MT": "OrdCxlRepReq",
"Tok": token,
"OrdCxlRepReq": {
"acct": account,
"orgClOrdId": str(org_sltp_order_id), # 要取消的停損停利單ID
"sym": symbol,
"secType": sec_type,
"side": close_side, # 平倉方向(反向)
"qty": str(qty),
"px": "0", # 市價平倉
"type": "1", # 改回市價單
"tif": "3", # 3=IOC(立即成交否則取消)
"sl": "0",
"tp": "0",
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": "v",
"px2": "0",
"txTime": now,
"handlInst": "1",
"prnAgc": "false",
"slpg": "0",
"tktType": "1", # 1=平倉
"tktNo": str(ticket_id),
"refTktNo": str(ticket_id)
}
}
resp = requests.post(REST_URL, json=payload, timeout=10)
result = resp.json()
print(f"平倉送出: 票號={ticket_id} {symbol} {'買' if close_side=='1' else '賣'}")
return result
# 平倉票號 22130 的持倉(原本是買進,停損停利委託ID=14565)
close_position(token, ACCOUNT,
org_sltp_order_id=14565,
ticket_id=22130,
symbol="EUR/USD",
open_side="1",
qty=100000)
5.1 傳送憑證(TransferCert)
僅在伺服器啟用 CA 登入功能時使用。可將憑證從一台裝置轉移到另一台裝置,需搭配 5.2 匯入憑證使用。預設憑證保存 10 分鐘。
import base64
def transfer_cert(token, account, transfer_password, cert_file_path):
"""
傳送憑證到伺服器(供另一台裝置取用)
transfer_password: 自訂的取用密碼(接收端需用相同密碼)
cert_file_path: .pfx 憑證檔案路徑
"""
with open(cert_file_path, "rb") as f:
cert_b64 = base64.b64encode(f.read()).decode("utf-8")
payload = {
"MT": "TransferCert",
"Tok": token,
"login": account,
"transfer_pwd": transfer_password,
"cert_b64": cert_b64
}
resp = requests.post(REST_URL, json=payload, timeout=10)
print("傳送憑證結果:", resp.json())
return resp.json()
transfer_cert(token, ACCOUNT, "自訂密碼123", "my_cert.pfx")
5.2 匯入憑證(ImportCert)
從伺服器取回已傳送的憑證。
def import_cert(account, transfer_password):
"""
匯入憑證(不需要 Token)
transfer_password: 與 transfer_cert 相同的密碼
"""
payload = {
"MT": "ImportCert",
"login": account,
"transfer_pwd": transfer_password
}
resp = requests.post(REST_URL, json=payload, timeout=10)
print("匯入憑證結果:", resp.json())
return resp.json()
import_cert(ACCOUNT, "自訂密碼123")
兩種使用模式
本 API 提供兩種完全獨立的整合方式,請依你的需求選擇其中一種:
適合寫自動交易策略、排程腳本、數據分析。
| 執行方式 | python ltm_client.py |
| 有沒有 UI | 無(終端機輸出) |
| 安裝套件 | requests websocket-client cryptography |
| 適合對象 | 量化交易、Python 開發者 |
適合想要可視化操作介面、點擊下單、看即時報價的使用者。
| 執行方式 | python server.py → 開瀏覽器 |
| 有沒有 UI | 有(交易面板) |
| 安裝套件 | flask flask-socketio + 上面全部 |
| 適合對象 | 一般交易者、需要 UI 的使用者 |
ltm_client.py 和 server.py 都能完成相同的 API 操作,只是介面不同。不需要同時使用,也不需要先跑一個再跑另一個。
模式一:完整 Python 腳本(ltm_client.py)
API_KEY、PASSWORD、ACCOUNT)務必改為您自己的資料。純 Python 客戶端,直接從終端機連接元大 API。下載後修改最上方設定區的 API_KEY、PASSWORD、ACCOUNT,以及 CA 憑證路徑 PFX_PATH、PFX_PASSWORD,再執行即可。
"""
槓桿全球贏家 Web API Python 客戶端
=====================================
Version: 4.13
安裝套件: pip install requests websocket-client cryptography
使用方式:
from ltm_client import LTMClient
client = LTMClient()
client.start()
client.place_market_order("EUR/USD", "1", 100000)
client.run_forever()
"""
import requests
import websocket
import json
import hashlib
import threading
import time
import base64
import os
from datetime import datetime, timezone
# CA 憑證登入(需安裝:pip install cryptography)
try:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.serialization import pkcs12
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
# ============================================================
# 【設定區】請修改這裡
# ============================================================
API_KEY = "ABCDEF0ECB394776ADDBA88CB3254321" # 從元大後台取得的 API Key
PASSWORD = "Ss12345#" # API Key 對應的密碼
ACCOUNT = "1041234" # 交易帳戶號碼
# CA 憑證設定(正式帳號必填,留空則走 Plain 登入)
PFX_PATH = r"" # .pfx 憑證完整路徑,例:r"C:\Users\你的名字\Downloads\1041234.pfx"
PFX_PASSWORD = "" # 憑證密碼(沒有則留空字串)
REST_URL = "https://webltm.yuantafutures.com.tw/WEBTRADER/rest"
WS_URL = "wss://webltm.yuantafutures.com.tw/WEBTRADER/ws"
# ============================================================
class LTMClient:
"""槓桿全球贏家 Web API 客戶端"""
def __init__(self):
self.token = None
self.ws_app = None
self._thread = None
self._connected = False
# ── 即時資料快取 ──────────────────────────────────────────
# quotes: 最新報價,key = 商品代碼,每次收到 Q 訊息自動更新
# 用法:client.quotes["EUR/USD"]["bid"] → 最新買價
# client.quotes["EUR/USD"]["ask"] → 最新賣價
# client.quotes["EUR/USD"]["time"] → 報價時間
self.quotes = {}
# orders: 委託狀態,key = clOrdId(下單時自訂的委託 ID)
# 每次收到 ExecRp 訊息(新委託確認/部分成交/全數成交/取消)都會更新
# 用法:client.orders["my-order-id"]["status"] → 委託狀態碼
# client.orders["my-order-id"]["cumQty"] → 已成交量
self.orders = {}
# ─────────────────────────────────────────────────────────
# 自訂訊息處理器(可由外部設定)
self.on_quote = None # 報價更新:fn(symbol, bid, ask, time)
self.on_execrp = None # 執行回報:fn(data)
self.on_ws_msg = None # 所有 WS 訊息:fn(data)
# ----------------------------------------------------------
# 登入與連線
# ----------------------------------------------------------
def start(self):
"""一鍵完成登入 + WebSocket 連線(自動選擇 CA 或 Plain 登入)"""
if PFX_PATH and os.path.exists(PFX_PATH):
print("🔐 使用 CA 憑證登入...")
success = self._signed_login()
else:
print("🔑 使用 Plain 登入(未設定 PFX_PATH)...")
success = self._rest_login()
if not success:
raise RuntimeError("登入失敗")
self._start_websocket()
print("✅ 已完成登入並建立 WebSocket 連線")
def _signed_login(self):
"""CA 憑證登入 (SignedReq):用 .pfx 私鑰簽名後登入,正式帳號使用"""
if not HAS_CRYPTO:
print("❌ 未安裝 cryptography,請執行:pip install cryptography")
return False
with open(PFX_PATH, "rb") as f:
pfx_bytes = f.read()
pfx_pass = PFX_PASSWORD.encode() if PFX_PASSWORD else None
try:
private_key, _, _ = pkcs12.load_key_and_certificates(pfx_bytes, pfx_pass)
except Exception as e:
print(f"❌ 無法讀取 .pfx 憑證:{e}")
return False
# 密碼用 base64(SHA256) 格式
pw_b64 = base64.b64encode(hashlib.sha256(PASSWORD.encode()).digest()).decode()
raw_obj = {"MT": "Login", "UserInfo": {"login": API_KEY, "password": pw_b64}}
raw_req = json.dumps(raw_obj, separators=(",", ":"))
# RSA-SHA256 簽名
try:
sig = private_key.sign(raw_req.encode(), asym_padding.PKCS1v15(), hashes.SHA256())
sig_b64 = base64.b64encode(sig).decode()
except Exception as e:
print(f"❌ 簽名失敗:{e}")
return False
# POST SignedReq
payload = {
"MT": "SignedReq",
"rawReq": raw_req,
"signerId": ACCOUNT,
"password": PASSWORD,
"signature": sig_b64,
}
try:
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
except Exception as e:
print(f"❌ SignedReq 連線失敗:{e}")
return False
if data.get("ErrCode", 0) != 0:
print(f"❌ CA 登入失敗: {data}")
return False
raw_tok = data.get("Tok", "")
if not raw_tok:
print(f"❌ CA 登入無回傳 Token: {data}")
return False
# RSA 解密 Token
try:
enc_tok = base64.b64decode(raw_tok + "==")
self.token = private_key.decrypt(enc_tok, asym_padding.PKCS1v15()).decode()
except Exception:
self.token = raw_tok # 解密失敗則直接用原始 token
print(f"🔑 CA 登入成功 Token={self.token[:10]}...")
return True
def _rest_login(self):
"""Plain 登入:SHA256 密碼(未設定 PFX_PATH 時使用)"""
hashed_pw = hashlib.sha256(PASSWORD.encode("utf-8")).hexdigest()
payload = {
"MT": "Login",
"UserInfo": {"login": API_KEY, "password": hashed_pw}
}
try:
resp = requests.post(REST_URL, json=payload, timeout=10)
data = resp.json()
if "Tok" in data:
self.token = data["Tok"]
print(f"🔑 REST 登入成功 Token={self.token[:10]}...")
return True
else:
print(f"❌ REST 登入失敗: {data}")
return False
except Exception as e:
print(f"❌ 連線錯誤: {e}")
return False
def _start_websocket(self):
"""Step 2: 建立 WebSocket 連線並登入"""
def on_open(ws):
print("🔌 WebSocket 已連線,正在登入...")
ws.send(json.dumps({
"MT": "Login",
"UserInfo": {"login": API_KEY, "password": PASSWORD},
"Tok": self.token
}))
def on_message(ws, message):
try:
data = json.loads(message)
except Exception:
return
# 每條訊息都要立即回 ACK
try:
ws.send(json.dumps({"MT": "Ack"}))
except Exception:
pass
mt = data.get("MT", "")
# 通知外部處理器
if self.on_ws_msg:
self.on_ws_msg(data)
if mt == "Login":
self._connected = True
print("✅ WebSocket 登入成功")
elif mt == "Q":
q = data.get("Q", {})
sym = q.get("s")
# 無論有無自訂 handler,都先把最新報價存入快取
# 策略程式可在任何時間點直接讀 self.quotes["EUR/USD"]["bid"]
# 不需要等待下一次 WebSocket 推播
if sym:
self.quotes[sym] = {
"bid": q.get("b"), # 買價(Bid)
"ask": q.get("a"), # 賣價(Ask)
"time": q.get("t"), # 報價時間
}
if self.on_quote:
self.on_quote(sym, q.get("b"), q.get("a"), q.get("t"))
else:
print(f"💹 {sym:<12} 買={q.get('b')} 賣={q.get('a')}")
elif mt == "ExecRp":
rp = data.get("ExecRp", {})
order_id = rp.get("clOrdId")
# 以 clOrdId 為 key,持續覆蓋成最新狀態
# 同一筆委託從「新委託」→「部分成交」→「全數成交」都記在同一個 key 下
# 查詢:self.orders["my-order-id"]["status"] / ["cumQty"] 等
if order_id:
self.orders[order_id] = rp
if self.on_execrp:
self.on_execrp(data)
else:
print(f"📋 執行回報: {json.dumps(data, ensure_ascii=False, indent=2)}")
elif mt == "GetAcctInfo":
bal = data.get("AcctVal", {}).get("bal", "?")
print(f"💰 帳戶餘額: {bal}")
elif mt == "NetPosReq":
positions = data.get("PositionAry", [])
print(f"📊 部位回應: {len(positions)} 筆")
def on_error(ws, error):
print(f"❌ WebSocket 錯誤: {error}")
def on_close(ws, code, msg):
self._connected = False
print("🔌 WebSocket 已斷線")
self.ws_app = websocket.WebSocketApp(
WS_URL,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
self._thread = threading.Thread(
target=self.ws_app.run_forever,
kwargs={"ping_interval": 30}
)
self._thread.daemon = True
self._thread.start()
time.sleep(2)
def run_forever(self):
"""保持程式執行,持續接收 WebSocket 推播"""
print("▶ 持續接收 WebSocket 推播(Ctrl+C 結束)")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n⏹ 程式結束")
if self.ws_app:
self.ws_app.close()
# ----------------------------------------------------------
# REST 工具
# ----------------------------------------------------------
def _rest(self, payload):
"""發送 REST 請求(自動帶入 Token)"""
payload["Tok"] = self.token
resp = requests.post(REST_URL, json=payload, timeout=10)
return resp.json()
def _now(self):
return datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
# ----------------------------------------------------------
# 帳戶查詢
# ----------------------------------------------------------
def get_account_info(self, account=None):
"""查詢帳戶資訊(餘額、保證金、盈虧)"""
return self._rest({
"MT": "GetAcctInfo",
"UserInfo": {"login": account or ACCOUNT}
})
def get_symbols(self):
"""查詢所有可交易商品"""
return self._rest({"MT": "SymListReq"})
def get_positions(self, account=None):
"""查詢目前持倉部位"""
return self._rest({
"MT": "NetPosReq",
"UserInfo": {"login": account or ACCOUNT}
})
def get_open_orders(self, account=None):
"""查詢掛單(未成交委託)"""
return self._rest({
"MT": "OpenOrdReq",
"UserInfo": {"login": account or ACCOUNT}
})
def get_tickets(self, include_open=True, include_closed=True,
from_date="2025/01/01 00:00:00",
to_date="2025/12/31 23:59:59",
account=None):
"""查詢成交紀錄"""
return self._rest({
"MT": "GetTickets",
"UserInfo": {"login": account or ACCOUNT},
"GetTickets": {
"inclOp": include_open,
"inclCls": include_closed,
"clsFrom": from_date,
"clsTo": to_date
}
})
def get_access_server_info(self):
"""取得所有可用伺服器列表"""
return self._rest({"MT": "GetAccessServerInfo"})
def echo(self, **kwargs):
"""Echo 測試(量測延遲)"""
payload = {"MT": "Echo", **kwargs}
return self._rest(payload)
# ----------------------------------------------------------
# 報價訂閱(QReq 走 REST,報價推播從 WebSocket 接收)
# ----------------------------------------------------------
def subscribe_quote(self, symbols, quote_type=1):
"""訂閱即時報價
symbols: 商品代碼列表 ["EUR/USD", "GBP/USD"]
quote_type: 1=L1基本, 2=L2深度, 3=全部
重要:QReq 用 REST 送出(不是 WebSocket)
報價更新(MT="Q")從 WebSocket on_message 接收
"""
return self._rest({
"MT": "QReq",
"QReq": {"sym": symbols, "sub_type": 1, "quote_type": quote_type}
})
def unsubscribe_quote(self, symbols):
"""取消訂閱報價"""
return self._rest({
"MT": "QReq",
"QReq": {"sym": symbols, "sub_type": 2, "quote_type": 1}
})
def get_chart_history(self, symbol, chart_type=1, days=2, group_size=5):
"""取得歷史K線
chart_type: 0=日線 1=分鐘線
group_size: 分鐘線合併(5=5分K)
"""
return self._rest({
"MT": "ChartHisReq",
"ChartHisReq": {
"sym": symbol,
"type": chart_type,
"days": days,
"reqId": 1,
"endTime": datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S"),
"groupSize": group_size
}
})
# ----------------------------------------------------------
# 交易操作
# ----------------------------------------------------------
def _build_ord(self, account, symbol, side, qty, px, ord_type,
tif="1", sl=0, tp=0, px2=0, tkt_type="0",
tkt_no="", ref_tkt_no="", exec_inst="u",
prn_agc="false", slpg="0", sec_type="FOR"):
"""建構 OrdReq 內容"""
return {
"acct": account,
"sym": symbol,
"secType": sec_type,
"side": str(side),
"qty": str(qty),
"px": str(px),
"type": str(ord_type),
"tif": str(tif),
"sl": str(sl),
"tp": str(tp),
"execDst": "INTX",
"minQty": "0",
"stopPx": "0",
"qtyRsrv": "0",
"maxShow": "0",
"execBrk": "",
"execInst": exec_inst,
"px2": str(px2),
"txTime": self._now(),
"handlInst":"1",
"prnAgc": prn_agc,
"slpg": slpg,
"tktType": str(tkt_type),
"tktNo": str(tkt_no),
"refTktNo": str(ref_tkt_no)
}
def place_market_order(self, symbol, side, qty,
sl=0, tp=0, sec_type="FOR", comment=""):
"""下市價單(side: '1'=買 '2'=賣)"""
slpg = "-1" if (sl != 0 or tp != 0) else "0"
ord_req = self._build_ord(
ACCOUNT, symbol, side, qty,
px=0, ord_type=1, sl=sl, tp=tp, slpg=slpg
)
ord_req["comment"] = comment
return self._rest({"MT": "OrdReq", "OrdReq": ord_req})
def place_limit_order(self, symbol, side, qty, price,
sl=0, tp=0, sec_type="FOR"):
"""下限價單"""
ord_req = self._build_ord(
ACCOUNT, symbol, side, qty,
px=price, ord_type=2, sl=sl, tp=tp
)
return self._rest({"MT": "OrdReq", "OrdReq": ord_req})
def add_sltp(self, ticket_id, symbol, open_side, qty,
sl_price, tp_price, sec_type="FOR"):
"""為持倉新增停損停利"""
close_side = "2" if str(open_side) == "1" else "1"
ord_req = self._build_ord(
ACCOUNT, symbol, close_side, qty,
px=sl_price, ord_type="U", px2=tp_price,
tkt_type=1, tkt_no=ticket_id, ref_tkt_no=ticket_id,
prn_agc="true"
)
return self._rest({"MT": "OrdReq", "OrdReq": ord_req})
def modify_sltp(self, org_order_id, ticket_id, symbol,
open_side, qty, new_sl, new_tp, sec_type="FOR"):
"""修改停損停利"""
close_side = "2" if str(open_side) == "1" else "1"
req = self._build_ord(
ACCOUNT, symbol, close_side, qty,
px=new_sl, ord_type="U", px2=new_tp,
tkt_type=1, tkt_no=ticket_id, ref_tkt_no=ticket_id
)
req["orgClOrdId"] = str(org_order_id)
return self._rest({"MT": "OrdCxlRepReq", "OrdCxlRepReq": req})
def cancel_order(self, order_id, symbol, side, sec_type="FOR"):
"""撤銷掛單"""
payload = {
"MT": "OrdCxlReq",
"OrdCxlReq": {
"acct": ACCOUNT,
"clOrdId": str(order_id),
"sym": symbol,
"secType": sec_type,
"side": str(side),
"txTime": self._now()
}
}
return self._rest(payload)
def close_position(self, org_sltp_order_id, ticket_id,
symbol, open_side, qty, sec_type="FOR"):
"""市價平倉"""
close_side = "2" if str(open_side) == "1" else "1"
req = self._build_ord(
ACCOUNT, symbol, close_side, qty,
px=0, ord_type=1, tif="3",
tkt_type=1, tkt_no=ticket_id, ref_tkt_no=ticket_id,
exec_inst="v"
)
req["orgClOrdId"] = str(org_sltp_order_id)
return self._rest({"MT": "OrdCxlRepReq", "OrdCxlRepReq": req})
# ----------------------------------------------------------
# 憑證管理
# ----------------------------------------------------------
def transfer_cert(self, transfer_password, cert_file_path):
"""傳送憑證到伺服器"""
with open(cert_file_path, "rb") as f:
cert_b64 = base64.b64encode(f.read()).decode("utf-8")
return self._rest({
"MT": "TransferCert",
"login": ACCOUNT,
"transfer_pwd": transfer_password,
"cert_b64": cert_b64
})
def import_cert(self, transfer_password):
"""從伺服器匯入憑證"""
payload = {
"MT": "ImportCert",
"login": ACCOUNT,
"transfer_pwd": transfer_password
}
resp = requests.post(REST_URL, json=payload, timeout=10)
return resp.json()
# ============================================================
# 使用範例(直接執行此檔時執行)
# ============================================================
if __name__ == "__main__":
client = LTMClient()
# 自訂報價處理
def my_quote_handler(symbol, bid, ask, t):
print(f" [{t}] {symbol:<12} 買={bid} 賣={ask}")
client.on_quote = my_quote_handler
# 登入
client.start()
# 查帳戶
acct = client.get_account_info()
v = acct.get("AcctVal", {})
print(f"\n帳戶淨值: {v.get('bal', '?'):>15}")
print(f"已實現P&L: {v.get('clsPL', '?'):>14}")
print(f"所需保證金: {v.get('rqdMrgn', '?'):>13}\n")
# 查持倉
pos = client.get_positions()
plist = pos.get("PositionAry", [])
print(f"持倉數: {len(plist)}")
# 訂閱報價(背景自動接收)
client.subscribe_quote(["EUR/USD", "GBP/USD"])
# 持續接收
client.run_forever()
pip install requests websocket-client cryptography2. 複製上方程式碼,儲存為
ltm_client.py3. 修改最上方的
API_KEY、PASSWORD、ACCOUNT、PFX_PATH、PFX_PASSWORD4. 在命令提示字元執行:
python ltm_client.py
模式二:瀏覽器前台(server.py + dashboard.html)
PFX_PATH 與 PFX_PASSWORD 需改為您自己的憑證路徑與密碼,切勿將憑證資訊硬寫入程式碼後公開發布。正式環境請加入適當的身份驗證與存取控制。本機代理伺服器,讓瀏覽器能透過視覺化介面操作元大 API。需同時搭配 dashboard.html 使用。
PFX_PATH 和 PFX_PASSWORD 兩行,填入你自己的 .pfx 路徑與密碼,才能完成 CA 登入。
所需檔案(兩個都需要)
| 檔案 | 用途 | 取得方式 |
|---|---|---|
server.py | 本機 API 代理伺服器 | 下載 |
dashboard.html | 瀏覽器交易介面 | 下載 |
你的帳號.pfx | CA 憑證(元大核發) | 向元大期貨申請 |
"""
槓桿全球贏家 Web API — 本機代理伺服器
=========================================
安裝套件: pip install flask flask-socketio requests websocket-client cryptography
執行方式: python server.py
開啟瀏覽器: http://localhost:5000
"""
from flask import Flask, jsonify, request, send_file
from flask_socketio import SocketIO
import requests as req
import websocket
import json
import hashlib
import base64
import threading
import time
import ssl
from datetime import datetime, timezone
try:
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.serialization import pkcs12
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
REST_URL = "https://webltm.yuantafutures.com.tw/WEBTRADER/rest"
WS_URL = "wss://webltm.yuantafutures.com.tw/WEBTRADER/ws"
# ══════════════════════════════════════════════════════════
# ★ 使用前必填:CA 憑證設定
#
# PFX_PATH : .pfx 檔案的完整路徑(Windows 路徑前加 r)
# PFX_PASSWORD : 開啟 .pfx 的密碼(沒有密碼則留空字串 "")
#
# 範例:
# PFX_PATH = r"C:\Users\YourName\Desktop\1041234.pfx"
# PFX_PASSWORD = "your_password"
# ══════════════════════════════════════════════════════════
PFX_PATH = r"C:\Users\YourName\Desktop\1041234.pfx"
PFX_PASSWORD = "your_password"
# 全域連線狀態
state = {
"token": None,
"api_key": None,
"password": None,
"account": None,
"ws": None,
"connected": False,
}
# ============================================================
# 元大 WebSocket 管理
# ============================================================
def start_ltm_websocket():
def on_open(ws):
ws.send(json.dumps({
"MT": "Login",
"UserInfo": {"login": state["api_key"], "password": state["password"]},
"Tok": state["token"]
}))
def on_message(ws, raw):
try:
data = json.loads(raw)
except Exception:
return
try:
ws.send(json.dumps({"MT": "Ack"}))
except Exception:
pass
mt = data.get("MT", "")
if mt == "MsgAry":
for msg in data.get("MsgAry", []):
_dispatch(msg)
return
_dispatch(data)
def on_error(ws, error):
socketio.emit("ws_error", {"msg": str(error)})
def on_close(ws, code, msg):
state["connected"] = False
socketio.emit("ws_status", {"connected": False})
state["ws"] = websocket.WebSocketApp(
WS_URL,
on_open=on_open, on_message=on_message,
on_error=on_error, on_close=on_close
)
def _run():
state["ws"].run_forever(
ping_interval=30,
sslopt={"cert_reqs": ssl.CERT_NONE}
)
threading.Thread(target=_run, daemon=True).start()
def _dispatch(data):
mt = data.get("MT", "")
if mt == "Login":
state["connected"] = True
socketio.emit("ws_status", {"connected": True})
def _auto_sub():
time.sleep(1)
if state["connected"]:
try:
r = req.post(REST_URL, json={
"MT": "QReq", "Tok": state["token"],
"QReq": {"sym": ["EUR/USD", "GBP/USD", "XAU/USD"],
"sub_type": 1, "quote_type": 1}
}, timeout=10)
except Exception:
pass
threading.Thread(target=_auto_sub, daemon=True).start()
elif mt == "Q": socketio.emit("quote", data.get("Q", {}))
elif mt == "MD": socketio.emit("market_depth", data.get("MD", {}))
elif mt == "ExecRp": socketio.emit("exec_report", data)
elif mt == "GetAcctInfo": socketio.emit("acct_info", data)
elif mt == "NetPosReq": socketio.emit("positions", data)
elif mt == "OpenOrdReq": socketio.emit("open_orders", data)
elif mt == "GetTickets": socketio.emit("tickets", data)
elif mt == "ChartHisReq": socketio.emit("chart_data", data)
else: socketio.emit("ws_msg", data)
# ============================================================
# 工具函式
# ============================================================
def ltm_rest(payload):
if state["token"]:
payload["Tok"] = state["token"]
try:
r = req.post(REST_URL, json=payload, timeout=10)
return r.json(), 200
except Exception as e:
return {"error": str(e)}, 500
def now_utc():
return datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3]
def require_login():
if not state["token"]:
return jsonify({"error": "尚未登入"}), 401
return None
# ============================================================
# HTTP 路由
# ============================================================
@app.route("/")
def index():
return send_file("dashboard.html")
@app.route("/docs")
def docs():
return send_file("index.html")
# --- 登入(CA SignedReq)---
def _do_signed_login(api_key, password, account, pfx_bytes, pfx_pwd):
if not HAS_CRYPTO:
return None, "請安裝 cryptography:pip install cryptography"
pfx_pass = pfx_pwd.encode() if pfx_pwd else None
try:
private_key, certificate, _ = pkcs12.load_key_and_certificates(pfx_bytes, pfx_pass)
except Exception as e:
return None, f"無法讀取 .pfx:{e}"
signer_id = account # signerId = 帳戶號碼(不是 API Key)
pw_b64 = base64.b64encode(hashlib.sha256(password.encode()).digest()).decode()
raw_req = json.dumps({"MT": "Login", "UserInfo": {"login": api_key, "password": pw_b64}},
separators=(",", ":"))
try:
sig_b64 = base64.b64encode(
private_key.sign(raw_req.encode(), asym_padding.PKCS1v15(), hashes.SHA256())
).decode()
except Exception as e:
return None, f"簽名失敗:{e}"
try:
r = req.post(REST_URL, json={
"MT": "SignedReq", "rawReq": raw_req,
"signerId": signer_id, "password": password, "signature": sig_b64,
}, timeout=10)
data = r.json()
except Exception as e:
return None, str(e)
if data.get("MT") == "Logout" or "ErrInfo" in data:
return None, str(data)
if "Tok" not in data:
return None, str(data)
try:
enc_tok = base64.b64decode(data["Tok"] + "==")
token = private_key.decrypt(enc_tok, asym_padding.PKCS1v15()).decode()
except Exception as e:
token = data["Tok"] # 解密失敗時直接使用原始值
return token, None
@app.route("/api/login", methods=["POST"])
def login():
body = request.json or {}
api_key = body.get("api_key", "")
password = body.get("password", "")
account = body.get("account", "")
import os
if PFX_PATH and os.path.exists(PFX_PATH):
with open(PFX_PATH, "rb") as f:
pfx_bytes = f.read()
token, err = _do_signed_login(api_key, password, account, pfx_bytes, PFX_PASSWORD)
else:
return jsonify({"ok": False, "error": "找不到 .pfx 憑證,請設定 PFX_PATH"}), 401
if err:
return jsonify({"ok": False, "error": err}), 401
state.update({"token": token, "api_key": api_key, "password": password, "account": account})
threading.Thread(target=start_ltm_websocket, daemon=True).start()
return jsonify({"ok": True})
@app.route("/api/logout", methods=["POST"])
def logout():
if state["ws"]:
try: state["ws"].close()
except Exception: pass
state.update({"token": None, "api_key": None, "password": None,
"account": None, "ws": None, "connected": False})
return jsonify({"ok": True})
@app.route("/api/status")
def status():
return jsonify({"logged_in": bool(state["token"]),
"ws_connected": state["connected"],
"account": state["account"]})
# --- 帳戶查詢 ---
@app.route("/api/account")
def get_account():
err = require_login()
if err: return err
data, code = ltm_rest({"MT": "GetAcctInfo", "UserInfo": {"login": state["account"]}})
return jsonify(data), code
@app.route("/api/positions")
def get_positions():
err = require_login()
if err: return err
data, code = ltm_rest({"MT": "NetPosReq", "UserInfo": {"login": state["account"]}})
return jsonify(data), code
@app.route("/api/orders")
def get_orders():
err = require_login()
if err: return err
data, code = ltm_rest({"MT": "OpenOrdReq", "UserInfo": {"login": state["account"]}})
return jsonify(data), code
@app.route("/api/symbols")
def get_symbols():
err = require_login()
if err: return err
data, code = ltm_rest({"MT": "SymListReq"})
return jsonify(data), code
@app.route("/api/tickets")
def get_tickets():
err = require_login()
if err: return err
data, code = ltm_rest({
"MT": "GetTickets",
"UserInfo": {"login": state["account"]},
"GetTickets": {
"inclOp": True, "inclCls": True,
"clsFrom": request.args.get("from", "2025/01/01 00:00:00"),
"clsTo": request.args.get("to", "2025/12/31 23:59:59"),
}
})
return jsonify(data), code
@app.route("/api/chart")
def get_chart():
err = require_login()
if err: return err
data, code = ltm_rest({
"MT": "ChartHisReq",
"ChartHisReq": {
"sym": request.args.get("sym", "EUR/USD"),
"type": int(request.args.get("type", 1)),
"days": int(request.args.get("days", 2)),
"reqId": 1,
"endTime": datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S"),
"groupSize": int(request.args.get("group", 5)),
}
})
return jsonify(data), code
# --- 報價訂閱(QReq 走 REST,報價推播走 WS)---
@app.route("/api/quote/subscribe", methods=["POST"])
def subscribe_quote():
err = require_login()
if err: return err
body = request.json or {}
symbols = body.get("symbols", [])
data, code = ltm_rest({
"MT": "QReq",
"QReq": {"sym": symbols, "sub_type": body.get("sub_type", 1), "quote_type": 1}
})
return jsonify(data), code
# --- 交易 ---
def _build_ord(b):
return {
"acct": state["account"], "sym": b["sym"],
"secType": b.get("secType", "FOR"),
"side": str(b["side"]), "qty": str(b["qty"]),
"px": str(b.get("px", 0)), "type": str(b["type"]),
"tif": str(b.get("tif", 1)),
"sl": str(b.get("sl", 0)), "tp": str(b.get("tp", 0)),
"execDst": "INTX", "minQty": "0", "stopPx": "0",
"qtyRsrv": "0", "maxShow": "0", "execBrk": "",
"execInst": b.get("execInst", "u"),
"px2": str(b.get("px2", 0)), "txTime": now_utc(),
"handlInst": "1", "prnAgc": str(b.get("prnAgc", "false")).lower(),
"slpg": str(b.get("slpg", 0)), "tktType": str(b.get("tktType", 0)),
"tktNo": str(b.get("tktNo", "")), "refTktNo": str(b.get("refTktNo", "")),
"comment": b.get("comment", ""),
}
@app.route("/api/order", methods=["POST"])
def place_order():
err = require_login()
if err: return err
data, code = ltm_rest({"MT": "OrdReq", "OrdReq": _build_ord(request.json or {})})
return jsonify(data), code
@app.route("/api/cancel", methods=["POST"])
def cancel_order():
err = require_login()
if err: return err
b = request.json or {}
data, code = ltm_rest({
"MT": "OrdCxlReq",
"OrdCxlReq": {
"acct": state["account"], "clOrdId": str(b["orderId"]),
"sym": b["sym"], "secType": b.get("secType", "FOR"),
"side": str(b.get("side", 1)), "txTime": now_utc(),
}
})
return jsonify(data), code
@app.route("/api/close_position", methods=["POST"])
def close_position():
err = require_login()
if err: return err
b = request.json or {}
close_side = "2" if str(b.get("openSide", 1)) == "1" else "1"
data, code = ltm_rest({
"MT": "OrdCxlRepReq",
"OrdCxlRepReq": {
"acct": state["account"], "orgClOrdId": str(b["orgClOrdId"]),
"sym": b["sym"], "secType": b.get("secType", "FOR"),
"side": close_side, "qty": str(b["qty"]),
"px": "0", "type": "1", "tif": "3",
"sl": "0", "tp": "0",
"execDst": "INTX", "minQty": "0", "stopPx": "0",
"qtyRsrv": "0", "maxShow": "0", "execBrk": "",
"execInst": "v", "px2": "0", "txTime": now_utc(),
"handlInst": "1", "prnAgc": "false", "slpg": "0",
"tktType": "1", "tktNo": str(b["tktNo"]), "refTktNo": str(b["tktNo"]),
}
})
return jsonify(data), code
@app.route("/api/modify_sltp", methods=["POST"])
def modify_sltp():
err = require_login()
if err: return err
b = request.json or {}
close_side = "2" if str(b.get("openSide", 1)) == "1" else "1"
data, code = ltm_rest({
"MT": "OrdCxlRepReq",
"OrdCxlRepReq": {
"acct": state["account"], "orgClOrdId": str(b["orgClOrdId"]),
"sym": b["sym"], "secType": b.get("secType", "FOR"),
"side": close_side, "qty": str(b["qty"]),
"px": str(b.get("sl", 0)), "type": "U", "tif": "1",
"sl": "0", "tp": "0",
"execDst": "INTX", "minQty": "0", "stopPx": "0",
"qtyRsrv": "0", "maxShow": "0", "execBrk": "",
"execInst": "u", "px2": str(b.get("tp", 0)), "txTime": now_utc(),
"handlInst": "1", "prnAgc": "false", "slpg": "0",
"tktType": "1", "tktNo": str(b["tktNo"]), "refTktNo": str(b["tktNo"]),
}
})
return jsonify(data), code
# ============================================================
if __name__ == "__main__":
print("=" * 52)
print(" 槓桿全球贏家 API 交易前台")
print(" 請開啟瀏覽器前往: http://localhost:5000")
print("=" * 52)
socketio.run(app, host="0.0.0.0", port=5000, debug=False)
pip install flask flask-socketio requests websocket-client cryptography2. 複製上方程式碼存成
server.py,與 dashboard.html 放在同一資料夾3. 修改
PFX_PATH 和 PFX_PASSWORD(程式最上方標有 ★ 的區塊)4. 執行:
python server.py5. 瀏覽器開啟
http://localhost:5000,輸入 API Key、密碼、帳戶號碼即可登入