文件
開啟 Studio

教學

下單:單一策略

本頁內容7

這一頁帶你把一個策略變成證券帳戶裡的委託:回測 → 換算張數 → 連接券商 → 預覽後下單。要同時跑好幾個策略,請看下單:多策略。

從回測到下單

確認 FinLab 版本

交易系統需要 finlab>=0.3.0.dev1,請先確認安裝的版本。

在開盤前跑一次策略

在下一個交易日開盤前執行你的策略,取得回測報告:

from finlab import backtest

# 寫你自己的策略
report = backtest.sim(...)

回測用 T 日收盤後已公布的資料產生訊號,在下一個交易日成交;實盤也照這個節奏,在開盤前算好要持有的股票:

  1. T 日收盤後 當天資料更新 日期是 T 的那一列,只用到 T 日已公布的資料。
  2. T 日 產生訊號 position 在 T 日那一列,決定要持有哪些股票。
  3. 下一個交易日 成交 價格依 trade_at_price,預設收盤價。
  4. 到下次換股 持有 resample='M' 在每月最後一個交易日後換股。

換算每檔股票的張數

先看最近一期的部位:

print(report.current_trades)
範例輸出
stock_identry_dateexit_dateentry_sig_dateexit_sig_datepositionperiodentry_indexexit_indexreturnentry_priceexit_pricemaegmfebmfemddpdaysnext_weights
1416 廣豐2021-10-01NaT2021-09-302022-06-300.510197159.03566.0-1.0-3.539823e-0210.60NaN-0.0530970.0707960.070796-0.11570237.00.060665
1453 大將2021-10-01NaT2021-09-302022-06-300.510197159.03566.0-1.02.348165e+008.43NaN-0.0867632.4037820.245829-0.62458560.00.171264
1524 耿鼎2022-04-01NaT2022-03-312022-06-300.61163239.03686.0-1.0-3.330669e-1610.60NaN-0.4766580.0151520.000000-0.4766582.00.122168
2543 皇昌2021-10-01NaT2021-09-30NaT0.510197159.03566.0-1.01.073232e-017.46NaN-0.0631310.3636360.008838-0.26851944.00.070298
2701 萬企2022-04-01NaT2022-03-312022-06-300.61163239.03686.0-1.0-3.330669e-1612.05NaN-0.0251050.0292890.029289-0.05284618.00.063107

確認沒問題後,用 Position.from_report() 依投入的總資金換算每檔股票的張數:

from finlab.online.order_executor import Position

# 總資金
fund = 1000000
position = Position.from_report(report, fund)
print(position)
[{'stock_id': '2330', 'quantity': 1, 'order_condition': <OrderCondition.CASH: 1>}]

這個 Position 代表:你的帳戶中只希望有一張 2330 台積電,並且是現貨。台股的 quantity 單位是張(1,000 股);美股帳戶是股數。

預設只買整張,分到的資金不足一張的股票會是 0 張。想買零股、自己指定張數或合併多個策略,見下方調整部位。

安裝券商套件並連接帳戶

台股支援玉山證券、永豐證券、台新證券(原元富證券)及富邦證券,美股支援嘉信(Charles Schwab),擇一使用即可。帳號密碼設成環境變數,只需設定你要用的券商。

  1. 照 config 教學取得 config 設定檔。
  2. 登入玉山證券 API,取得市場行情 Token。
  3. 安裝套件,設定環境變數並連線:
pip install esun-trade
from finlab.online.esun_account import EsunAccount
import os

os.environ['ESUN_CONFIG_PATH'] = '玉山證券交易設定檔(config.ini.example)路徑'
os.environ['ESUN_MARKET_API_KEY'] = '玉山證券的行情API Token'
os.environ['ESUN_ACCOUNT_PASSWORD'] = '玉山證券的帳號密碼'
os.environ['ESUN_CERT_PASSWORD'] = '玉山證券的憑證密碼'

acc = EsunAccount()
舊版 Fugle 環境變數(仍可使用)
from finlab.online.fugle_account import FugleAccount
import os

