IBKR Flex 自動存檔要先向 SendRequest 要求產檔,拿到 ReferenceCode,再交給 GetStatement 取回檔案。下載後先檢查回應內容,因為錯誤訊息也可能是一份 XML。這篇用一支本機 Python 程式,把通過格式初檢的原檔存進指定資料夾;帳戶、期間和一筆已知紀錄核對完,才移入正式封存區。
開始前準備可登入 Client Portal 的帳戶、一份已建好的 Activity Flex Query,以及已安裝 Python 3.9 或以上版本的 Windows/Mac 電腦。本文依官方文件撰寫,程式只用合成回應做過本機測試,沒有以真實帳戶或 token 驗證下載。第一次請只選一戶,先完成一次人工驗收。

先留一份手動基準檔,固定範本與輸出格式
先到 Performance & Reports → Flex Queries,選定帳戶,在既有範本旁按 Edit 鉛筆,入口見官方編輯範本指南。在 Delivery Configuration 將 Format 設為 XML,核對 Accounts,並把 Period 存成預定期間,例如 Last Month。依編輯畫面完成確認與保存後,重新按鉛筆進入,確認 XML、帳戶及 Period 都已留下;官方範本設定指南列有這些欄位,但編輯指南未列保存按鈕名稱,找不到時先請客服確認,不只關掉視窗。
保存確認後,回清單按同列右側藍色 Run 箭頭,依官方手動執行流程核對當次帳戶、Period 和 Format 再 Run。這次沿用剛存好的期間及 XML,下載後將原檔命名為 baseline.xml;若是壓縮檔,先解壓縮取出 XML。Run 視窗的當次選項與已保存的範本設定要分開確認:本篇 API 程式沿用後者,不能只在手動 Run 暫改日期或格式。
用純文字編輯器開啟 baseline.xml,只查看、不另存覆寫。以下以含 Cash Transactions 的 Activity XML 為定位示例,欄位結構參照開源解析器 ibflex 的 FlexStatement/CashTransaction 定義,不是所有範本都具備的欄位保證,也不需安裝該套件。用編輯器的搜尋功能找 <FlexStatement ,查看同一個標籤內的 accountId、fromDate、toDate;再找 <CashTransaction ,看一筆活動的 dateTime、currency、amount 與描述。注意 FlexStatementResponse 是另一個名稱,不能只看字首。
<FlexStatement accountId="SYNTHETIC" fromDate="20261001" toDate="20261002">
<CashTransactions>
<CashTransaction accountId="SYNTHETIC" dateTime="20261001;120000"
currency="USD" amount="1000" description="SYNTHETIC EVENT" />
</CashTransactions>
</FlexStatement>
上面是省略外層的合成片段,帳戶、日期與金額全是假設值。讀法是帳戶 SYNTHETIC、報表 2026-10-01~2026-10-02,其中示範一筆 10 月 1 日的 USD 1,000 活動;實際日期格式還要對照範本的 General Configuration。官方 Cash Transactions 欄位表可用來對照 Account ID、Date/Time、Currency、Amount、Description 的意義。請拿自己的已知活動核對,並檢查所有 FlexStatement 的帳戶及起訖;缺少必要欄位、空活動或無法辨認期間,都先留在 pending,回範本補查 Sections、所選欄位與期間後重跑,不從檔名或第一筆日期猜起訖。
核對完成後,記下範本名稱、Query ID、Period 與格式。Query ID 是範本編號。在 Flex Queries 頁按範本左側資訊圖示,從資訊框頂端記下 Query ID;這是官方取號步驟,不是登入帳號或 Query Name。若還沒建好範本,先完成YTD 與 Flex 現金明細教學再回來。

