雖然公眾資料庫和網路爬蟲,已經可以玩得不亦樂乎了,但所有研究最終的目的,還是要實施於一個真實賬戶,真金白銀的實際效益才有意義。尤其是「即時」性要求較高的策略,一定需要用程式補上「自動化下單」的部分,快速變動的資料如「行情報價」,「訂單狀態」,「帳戶庫存」等等,也需要程式嚴密監控,這就需要券商提供 api 介面了。欣逢富邦推出「新一代 api」 服務,用 python 就能玩,連接 python 生態系的豐富資源,讓策略發展增添無限的想像空間,怎不完爆!隱約看見寶庫的大門等著我去叩關了。
準備工作如官網 (https://www.fbs.com.tw/TradeAPI) 說明,照做就好:
在 mac 系統終於安裝成功,好感動。多年來,國內券商對 mac 非常不友善,終於有進步了,甚至 Apple 的 M 系列晶片,也就是 arm 版本都支援。我的 mac 雖然是採用 m2 晶片的,安裝 arm 版本竟然失敗,改安裝 x86 版本成功,原因是所用的開發環境 vscode 是「模擬 x86」環境的版本,這是為了個人其他專案的相容性要求。
# ! pip install fubon_neo-2.0.1-cp37-abi3-macosx_11_0_arm64.whl
! pip install fubon_neo-2.0.1-cp37-abi3-macosx_10_12_x86_64.whl
---------------------------------------------------------------
Processing ./fubon_neo-2.0.1-cp37-abi3-macosx_10_12_x86_64.whl
Collecting websocket-client==1.5.1 (from fubon-neo==2.0.1)
Downloading websocket_client-1.5.1-py3-none-any.whl.metadata (7.6 kB)
Requirement already satisfied: pyee>=9.0.4 in /Users/newman/Documents/prjPython/DataScience/.venv/lib/python3.9/site-packages (from fubon-neo==2.0.1) (9.1.1)
Requirement already satisfied: typing-extensions in /Users/newman/Documents/prjPython/DataScience/.venv/lib/python3.9/site-packages (from pyee>=9.0.4->fubon-neo==2.0.1) (4.12.2)
Downloading websocket_client-1.5.1-py3-none-any.whl (55 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 55.9/55.9 kB 591.1 kB/s eta 0:00:00a 0:00:01
Installing collected packages: websocket-client, fubon-neo
Attempting uninstall: websocket-client
Found existing installation: websocket-client 1.6.1
Uninstalling websocket-client-1.6.1:
Successfully uninstalled websocket-client-1.6.1
Successfully installed fubon-neo-2.0.1 websocket-client-1.5.1
coding 起手式,匯入套件,初始化 sdk,登入帳戶。注意把憑證檔案準備好。成功登入,取得帳戶基本資料。若一個人有多個帳戶,會一併傳回,以下存在 acc 陣列中。共兩個帳戶,一個股票,一個期貨。
from fubon_neo.sdk import FubonSDK, Order, FutOptOrder
from fubon_neo.constant import TimeInForce, OrderType, PriceType, MarketType, BSAction
from fubon_neo.constant import FutOptOrderType, FutOptPriceType, FutOptMarketType, CallPut
import os
from dotenv import load_dotenv
from datetime import datetime, timedelta
import pandas as pd
sdk = FubonSDK()
load_dotenv()
id = os.getenv('id')
pwd = os.getenv('pwd')
accounts = sdk.login(id, pwd, f"{id}.pfx" , f"{pwd}")
acc = accounts.data
acc
---
[Account {
name: "陳敏志",
branch_no: "xxxxx",
account: "xxxxx",
account_type: "stock",
},
Account {
name: "陳敏志",
branch_no: "xxxxx",
account: "xxxxx",
account_type: "futopt",
}]
整套 api 分成兩部分:「股票類」和「期權類」,各自又分為「交易類」(帳戶相關) 和「行情類」。既然要花時間玩券商的 api,首先當然要掌握自己的帳戶囉,查詢庫存是很基本的,有兩項功能可以用,一是庫存查詢,一是「未實現損益」查詢,後者也有庫存資訊,對我已經夠用:
# 庫存資訊,我沒用到
result = sdk.accounting.inventories(acc[0])
-------------------------------------------
# 未實現損益,包含庫存資訊。回傳陣列,先印出第一個元素,觀察資料結構:
unrealized_pnl = sdk.accounting.unrealized_gains_and_loses(acc[0])
unrealized_pnl.data[0]
----------------------
UnrealizedData {
date: "2024/11/08",
branch_no: "xxxxx",
account: "xxxxx",
stock_no: "00715L",
buy_sell: Buy,
order_type: Stock,
cost_price: 13.085,
tradable_qty: 1000,
today_qty: 1000,
unrealized_profit: 502,
unrealized_loss: 0,
}
為了在 python 中運用方便,把資料轉成 dataFrame 比較清楚:
df = pd.DataFrame([{'account': obj.account, 'date': obj.date,
'buy_sell': obj.buy_sell,
'stock_no': obj.stock_no,
'today_qty': obj.today_qty,
'market_value': f'{obj.cost_price * obj.today_qty
+ obj.unrealized_profit - obj.unrealized_loss: .0f}'
} for obj in unrealized_pnl.data])
df
此資訊我比較關心的「market_value」的部分,用其他欄位輾轉計算得到,系統直接提供的 cost_price,因為用死板的先進先出記帳法,根本就是有毒的資訊,所以直接忽略之。
# 期貨留倉,先列出一筆資料,觀察資料結構
h_position = sdk.futopt_accounting.query_hybrid_position(acc[1])
h_position.data[0]
------------------
HybridPosition {
date: "2024/11/08",
branch_no: "xxxxx",
account: "xxxxx",
is_spread: false,
position_kind: 1,
symbol: "FITM",
expiry_date: "202412",
strike_price: None,
call_put: None,
buy_sell: Sell,
price: 23190.4666,
orig_lots: 15,
tradable_lot: 15,
order_type: New,
currency: "TWD",
market_price: "23629",
initial_margin: 0.0,
maintenance_margin: 0.0,
clearing_margin: 0.0,
initial_margin_all_single: 0.0,
opt_value: 0.0,
opt_long_value: 0.0,
opt_short_value: 0.0,
profit_or_loss: 0.0,
premium: 0.0,
spreads: None,
}
再挑出有用欄位,轉成 dataframe。一樣重點關心最新「市值」變化,製作一個欄位來表達。
df = pd.DataFrame([{'account': obj.account, 'date': obj.date,
'buy_sell': obj.buy_sell,
'symbol': obj.symbol, 'market_price': obj.market_price,
'orig_lots': obj.orig_lots,
'market_value': obj.orig_lots * int(obj.market_price)
} for obj in h_position.data])
df
這個功能我超期待,因為這是平時使用手機 app 最常使用的功能。測試成功後發現,只能查「兩天」內的交易紀錄!真是太可惜了。--> 希望券商能改善,至少給查個半年吧!
# 準備下 30 天的條件去執行,很可惜只得到兩天,這是系統限制,這樣很爛,非常可惜
# 一樣先列出一筆,觀察資料結構
endDate = datetime.today()
startDate = endDate - timedelta(days = 30)
result = sdk.stock.filled_history(acc[0],
start_date= startDate.strftime("%Y%m%d"),
end_date=endDate.strftime("%Y%m%d"))
filled_orders = result.data
filled_orders[0]
----------------
FilledData {
date: "2024/11/07",
branch_no: "20212",
account: "xxxxx",
order_no: "x1621",
stock_no: "3711",
buy_sell: Sell,
order_type: Stock,
seq_no: None,
filled_no: "02000488176",
filled_avg_price: 154.5,
filled_qty: 1,
filled_price: 154.5,
filled_time: "09:16:07.419",
user_def: None,
}
df = pd.DataFrame([{'date': obj.date,
'filled_time': obj.filled_time.split('.')[0],
'branch_no': obj.branch_no, 'account': obj.account,
'stock_no': obj.stock_no, 'buy_sell': obj.buy_sell,
'filled_price': obj.filled_price,
'filled_qty': obj.filled_qty
} for obj in filled_orders])
df
# for future
endDate = datetime.today()
startDate = endDate - timedelta(days = 30)
result = sdk.futopt.filled_history(acc[1], FutOptMarketType.Future,
start_date= startDate.strftime("%Y%m%d"),
end_date=endDate.strftime("%Y%m%d"))
filled_orders = result.data
filled_orders[0]
----------------
FutOptFilledData {
date: "2024/11/07",
branch_no: "xxxxx",
account: "xxxxx",
order_no: "s00V3",
symbol: "FITM",
buy_sell: Sell,
order_type: New,
seq_no: None,
user_def: None,
filled_no: "00100004158",
filled_avg_price: 23349.0,
filled_lot: 1,
filled_price: 23349.0,
filled_time: "09:10:22.660",
expiry_date: "202412",
strike_price: None,
call_put: None,
symbol_leg2: None,
expiry_date_leg2: None,
strike_price_leg2: None,
call_put_leg2: None,
buy_sell_leg2: None,
}
---------------------------
# 轉成 dataFrame
df = pd.DataFrame([{'date': obj.date, 'filled_time': obj.filled_time.split('.')[0],
'branch_no': obj.branch_no, 'account': obj.account,
'symbol': obj.symbol, 'buy_sell': obj.buy_sell,
'filled_price': obj.filled_price,
'filled_lot': obj.filled_lot
} for obj in filled_orders])
這是純機械化動作,僅把測試成功的 code 記錄下來。
# 現股買進台積電 (2330) 1 張 (1000 股),注意零股的 market_type 不一樣
order = Order(
buy_sell = BSAction.Buy,
symbol = "2330",
price = "1040",
quantity = 1000,
market_type = MarketType.Common, # MarketType.IntradayOdd (盤中零股)
price_type = PriceType.Limit,
time_in_force= TimeInForce.ROD,
order_type = OrderType.Stock,
user_def = "From Python" # optional field
)
order_res = sdk.stock.place_order(acc[0], order)
order_res
# 賣出微台指 1 口,注意日盤夜盤的 market_type 不一樣
order = FutOptOrder(
buy_sell = BSAction.Sell,
symbol = "TMFL4", # TMF:微台指,L:12月,4:2024年
price = "23749",
lot = 1,
market_type = FutOptMarketType.Future, # FutureNight 夜盤
price_type = FutOptPriceType.Limit,
time_in_force= TimeInForce.ROD,
order_type = FutOptOrderType.New, # FutOptOrderType.Auto,
user_def = "From Python" # optional field
)
sdk.futopt.place_order(accounts.data[1], order)
# 取得當前委託單,需要注意下出去的單的狀態,很重要
# 關鍵欄位是 lot: 下單數量,filled_lot: 成交數量
order_results = sdk.futopt.get_order_results(acc[1])
print(order_results.data[0])
-------------------------
FutOptOrderResult {
function_type: None,
date: "2024/11/13",
seq_no: "00110749918",
branch_no: "xxxxx",
account: "xxxxx",
order_no: "s00CX",
asset_type: 1,
market: "TAIMEX",
market_type: Future,
symbol: "FITM",
unit: None,
currency: None,
expiry_date: "202412",
strike_price: None,
call_put: None,
buy_sell: Buy,
symbol_leg2: None,
expiry_date_leg2: None,
strike_price_leg2: None,
call_put_leg2: None,
buy_sell_leg2: None,
price_type: Limit,
price: 22701,
after_price: 22851,
lot: 1,
after_lot: 1,
time_in_force: ROD,
order_type: Close,
status: 50,
is_pre_order: false,
after_price_type: None,
filled_lot: 1,
filled_money: 22851,
before_lot: None,
before_price: None,
user_def: None,
last_time: "13:32:16.320",
details: None,
error_message: None,
}
# 取得當前委託單,股票部分
orderResults = sdk.stock.get_order_results(acc[0])
print(orderResults.data[0])
---------------------------
OrderResult {
function_type: None,
date: "2024/11/13",
seq_no: "02079448748",
branch_no: "xxxxx",
account: "xxxxx",
order_no: "x1246",
asset_type: 0,
market: "TAIEX",
market_type: IntradayOdd,
stock_no: "3008",
buy_sell: Sell,
price_type: Limit,
price: 2440,
quantity: 5,
time_in_force: ROD,
order_type: Stock,
is_pre_order: false,
status: 10,
after_price_type: None,
after_price: 2440,
unit: 1,
after_qty: 5,
filled_qty: 0,
filled_money: 0,
before_qty: None,
before_price: None,
user_def: None,
last_time: "09:06:19.373",
details: None,
error_message: None,
}
# get symbols,取得所有股票代號
reststock = sdk.marketdata.rest_client.stock
tickers = reststock.intraday.tickers(type='EQUITY', exchange="TWSE", isNormal=True)
tickers['data'][0]
------------------
{'symbol': '0051', 'name': '元大中型100'}
---------------------------------------
# 抓出特定股票,當時交易資訊
# get ticker
ticker = reststock.intraday.ticker(symbol='2330')
ticker
------
{'date': '2024-11-08',
'type': 'EQUITY',
'exchange': 'TWSE',
'market': 'TSE',
'symbol': '2330',
'name': '台積電',
'industry': '24',
'securityType': '01',
'previousClose': 1065,
'referencePrice': 1065,
'limitUpPrice': 1170,
'limitDownPrice': 959,
'canDayTrade': True,
'canBuyDayTrade': True,
'canBelowFlatMarginShortSell': True,
'canBelowFlatSBLShortSell': True,
'isAttention': False,
'isDisposition': False,
'isUnusuallyRecommended': False,
'isSpecificAbnormally': False,
'matchingInterval': 0,
'securityStatus': 'NORMAL',
'boardLot': 1000,
'tradingCurrency': 'TWD'}
----------------------------
# 取得零股交易資訊
tickerOdd = reststock.intraday.ticker(symbol='2330', type='oddlot')
tickerOdd
---------
返回結果類似
# 抓出特定股票的即時報價,包含最佳五擋,發展短線策略,這很重要
# get quote
reststock = sdk.marketdata.rest_client.stock
quote = reststock.intraday.quote(symbol='2330')
quote
-----
{'date': '2024-11-08',
'type': 'EQUITY',
'exchange': 'TWSE',
'market': 'TSE',
'symbol': '2330',
'name': '台積電',
'referencePrice': 1065,
'previousClose': 1065,
'openPrice': 1085,
'openTime': 1731027602469930,
'highPrice': 1090,
'highTime': 1731027723904904,
'lowPrice': 1080,
'lowTime': 1731027603202007,
'closePrice': 1090,
'closeTime': 1731043800000000,
'avgPrice': 1087.1,
'change': 25,
'changePercent': 2.35,
'amplitude': 0.94,
'lastPrice': 1090,
'lastSize': 5142,
'bids': [{'price': 1085, 'size': 1726},
{'price': 1080, 'size': 2392},
{'price': 1075, 'size': 1011},
{'price': 1070, 'size': 1238},
{'price': 1065, 'size': 1394}],
'asks': [{'price': 1090, 'size': 2644},
{'price': 1095, 'size': 5440},
{'price': 1100, 'size': 8099},
{'price': 1105, 'size': 2027},
{'price': 1110, 'size': 1718}],
'total': {'tradeValue': 39618140000,
'tradeVolume': 36444,
'tradeVolumeAtBid': 10301,
'tradeVolumeAtAsk': 22795,
'transaction': 10704,
'time': 1731043800000000},
'lastTrade': {'bid': 1085,
'ask': 1090,
'price': 1090,
'size': 5142,
'time': 1731043800000000,
'serial': 10576591},
'lastTrial': {'bid': 1085,
'ask': 1090,
'price': 1090,
'size': 5142,
'time': 1731043798531691,
'serial': 10576210},
'isClose': True,
'serial': 10576591,
'lastUpdated': 1731043800000000}
---------------------------------
# 取得零股報價
quoteOdd = reststock.intraday.quote(symbol='2330', type='oddlot')
quoteOdd
--------
返回結果類似
# 取得日內 k 線,以 5 分線為例
candles = reststock.intraday.candles(symbol='2330', timeframe=5)
candles
-------
{'date': '2024-11-08',
'type': 'EQUITY',
'exchange': 'TWSE',
'market': 'TSE',
'symbol': '2330',
'timeframe': '5',
'data': [{'date': '2024-11-08T09:00:00.000+08:00',
'open': 1085,
'high': 1090,
'low': 1080,
'close': 1085,
'volume': 5915,
'average': 1084.92},
{'date': '2024-11-08T09:05:00.000+08:00',
'open': 1085,
'high': 1090,
'low': 1080,
'close': 1080,
'volume': 1167,
'average': 1084.89},
{'date': '2024-11-08T09:10:00.000+08:00',
'open': 1085,
'high': 1085,
'low': 1080,
'close': 1085,
...
'high': 1090,
'low': 1090,
'close': 1090,
'volume': 5142,
'average': 1087.1}]}
# 取得全市場快照,可方便即時條件選股
snapshotTse = reststock.snapshot.quotes(market='TSE') # 若抓上櫃改 OTC
snapshotTse['data']
-------------------
[{'type': 'EQUITY',
'symbol': '0050',
'name': '元大台灣50',
'openPrice': 199.4,
'highPrice': 200,
'lowPrice': 198.7,
'closePrice': 199,
'change': 1.55,
'changePercent': 0.79,
'tradeVolume': 12774,
'tradeValue': 2546347300,
'lastPrice': 199,
'lastUpdated': 1731043800000000},
{'type': 'EQUITY',
'symbol': '0051',
'name': '元大中型100',
'openPrice': 83.2,
'highPrice': 83.2,
'lowPrice': 82.1,
'closePrice': 82.15,
'change': -0.1,
'changePercent': -0.12,
'tradeVolume': 84,
'tradeValue': 6931250,
'lastPrice': 82.15,
...
'tradeVolume': 2496,
'tradeValue': 40514050,
'lastPrice': 16.15,
'lastUpdated': 1731043800000000},
...]
# 抓即時成交值排行
activeStocks = reststock.snapshot.actives(market='TSE', trade='value')
activeStocks
------------
# 另外還有抓歷史日 k 線的,抓法都類似
candles = (reststock.historical.candles(**{"symbol": "2330", "from": "2024-01-01", "to": "2024-11-07"}))
candles
-------
# get quote
# TMF 微台指,MXF 小台指,TXF 大台指;
# 月碼:ABCD...L 分別代表 1~12 月
# 年碼:4 代表 2024
restfut = sdk.marketdata.rest_client.futopt
quote = restfut.intraday.quote(symbol='TMFL4', session='afterhours')
quote
-----
{'date': '2024-11-11',
'type': 'FUTURE_AH',
'exchange': 'TAIFEX',
'symbol': 'TMFL4', # 注意代號年月編碼
'name': '微型臺指期貨124',
'previousClose': 23745,
'openPrice': 23770,
'openTime': 1731049200021000,
'highPrice': 23848,
'highTime': 1731052943206000,
'lowPrice': 23581,
'lowTime': 1731068607062000,
'closePrice': 23591,
'closeTime': 1731077935773000,
'avgPrice': 23696.78,
'change': -194,
'changePercent': -0.82,
'amplitude': 1.12,
'lastPrice': 23591,
'lastSize': 1,
'total': {'tradeVolume': 3639,
'totalBidMatch': 3249,
'totalAskMatch': 2976,
'time': 1731077935773000},
'lastTrade': {'bid': 23591,
'ask': 23596,
'price': 23591,
'size': 1,
'time': 1731077935773000,
'serial': '00128484'},
'serial': 2349082,
'lastUpdated': 1731077936679000}
--------------------------------
df = pd.DataFrame([{'symbol': obj['symbol'],
'name': obj['name'], 'date': obj['date'],
'closePrice': obj['closePrice'],
'lastPrice': obj['lastPrice'],
'closeTime': datetime.fromtimestamp(obj['closeTime']/1000000)}
for obj in [quote]])
df
--
symbol name date closePrice lastPrice closeTime
0 TMFL4 微型臺指期貨124 2024-11-11 23591 23591 2024-11-08 22:58:55.773
以上已經演練完成幾乎所有官方提供的功能,整體體驗還不錯,僅這點比較可惜:
雖然學了一堆技術,但身為一個自營者,沒有一個客戶或老闆可為我的投入埋單,惟有搭配正期望值的策略,去市場撈這一途了,真是艱難的任務。無論如何,多了一項武器裝備,就提升勝利的機會。
我將製作 python gui 介面,從自己喜愛的操作模式出發,逐步刻出自己的看盤和下單介面。以往的開發偏重在 web 架構,如「精明管家-風險部位管理系統」,原因是為了做成後可以跟多人分享,或者可以試著銷售軟體,讓家人朋友感覺我有比較「正常的工作」,但一直乏人問津,也就不強求了。因此若從自己的需求出發,就暫時不採用 web 這種比較複雜的架構開發,直接用本機 app gui 應該是最方便的,稍微查一下有一堆工具,又可以來玩新的玩具了,後續再來分享。
Newman 2024/11/9
導覽頁:紐曼的技術筆記-索引