os.environ['FUGLE_CONFIG_PATH'] = '玉山證券交易設定檔(config.ini.example)路徑'
os.environ['FUGLE_MARKET_API_KEY'] = '玉山證券的行情API Token'
os.environ['FUGLE_ACCOUNT_PASSWORD'] = '玉山證券的帳號密碼'
os.environ['FUGLE_CERT_PASSWORD'] = '玉山證券的憑證密碼'

acc = FugleAccount()

下單錯誤代碼請見玉山證券文件;若不熟券商 API,可先照券商教學練習。

檢查處置股與警示股

用 OrderExecutor 比對帳戶現有部位與 position,之後的下單都由它執行:

from finlab.online.order_executor import OrderExecutor

order_executer = OrderExecutor(position, account=acc)

# 顯示下單部位是否有警示股及相關資訊
order_executer.show_alerting_stocks()
=== 處置股/警示股警告 ===
以下待下單股票在處置股或信用警示名單中:
買入 8101 0.429 張 - 總價約         2672.67
請注意:處置股可能有預收款券、分盤交易等限制。

若沒有待下單股票落在名單中,則只會顯示「待下單股票中無處置股或警示股。」

下單部位含有「全額交割股」、「處置股」、「警示股」時,需先於證券帳戶圈存,作法依券商而定,例如永豐圈存。依 show_alerting_stocks() 的結果圈存後,再繼續下單。

先預覽,再下單

先用瀏覽模式看會送出哪些委託,這一步不會真的下單:

order_executer.create_orders(view_only=True)

確認無誤後才實際下單。預設使用最近一筆成交價當成限價;初次使用建議收市時測試。

# 會真的下單
order_executer.create_orders()

委託送出後還沒成交時,可以更新限價或刪單:

# 更新限價(將最後一筆成交價當成新的限價)
order_executer.update_order_price()

# 刪除所有委託單
order_executer.cancel_orders()

調整部位

Position 可以直接建構,也可以相加減,用來改買零股、指定張數或合併策略。

改買零股

在 from_report() 加上 odd_lot=True:

# 整股
position = Position.from_report(report, fund)

# 零股
position = Position.from_report(report, fund, odd_lot=True)

自己指定張數

直接用「股票代號: 張數」建構 Position:

position = Position({'2330': 1, '1101': 1.001})
[{'stock_id': '2330', 'quantity': 1, 'order_condition': <OrderCondition.CASH: 1>}
 {'stock_id': '1101', 'quantity': 1.001, 'order_condition': <OrderCondition.CASH: 1>}]

增減部位

Position 可以相加減:

# 減一張 2330 股票
new_position = position - Position({'2330': 1})
# 增加一張 1101 股票
new_position = position + Position({'1101': 1})

合併多個策略

多個策略的部位直接相加,再交給同一個 OrderExecutor:

from finlab import backtest
from finlab.online.order_executor import Position

report1 = backtest.sim(...)
report2 = backtest.sim(...)

position1 = Position.from_report(report1, 1000000)  # 策略操作金額一百萬
position2 = Position.from_report(report2, 1000000)  # 策略操作金額一百萬

total_position = position1 + position2

要每天管理多個策略的權重、換股日與停損停利,請用下單:多策略的 PortfolioSyncManager。

查看帳戶目前的部位

連線後的帳戶物件都可以用 get_position() 查目前的部位,例如永豐或玉山證券:

from finlab.online.sinopac_account import SinopacAccount

acc = SinopacAccount()
print(acc.get_position())
[{'stock_id': '2330', 'quantity': 1, 'order_condition': <OrderCondition.CASH: 1>}
 {'stock_id': '1101', 'quantity': 1.001, 'order_condition': <OrderCondition.CASH: 1>}]

常見錯誤與解決方法

錯誤 1:帳戶連線失敗

現象:執行 acc = SinopacAccount() 或 acc = EsunAccount() 時拋出連線錯誤:

from finlab.online.sinopac_account import SinopacAccount

acc = SinopacAccount()
# ConnectionError: 無法連線至券商 API

原因:

  • 環境變數未正確設定(API KEY、憑證路徑等)
  • 憑證檔案不存在或路徑錯誤
  • 憑證密碼錯誤
  • 網路連線問題或券商 API 服務暫停

解決方法:依序檢查環境變數、憑證檔案,再試著連線並看錯誤類型。

永豐證券的檢查程式
import os
from finlab.online.sinopac_account import SinopacAccount

# 步驟 1:檢查環境變數是否設定
required_env_vars = [
    'SHIOAJI_API_KEY',
    'SHIOAJI_SECRET_KEY',
    'SHIOAJI_CERT_PERSON_ID',
    'SHIOAJI_CERT_PATH',
    'SHIOAJI_CERT_PASSWORD',
]

print("=== 環境變數檢查 ===")
for var in required_env_vars:
    value = os.environ.get(var)
    if value:
        # 隱藏敏感資訊
        if 'KEY' in var or 'PASSWORD' in var:
            print(f"✅ {var}: {'*' * 8} (已設定)")
        else:
            print(f"✅ {var}: {value}")
    else:
        print(f"❌ {var}: 未設定")

# 步驟 2:檢查憑證檔案是否存在
cert_path = os.environ.get('SHIOAJI_CERT_PATH')
if cert_path and os.path.exists(cert_path):
    print(f"\n✅ 憑證檔案存在:{cert_path}")
else:
    print(f"\n❌ 憑證檔案不存在:{cert_path}")
    print("請確認憑證路徑是否正確")
    exit(1)

# 步驟 3:嘗試連線並捕捉錯誤
try:
    acc = SinopacAccount()
    print("\n✅ 帳戶連線成功")

    # 驗證帳戶狀態
    print(f"✅ 現金餘額:NT$ {acc.get_cash():,.0f}")

except FileNotFoundError as e:
    print(f"\n❌ 憑證檔案錯誤:{e}")
    print("請檢查 SHIOAJI_CERT_PATH 是否正確")

except PermissionError as e:
    print(f"\n❌ 憑證權限錯誤:{e}")
    print("請確認憑證密碼是否正確(SHIOAJI_CERT_PASSWORD)")

except ConnectionError as e:
    print(f"\n❌ 網路連線錯誤:{e}")
    print("可能原因:")
    print("1. 網路連線不穩定")
    print("2. 券商 API 服務暫時無法使用")
    print("3. API KEY 或 SECRET KEY 錯誤")
    print("請稍後再試或聯繫券商客服")

except Exception as e:
    print(f"\n❌ 未知錯誤:{e}")
    print("請檢查所有環境變數是否正確設定")
    print("參考教學:https://doc.finlab.tw/details/order_api/")
玉山證券的檢查程式
import os
from finlab.online.esun_account import EsunAccount

# 檢查玉山證券環境變數
required_vars = [
    'ESUN_CONFIG_PATH',
    'ESUN_MARKET_API_KEY',
    'ESUN_ACCOUNT_PASSWORD',
    'ESUN_CERT_PASSWORD',
]

for var in required_vars:
    if not os.environ.get(var):
        print(f"❌ {var} 未設定")
        print(
            f"   請參考:https://www.esunsec.com.tw/trading-platforms/api-trading/docs/trading/quick-start/"
        )
        exit(1)

# 檢查 config.ini 是否存在
config_path = os.environ.get('ESUN_CONFIG_PATH')
if not os.path.exists(config_path):
    print(f"❌ Config 檔案不存在:{config_path}")
    exit(1)

try:
    acc = EsunAccount()
    print("✅ 玉山證券帳戶連線成功")
except Exception as e:
    print(f"❌ 連線失敗:{e}")
    print("請檢查 config.ini 內容是否正確")

錯誤 2:下單前安全檢查失敗

現象:資金不足、持倉過多或單股權重過高。