本篇程式只處理 XML:它用已人工核對的基準檔辨識外層結構,不解析 CSV,也不會把 ZIP 解壓縮。官方 Delivery Configuration 說明列有 XML、CSV 與文字輸出;若收到其他格式,先停下來改回這份範本的設定。不要只把副檔名改成 .xml。
啟用 IBKR Flex 自動存檔:token 留在本機
到 Performance & Reports → Flex Queries → Flex Web Service Configuration,按齒輪進入設定,啟用 Flex Web Service Status,再按 Save 保存。頁面提供 Current Token,之後程式便能透過 HTTPS 取得預先設定的 Flex Query,不必每次登入 Portal。這是 官方啟用指南描述的報表存取流程。
需要設定到期或限制來源 IP 時,進入 Generate New Token 的換發流程,選 Expire After,並依需要填 Valid for IP Address,產生後保存。官方 token 文件列出的有效期間為 6 小時至 1 年,也提醒 token 可能讀到範本納入的連結帳戶資料;若是多戶結構,不能只因這次打算下載一戶,就把 token 當作只有單戶範圍。

首次執行採用隱藏輸入,不把 token 寫進程式或命令列。若日後需要無人值守下載,可讓程式從自己的本機文字檔讀取 token,但檔案要限制為執行者可讀,並排除雲端同步、共用資料夾與版本控制。Mac 在 Finder 的檔案「取得資訊 → 共享與權限」檢查讀取者;Windows 在檔案「內容 → 安全性」檢查並限制讀取權限。這是本篇的憑證保存建議,不是 IBKR 替你保管憑證的功能。
兩階段下載的 q 不同:第一次是範本,第二次是產檔結果
依 SendRequest 參數說明,第一個請求帶 t=token、q=Query ID、v=3。收到 Status=Success 和 ReferenceCode,只表示產檔請求成功;還沒有下載到報表。
第二個請求送到 GetStatement,t 與版本不變,q 必須換成剛才的 ReferenceCode,用來辨認這次產出的報表。回傳格式取決於範本設定,失敗則可能回 XML 錯誤。即使 HTTP 回 200、檔案也能開啟,仍須解析內容。
以下程式沿用已保存範本的期間,不另外帶日期覆寫參數;這是 官方產檔文件所述的預設方式。它也依該文件忽略第一階段回應內的 url,使用固定的 IBKR GetStatement 端點。回傳格式由範本決定,因此連線前先確認已保存為 XML。
把程式存成本機檔,先下載到 pending 資料夾
將下面完整程式存成純文字 flex_download.py,與已確認的 baseline.xml 放在同一工作資料夾。只使用 Python 標準函式庫,不需安裝額外套件。程式每次取檔前等 10 秒;遇到「生成中」1019,才繼續用同一 ReferenceCode 取檔,取檔總次數最多 6 次,也就是首次之後最多重試 5 次。這些是本文的保守停止條件,不是 IBKR 承諾的產檔時間。
#!/usr/bin/env python3
"""AlphaLab local example; saves XML candidates for manual reconciliation."""
import argparse
from datetime import datetime, timezone
from getpass import getpass, GetPassWarning
import hashlib
import json
from pathlib import Path
import time
import warnings
from urllib.parse import urlencode
from urllib.request import Request, build_opener, HTTPRedirectHandler
from uuid import uuid4
import xml.etree.ElementTree as ET
BASE = 'https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/'
class Stop(Exception):
pass
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def xml(data):
try:
return ET.fromstring(data)
except ET.ParseError:
raise Stop('NOT_XML') from None
def error_code(root):
return root.findtext('.//ErrorCode')
def report_ok(data, baseline):
root = xml(data)
if (root.tag != baseline.tag or not len(root)
or root.find('.//ErrorCode') is not None
or root.find('.//Status') is not None):
raise Stop('NOT_REPORT')
return data
def request(stage, token, q):
url = BASE + stage + '?' + urlencode({'t': token, 'q': q, 'v': '3'})
req = Request(url, headers={'User-Agent': 'AlphaLab-Flex-Local/1.0'})
try:
with build_opener(NoRedirect()).open(req, timeout=30) as response:
return response.read()
except Exception:
# Never print the exception: it may contain the token-bearing URL.
raise Stop('HTTP_OR_NETWORK') from None
def download(token, query, baseline, emit, fetch=request, pause=time.sleep):
emit('SendRequest', 'START')
control = xml(fetch('SendRequest', token, query))
if control.findtext('Status') != 'Success':
code = error_code(control) or ''
raise Stop('SEND_' + code if code.isdigit() else 'SEND_INVALID_RESPONSE')
ref = control.findtext('ReferenceCode')
if not ref or not ref.isdigit():
raise Stop('MISSING_REFERENCE')
emit('SendRequest', 'REFERENCE', reference=ref)
for attempt in range(1, 7):
pause(10)
data = fetch('GetStatement', token, ref)
root = xml(data)
code = error_code(root)
emit('GetStatement', code or 'RECEIVED', attempt=attempt)
if code == '1019':
continue
if code:
raise Stop('GET_' + code if code.isdigit() else 'GET_ERROR')
return report_ok(data, baseline)
raise Stop('RETRY_LIMIT_1019')
def save_raw(folder, data):
folder.mkdir(parents=True, exist_ok=True)
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
path = folder / ('pending-' + stamp + '-' + uuid4().hex + '.xml')
created = False
try:
with path.open('xb') as handle:
created = True
handle.write(data)
except Exception:
if created:
path.unlink(missing_ok=True)
raise
return path, hashlib.sha256(data).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--query', required=True)
parser.add_argument('--baseline', type=Path, required=True)
parser.add_argument('--out', type=Path, required=True)
parser.add_argument('--token-file', type=Path)
args = parser.parse_args()
def emit(stage, result, **extra):
record = dict(at=datetime.now(timezone.utc).isoformat(),
stage=stage, result=result, **extra)
try:
with (args.out / 'run.jsonl').open('a', encoding='utf-8') as handle:
handle.write(json.dumps(record) + '\n')
except OSError:
raise Stop('LOG_WRITE_FAILED') from None
path = None
try:
try:
args.out.mkdir(parents=True, exist_ok=True)
except OSError:
raise Stop('OUTPUT_DIRECTORY') from None
baseline = xml(args.baseline.read_bytes())
if (baseline.tag == 'FlexStatementResponse' or not len(baseline)
or baseline.find('.//ErrorCode') is not None
or baseline.find('.//Status') is not None):
raise Stop('CHECK_BASELINE')
with warnings.catch_warnings():
warnings.simplefilter('error', GetPassWarning)
token = (args.token_file.read_text(encoding='utf-8').strip()
if args.token_file else getpass('Flex token (hidden): ').strip())
if not token or not args.query.isdigit():
raise Stop('CHECK_INPUT')
data = download(token, args.query, baseline, emit)
path, digest = save_raw(args.out, data)
emit('Local', 'PENDING', file=path.name, sha256=digest, bytes=len(data))
print('PENDING:', path)
print('Check account, report dates and one known event before archiving.')
return 0
except Exception as exc:
reason = str(exc) if isinstance(exc, Stop) else 'LOCAL_ERROR'
if reason not in ('OUTPUT_DIRECTORY', 'LOG_WRITE_FAILED'):
try:
emit('Local', 'FAILED', reason=reason)
except Exception:
print('LOG_UNAVAILABLE: retain this terminal result locally.')
else:
print('LOG_UNAVAILABLE: retain this terminal result locally.')
print('FAILED:', reason, '(old report files retained)')
if path is not None:
print('UNCONFIRMED_FILE:', path)
print('XML saved but completion not logged; keep it pending.')
return 1
if __name__ == '__main__':
raise SystemExit(main())
存好下載程式後,第一次連線前先做離線格式檢查。將下一段另存為同資料夾的純文字 check_flex_format.py,不要直接貼進 PowerShell。這是合成錯誤回應,只檢查錯誤分流,不是一次 IBKR 連線測試。
import flex_download as f
base = f.xml(b"<FlexQueryResponse><FlexStatements/></FlexQueryResponse>")
error = b"<FlexStatementResponse><Status>Fail</Status><ErrorCode>1015</ErrorCode></FlexStatementResponse>"
try:
f.report_ok(error, base)
except f.Stop:
print("錯誤已拒收")
else:
raise AssertionError("錯誤檔不應通過")
在該工作資料夾的終端機執行下列命令,預期顯示「錯誤已拒收」。若出現匯入錯誤,先查兩個檔案是否同資料夾、檔名是否被加上 .txt;未通過就先不連線。
# Mac
python3 check_flex_format.py
# Windows PowerShell
py -3 check_flex_format.py
在終端機切換到上述工作資料夾,把 123456 換成自己的 Query ID,把路徑換成想存放報表的位置。Mac 範例用 python3;Windows 範例用已安裝 Python 的 py -3。路徑含空白時保留引號:
# Mac
python3 flex_download.py --query 123456 --baseline baseline.xml --out "$HOME/IBKR/pending"
# Windows PowerShell
py -3 flex_download.py --query 123456 --baseline baseline.xml --out "$HOME\IBKR\pending"
看到 Flex token (hidden): 後在自己的終端機輸入 token。程式完成寫檔及完成紀錄後,才顯示 PENDING: 和新檔路徑;資料夾裡會多出一份帶 UTC 下載時間與隨機識別碼的 XML,以及 run.jsonl 紀錄。PENDING 只代表格式初檢通過,尚未完成對帳。程式保留收到的原始位元組,不重新輸出 XML;同次下載的 SHA-256 摘要和大小寫在紀錄裡,可用來追蹤保存的檔案。
原有報表不會被同名寫入取代。伺服器回錯誤時不把錯誤 XML 寫成報表;HTTP/網路錯誤也不印出可能含 token 的完整網址。可寫入時,run.jsonl 會記 ReferenceCode、執行階段與結果,沒有 token 與完整請求網址。若輸出目錄無法建立,程式以 FAILED: OUTPUT_DIRECTORY 停止;若紀錄無法寫入,則停止並顯示 LOG_UNAVAILABLE,不反覆嘗試寫同一份失敗紀錄。這些是本文程式的行為;XML 本身仍可能含帳戶資料。
若 XML 已寫出,但最後的完成紀錄失敗,畫面會列 UNCONFIRMED_FILE:,退出碼為 1。先把終端機顯示的檔名與錯誤結果留在自己的安全位置,該檔仍留 pending;修復權限或空間後,依「封存前核對」一節逐項人工核對,再另記檔名、核對結果與「本次完成紀錄未寫入」。不要只因檔案存在就移入 archive,或把舊的成功紀錄當成本次結果。
IBKR Flex 自動存檔失敗時,按錯誤碼停在正確位置
依 官方錯誤碼表,以下幾種錯誤要分開處理;程式對 1019 以外的錯誤均停止,避免自動化一直重送。
- 1019,生成中:本程式用同一 ReferenceCode 等待後再取檔,不重送 SendRequest。6 次仍未完成就退出,先留住紀錄,稍後再處理,不把空檔當完成。
- 1012,token 過期;1015,token 無效:回 Configuration 檢查或換發,更新本機來源後再執行。不要反覆提交同一組失效憑證。
- 1013,IP 限制:檢查本次連線的對外 IP 與已設限制;換網路後卡住,先確認這一層。
- 1014,query 無效;1017,reference code 無效:前者先核對 Query ID、帳戶選取與範本;後者先查第二階段是否誤用了 Query ID,或沿用了別次 ReferenceCode。這是診斷順序,不保證錯誤只有一種成因。
- 1018,請求過多:官方列每個 token 每秒 1 次、每分鐘 10 次的限制。先停止重送,檢查是否還有其他程式共用 token;本例的 10 秒間隔無法替其他程式保留額度。
- 1003、1004 或其他代碼:分別可能是報表不可用、尚未完整,其他情況查表處理;本例不自動重試這些代碼。保存時間、階段、代碼後再查,不改程式假裝成功。
若顯示 NOT_XML、NOT_REPORT 或 CHECK_BASELINE,先回 Portal 手動執行同一範本,確認 XML 格式與基準檔;HTTP_OR_NETWORK 先查網路和服務可用性。OUTPUT_DIRECTORY、LOG_WRITE_FAILED 或 LOCAL_ERROR 則查路徑、讀寫權限與磁碟空間,修好後才重跑;檢查 pending 是否留下未完成檔,不直接封存。若 run.jsonl 缺漏或末列截斷,保留原狀,另外保存終端機結果;有 UNCONFIRMED_FILE 時依前節人工核對。請客服協助時提供時間、階段、代碼與範本資料,不貼 token。
封存前核對帳戶與期間,再決定是否排程
真實下載的 XML 還要人工核對以下項目。程式只比對基準檔根元素、拒絕空結構及錯誤/狀態欄位,不會自行驗證帳戶、起訖或金額;格式正確但拿錯範本,仍可能進入 pending。
- 依前面的 XML 定位法,逐一核對 FlexStatement 的 accountId 與範本,確認沒有夾入其他帳戶;只在自己的本機紀錄保留完整帳號。
- 讀取 FlexStatement 的 fromDate/toDate,對回預期期間;欄位找不到就留 pending、回查範本。檔名上的 UTC 時間是下載時間,不是報表資料截止日;不要用第一筆活動日期代替報表起日。
- 依基準檔定位已知活動;若是現金活動,找 CashTransaction 的 dateTime、currency、amount 與描述,和手動基準檔對照;基準與新檔期間不同時,另跑同期間手動報表再比。佣金資料可接著用 佣金核對教學查。
- 確認上述項目後,用檔案總管或 Finder 把原檔移到獨立的
archive資料夾,保留對帳紀錄與原檔名。若要加註帳戶尾碼和報表期間,可在索引記錄,不改檔內資料。
對不上就留在 pending,標明「帳戶不符/期間不符/金額待查」,保留當次範本設定與兩份原檔,回查 Accounts、Period、Format、Sections 及所選欄位。修正後重新下載並核對;必要欄位仍缺少時,帶著範本資料請客服確認,不猜日期或補金額。
完成一次實際下載與人工核對後,再考慮排程執行同一支程式。無人值守時,可在上述命令另加 --token-file "/自己的私密路徑/flex-token.txt";檔案只放 token 本身。排程使用 Python、程式、基準檔、輸出資料夾和 token 檔的完整路徑,同一 token 的工作不要重疊。Mac 的排程與 Windows 工作排程器由你另行設定;這篇完成的是下載程式,不替讀者安裝系統排程。
排程仍只下載到 pending。保留退出碼和最近一次完成下載的時間,沒有新檔時查紀錄;0 表示待核對檔及其完成紀錄都已寫出,1 表示失敗,可能已有尚待處理的 XML,兩者都不是完成對帳的證明。每次範本欄位、帳戶或格式改動後,重新產出人工核對的基準檔,再確認下次下載結果。
練習換發 token,確認舊憑證真的不能再用
測試結束後,回 Flex Web Service Configuration,走 Generate New Token 換發並保存。官方啟用指南明確說,新 token 會使舊 token 失效。只在自己的本機用舊 token 再執行一次:若收到 IBKR 的 1015(token 無效),可記為舊值已被拒絕;1012(token 過期)只能確認已過期,不能證明是這次換發造成。記下實際回應後,換成新 token 重新下載與驗收。網路/HTTP 失敗、1013 IP 限制、本機寫檔錯誤或原因不明,都標「舊 token 驗證未完成」;沒有新檔本身不算撤銷證據。
暫時不需要服務時,將 Flex Web Service Status 停用並保存,再重新開啟 Configuration,確認仍顯示停用,作為這次設定的核對紀錄。若要另測請求是否被擋,記下實際回應;本文不指定停用後必回哪個錯誤碼,也不把一般下載失敗當作已完成伺服器端驗證。設定無法確認,或請求失敗原因不明,就保留「停用驗證未完成」並向客服確認。「停用服務」與「換發 token 使舊值失效」是兩個控制,關掉程式不等於撤銷憑證。若懷疑 token 外流,先停用或換發,再查本機保存位置及共用紀錄。需要補查帳戶與通知設定,可回 IBKR 開戶後安全設定地圖。

接著閱讀
左右滑動查看更多推薦