原因:

  • 可用資金不足以買入 position 指定的股票
  • 策略產生的持股數量超過風險限制
  • 單一股票權重過高(未分散風險)

解決方法:下單前實施多層安全檢查:持倉數量、單股權重、可用資金,最後再檢查處置股。

下單前安全檢查的範例函式
from finlab.online.order_executor import OrderExecutor, Position
from finlab.online.sinopac_account import SinopacAccount

def safe_order_check(
    position, account, max_stocks=50, max_weight=0.15, min_cash_buffer=0.1
):
    """
    下單前安全檢查函數

    Args:
        position: Position 物件
        account: 券商帳戶物件
        max_stocks: 最大持股數量(預設 50 檔)
        max_weight: 單股最大權重(預設 15%)
        min_cash_buffer: 最小現金緩衝比例(預設 10%)

    Raises:
        ValueError: 檢查不通過時拋出錯誤
    """
    print("=== 下單安全檢查 ===\n")

    # 以最新價格計算每檔目標持股的市值(台股一張 1,000 股)
    price = account.get_price([p['symbol'] for p in position])
    values = {
        p['symbol']: float(p['quantity']) * 1000 * price[p['symbol']] for p in position
    }
    total_value = sum(values.values())

    # 檢查 1:持倉數量
    print(f"持倉數量:{len(values)} 檔")
    if len(values) > max_stocks:
        raise ValueError(
            f"❌ 持倉過多({len(values)} 檔),超過限制 {max_stocks} 檔\n"
            f"   建議:使用 .is_largest(N) 限制選股數量"
        )
    print(f"✅ 持倉數量檢查通過(<= {max_stocks} 檔)\n")

    # 檢查 2:單一持股權重
    for symbol, value in values.items():
        weight = value / total_value if total_value > 0 else 0
        print(f"  {symbol}: NT$ {value:,.0f} ({weight:.1%})")

        if weight > max_weight:
            raise ValueError(
                f"❌ 單股權重過高:{symbol} 佔 {weight:.1%},超過限制 {max_weight:.0%}\n"
                f"   建議:使用 position_limit 參數限制單股權重"
            )

    print(f"\n✅ 單股權重檢查通過(<= {max_weight:.0%})\n")

    # 檢查 3:可用資金
    available = account.get_total_balance()
    cash_buffer = available * min_cash_buffer
    print(f"帳戶淨值:NT$ {available:,.0f},目標市值:NT$ {total_value:,.0f}")

    if total_value > (available - cash_buffer):
        raise ValueError(
            f"❌ 資金不足!\n"
            f"   帳戶淨值:NT$ {available:,.0f}\n"
            f"   目標市值:NT$ {total_value:,.0f}\n"
            f"   現金緩衝:NT$ {cash_buffer:,.0f}(保留 {min_cash_buffer:.0%})\n"
            f"   建議:降低傳給 Position.from_report() 的 fund 或追加資金"
        )

    print(f"✅ 資金檢查通過(保留 {min_cash_buffer:.0%} 緩衝)\n")

    # 檢查 4:處置股/警示股
    print("⚠️  提醒:下單前請執行 order_executer.show_alerting_stocks() 檢查處置股\n")

    print("=" * 50)
    print("✅ 所有安全檢查通過,可以執行下單")
    print("=" * 50)

# 使用範例
try:
    acc = SinopacAccount()
    position = Position.from_report(report, fund=1000000)

    # 執行安全檢查
    safe_order_check(position, acc, max_stocks=30, max_weight=0.10)

    # 通過檢查後,執行下單
    order_executor = OrderExecutor(position, account=acc)

    # 先預覽(不會真的下單)
    print("\n=== 預覽訂單 ===")
    order_executor.create_orders(view_only=True)

    # 確認無誤後,執行下單
    # order_executor.create_orders()  # 取消註解以實際下單

except ValueError as e:
    print(f"\n{e}")
    print("\n請修正問題後再執行下單")

except Exception as e:
    print(f"\n❌ 未預期的錯誤:{e}")
    print("請聯繫技術支援或查看詳細錯誤訊息")

錯誤 3:訂單被拒絕

現象:執行 create_orders() 後,部分或全部訂單被券商拒絕:

order_executor.create_orders()
# 訂單狀態:部分成功、部分失敗

常見拒單原因:

  1. 處置股未圈存
  2. 股票暫停交易
  3. 漲跌停鎖死
  4. 零股交易時段錯誤
  5. 帳戶權限不足

解決方法:先檢查處置股並圈存,確認在交易時段內,預覽後再下單,最後查看仍在委託中的訂單。

下單前檢查並逐筆確認的範例
from datetime import datetime
from zoneinfo import ZoneInfo

from finlab.online.order_executor import OrderExecutor

# 建立 OrderExecutor
order_executor = OrderExecutor(position, account=acc)

# 步驟 1:檢查處置股/警示股(結果直接印出,沒有回傳值)
print("=== 檢查處置股 ===")
order_executor.show_alerting_stocks()
print("若上面列出處置股/警示股,請至券商平台圈存後再下單")
print("參考圈存教學:")
print("  - 富果:https://support.fugle.tw/trading/trading-troubleshoot/5231/")
print("  - 永豐:https://www.sinotrade.com.tw/richclub/freshman/...")
# 等待用戶手動圈存後再繼續
input("圈存完成(或不需圈存)後,按 Enter 繼續...")

# 步驟 2:檢查交易時段(台北時間)
now = datetime.now(ZoneInfo('Asia/Taipei'))
minutes = now.hour * 60 + now.minute

if now.weekday() < 5 and 9 * 60 <= minutes <= 13 * 60 + 30:
    print("✅ 當前為交易時段(09:00-13:30)")
else:
    print("⚠️  當前為非交易時段")
    print("   整股交易時段:09:00-13:30")
    print("   零股交易時段:09:00-13:30(盤中)、13:40-14:30(盤後)")

# 步驟 3:先預覽,再下單
print("\n=== 預覽訂單 ===")
order_executor.create_orders(view_only=True)

confirmation = input("\n確認無誤後輸入 'YES' 執行下單:")
if confirmation == 'YES':
    try:
        order_executor.create_orders()

        # 步驟 4:查看仍在委託中的訂單
        print("\n=== 委託中的訂單 ===")
        for order_id, order in acc.get_orders().items():
            print(
                f"{order_id} {order.stock_id} {order.action.name} "
                f"{order.filled_quantity}/{order.quantity} {order.status.name}"
            )
    except Exception as e:
        print(f"\n❌ 下單失敗:{e}")
        print("請檢查券商的錯誤訊息、可用資金與交易時段")
else:
    print("已取消下單")

錯誤 4:零股下單失敗

現象:使用 odd_lot=True 但訂單失敗:

position = Position.from_report(report, fund, odd_lot=True)
order_executor = OrderExecutor(position, account=acc)
order_executor.create_orders()
# 錯誤:零股交易時段不符

原因:

  • 零股交易時段限制(盤中 09:00-13:30、盤後 13:40-14:30,台北時間)
  • 部分券商不支援零股 API 交易
  • 零股成交量不足

解決方法:在零股交易時段內下單,或改用整股交易(移除 odd_lot=True)。

檢查零股交易時段的範例
from datetime import datetime
from zoneinfo import ZoneInfo

from finlab.online.order_executor import OrderExecutor, Position

# 檢查是否在零股交易時段(台北時間)
now = datetime.now(ZoneInfo('Asia/Taipei'))
minutes = now.hour * 60 + now.minute

# 盤中零股:09:00-13:30
is_intraday_odd_lot = 9 * 60 <= minutes <= 13 * 60 + 30

# 盤後零股:13:40-14:30
is_afterhours_odd_lot = 13 * 60 + 40 <= minutes <= 14 * 60 + 30

if not (is_intraday_odd_lot or is_afterhours_odd_lot):
    print("⚠️  當前不在零股交易時段")
    print("   盤中零股:09:00-13:30")
    print("   盤後零股:13:40-14:30")
    print("\n建議:")
    print("1. 等待零股交易時段再下單")
    print("2. 改用整股交易(移除 odd_lot=True)")
else:
    # 建立零股部位
    position = Position.from_report(report, fund=500000, odd_lot=True)
    print("✅ 當前在零股交易時段,可執行下單")

    # 執行下單
    try:
        order_executor = OrderExecutor(position, account=acc)
        order_executor.create_orders(view_only=True)  # 先預覽
        # order_executor.create_orders()  # 確認後取消註解

    except Exception as e:
        print(f"❌ 零股下單失敗:{e}")
        print("\n可能原因:")
        print("1. 券商不支援零股 API 交易(請聯繫券商確認)")
        print("2. 零股成交量不足(建議改用整股)")
        print("3. 網路連線問題")

錯誤 5:有訊號但沒有下單

現象:策略有選股,但 Position.from_report() / Position.from_weight() 產生的部位是空的(或少了幾檔),create_orders() 沒有送出任何委託,並出現 ZeroShareAllocationWarning:

ZeroShareAllocationWarning: 2 of 2 stocks with a non-zero target weight get 0 shares because their allocated fund cannot buy one board lot (1000 shares): 2330 (one lot 900,000 vs allocated 15,000), 1101 (one lot 40,000 vs allocated 15,000). The position is empty, so no orders will be placed. Use odd_lot=True to trade odd lots, or increase fund.

原因:odd_lot=False(預設)時只買整張,每檔股票分到的資金(fund × 權重)會無條件捨去成整張。分到的資金不足一張(股價 × 1000)的股票就是 0 股;全部都不足時部位就是空的。

解決方法:改用零股,或提高 fund。

position = Position.from_report(report, fund=30000, odd_lot=True)

讓實盤穩定執行

先用模擬帳戶測試

大多數券商提供模擬帳戶,建議先測試 1-2 週,確認所有流程正常後再使用真實帳戶。

設定每日下單排程

import time
from datetime import datetime

import schedule
from finlab.online.order_executor import OrderExecutor, Position

# acc 是前面建立的券商帳戶;safe_order_check 見上方錯誤 2

def daily_rebalance():
    """每日收盤後自動調倉"""
    try:
        # 1. 執行策略(你自己的函式,回傳 backtest.sim(...) 的報告)
        report = run_strategy()

        # 2. 計算部位
        position = Position.from_report(report, fund=1000000)

        # 3. 安全檢查
        safe_order_check(position, acc)

        # 4. 執行下單
        order_executor = OrderExecutor(position, account=acc)
        order_executor.create_orders()

        print(f"✅ {datetime.now()} - 下單完成")

    except Exception as e:
        print(f"❌ {datetime.now()} - 下單失敗:{e}")
        # 發送通知(LINE/Email)

# 每日 14:00 執行(13:30 收盤後;以電腦的時鐘為準,請設為台北時間)
schedule.every().day.at("14:00").do(daily_rebalance)

while True:
    schedule.run_pending()
    time.sleep(60)

記錄所有交易

import json
from datetime import datetime

def log_trade(position, orders, status):
    """記錄交易日誌"""
    log_entry = {
        'timestamp': datetime.now().isoformat(),
        'position': position,
        'orders': orders,
        'status': status,
    }

    with open('trade_log.json', 'a') as f:
        f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')

上線前的檢查清單

請務必在模擬帳戶充分測試後,才使用真實資金! 建議測試清單:

  • 帳戶連線穩定無錯誤
  • 下單流程完整執行
  • 持倉計算正確(與預期一致)
  • 資金控制正常(不超支)
  • 異常處理機制有效
  • 連續運行 1-2 週無問題

只有在以上項目全部確認後,才考慮使用真實資金!

下一步

更多疑難排解請看常見問題 FAQ。

↑↓ 選擇 Enter 開啟 Esc 關閉