用永豐 API 抓取台股即時盤中內外盤成交資訊
BY PJ.
-2025 年 5 月 2 日
(最後更新於: 2025 年 5 月 10 日)
繼上次透過永豐證券 API 找出短線交易潛力股,我們獲得了一組股票清單。接下來,我們將介紹如何使用永豐 API 取得盤中即時交易資訊
,並進一步分析內外盤成交
來識別即時的買賣訊號。
內外盤成交分析
- 外盤成交:代表買方提高買價,成交在較高價位,顯示出買方較強。
- 內盤成交:代表賣方降低賣價,成交在較低價位,顯示出賣方較強。
透過這些資訊,我們可以判斷股票的買賣盤力道,從而預測未來的價格走勢。
永豐(Shioaji)即時行情API說明
- 登入永豐API
import shioaji as sj
api = sj.Shioaji(simulation=True) # 使用模擬環境
api.login(
api_key="your_api_key",
secret_key="your_secret_key",
receive_window=60000
)
- 訂閱股票
訂閱股票後,當盤中有成交訊息時,永豐將自動回傳資料。我們不需要透過 requests 查詢,只需接收並擷取回傳的資料即可。
訂閱數量限制:每個帳號最多可訂閱200檔股票,並且根據交易金額的不同,流量限制會有所不同。
近 30 日使用 API 成交金額 | 每日流量限制 |
---|---|
0 | 500MB |
1 - 1億 | 2GB |
>1億 | 10GB |
詳細流量限制參考:永豐API限制說明
- 訂閱 tick 資訊與 bidask 資訊
你可以選擇訂閱 tick 或 bidask 來獲取不同的市場資訊:
- Tick:
即時成交量與價格資訊。 - Bidask:
委買賣五檔價格資訊。
以訂閱股票 2330 為例:
api.quote.subscribe(
api.Contracts.Stocks["2330"],
quote_type = sj.constant.QuoteType.Tick,
version = sj.constant.QuoteVersion.v1
)
* 使用 callback 函數擷取回傳資訊
當永豐傳回資料後,我們可以使用 callback 函數來擷取資料並進行後續處理。
from shioaji import TickSTKv1, Exchange
def quote_callback(exchange: Exchange, tick: TickSTKv1):
print(f"Exchange: {exchange}, Tick: {tick}")
api.quote.set_on_tick_stk_v1_callback(quote_callback)
這段程式碼會把回傳的交易資料打印出來,接下來我們將演示如何將這些資訊儲存至資料庫,便於後續分析與應用。
我們將盤前分析的潛力股票清單 (stock.xlsx),透過永豐 API 訂閱並將回傳資訊儲存至 SQLite 資料庫,方便後續進行即時下單應用。
在stock.xlsx同層資料夾,我們分別建立四個 Python 檔案:main.py、tick.py、sql.py、type.py。
main.py - 永豐 API 登入與資料處理
import shioaji as sj
import tick
class RunMachine:
def __init__(self):
# 登入永豐模擬環境
self.api = sj.Shioaji(simulation=True)
self.api.login(
api_key="your_api_key",
secret_key="your_secret_key",
receive_window=60000
)
# 執行 tick 串接 API 程式
tick_func = tick.update_tick_df(self.api)
tick_func.save_loop() #不定數迴圈執行程式
RunMachine()
tick.py - 訂閱股票與回傳資料儲存
import shioaji as sj
import time , datetime
import pandas as pd
import time
from sql import use_sql
from type import chg_type
from shioaji import BidAskSTKv1, Exchange, TickSTKv1
class update_tick_df:
def __init__(self,api):
self.api = api
self.df = pd.read_excel('stock.xlsx',dtype=object)
self.df.index = self.df.name
self.df = self.df.drop('name',axis=1)
#使用sqlite
self.ldb = use_sql()
self.ldb.delete_table() #清空原資料
#訂閱股票
self.subscribe_stock(self.df.symbol)
self.add_column()
self.tick()
# self.bidask()
#訂閱
def subscribe_stock(self,stock_list):
for symbol in stock_list:
self.api.quote.subscribe(
self.api.Contracts.Stocks[symbol],
quote_type = sj.constant.QuoteType.Tick,
version = sj.constant.QuoteVersion.v1)
print('subscribing tick to %s' % symbol)
self.api.quote.subscribe(
self.api.Contracts.Stocks[symbol],
quote_type = sj.constant.QuoteType.BidAsk,
version = sj.constant.QuoteVersion.v1)
print('subscribing bidask to %s' % symbol)
#取消訂閱
def add_column(self):
self.df['high_price'] = 0.0
self.df['low_price'] = 9999.0
self.df['price'] = 0.0
self.df['bid_side_total_vol'] =\
self.df['ask_side_total_vol'] = 0
self.df['tick_time'] = ""
self.df['bid_continuous_times'] = self.df['ask_continuous_times'] = self.df['bid_continuous_volumn'] = self.df['ask_continuous_volumn'] = 0
self.df['origin_ask_times'] = self.df['origin_bid_times'] = 0
self.df['origin_ask_volumn'] = self.df['origin_bid_volumn'] = 0
def tick(self):
@self.api.on_tick_stk_v1()
def quote_callback(exchange: Exchange, tick:TickSTKv1):
try:
self.df.loc[self.df.symbol == tick.code,'bid_side_total_vol']=tick.bid_side_total_vol
self.df.loc[self.df.symbol == tick.code,'ask_side_total_vol']=tick.ask_side_total_vol
self.df.loc[self.df.symbol == tick.code,'price']=tick.close
self.df.loc[self.df.symbol == tick.code,'tick_time']=tick.datetime.strftime('%Y-%m-%d %H:%M:%S')
if tick.tick_type == 1:
if (self.df.loc[self.df.symbol == tick.code,'ask_continuous_times'] != 0).any():
self.df.loc[self.df.symbol == tick.code,'origin_ask_times'] = self.df.loc[self.df.symbol == tick.code,'ask_continuous_times']
self.df.loc[self.df.symbol == tick.code,'origin_ask_volumn'] = self.df.loc[self.df.symbol == tick.code,'ask_continuous_volumn']
self.df.loc[self.df.symbol == tick.code,'origin_bid_times'] = 0
self.df.loc[self.df.symbol == tick.code,'origin_bid_volumn'] = 0
# 原內盤10次以上,出現外盤連續2次~5次 > 賣方竭點
self.df.loc[self.df.symbol == tick.code,'bid_continuous_times'] += 1
self.df.loc[self.df.symbol == tick.code,'ask_continuous_times'] = 0
self.df.loc[self.df.symbol == tick.code,'bid_continuous_volumn'] += tick.volume
self.df.loc[self.df.symbol == tick.code,'ask_continuous_volumn'] = 0
elif tick.tick_type == 2:
if (self.df.loc[self.df.symbol == tick.code,'bid_continuous_times'] != 0).any():
self.df.loc[self.df.symbol == tick.code,'origin_bid_times'] = self.df.loc[self.df.symbol == tick.code,'bid_continuous_times']
self.df.loc[self.df.symbol == tick.code,'origin_bid_volumn'] = self.df.loc[self.df.symbol == tick.code,'bid_continuous_volumn']
self.df.loc[self.df.symbol == tick.code,'origin_ask_times'] = 0
self.df.loc[self.df.symbol == tick.code,'origin_ask_volumn'] = 0
# 原外盤10次以上,出現內盤連續2次~5次 > 買方竭點
self.df.loc[self.df.symbol == tick.code,'ask_continuous_times'] += 1
self.df.loc[self.df.symbol == tick.code,'bid_continuous_times'] = 0
self.df.loc[self.df.symbol == tick.code,'ask_continuous_volumn'] += tick.volume
self.df.loc[self.df.symbol == tick.code,'bid_continuous_volumn'] = 0
if (self.df.loc[self.df.symbol == tick.code,'high_price'] < tick.close).any():
self.df.loc[self.df.symbol == tick.code,'high_price'] = tick.close
if (self.df.loc[self.df.symbol == tick.code,'low_price'] > tick.close).any():
self.df.loc[self.df.symbol == tick.code,'low_price'] = tick.close
except:
print('tick data Error symbol: %s\n' % tick.code)
time.sleep(3)
def save_df(self):
#讀取excel調整dtype格式
df = chg_type(self.df)
#try:
if len(df) > 0:
self.ldb.update_table(df)
else:
print("tick None")
def save_loop(self):
while True:
if datetime.datetime.now().strftime("%H:%M:%S") <= '13:30:00'\
and datetime.datetime.now().strftime("%H:%M:%S") >= '09:00:00':
time.sleep(0.5)
self.save_df()
else:
break
sql.py - 資料庫操作
import sqlite3
from pandas.io import sql
import pandas as pd
import time
class use_sql:
def __init__(self):
self.con = sqlite3.connect('stock_database.db', check_same_thread=False)
self.cursorObj = self.con.cursor()
self.cursorObj.execute("""create table if not exists stock_data(name varchar, symbol varchar,
avg_volume int, high float,low float, open float, close float, price_change float,
high_price float,low_price float , price float,
bid_side_total_vol int, ask_side_total_vol int,
tick_time varchar,
bid_continuous_times int, ask_continuous_times int, bid_continuous_volumn int, ask_continuous_volumn int,
origin_ask_times int , origin_bid_times int,
origin_ask_volumn int , origin_bid_volumn int)
""")
self.con.commit()
def update_table(self, df):
self.cursorObj.execute("DELETE FROM stock_data")
self.con.commit()
sql.to_sql(df, name='stock_data', con = self.con,if_exists='append')
def delete_all(self):
self.cursorObj.execute("delete from stock_data")
self.con.commit()
type.py - 資料型態轉換
def chg_type(df):
def convert_decimal_to_float(value):
return float(value)
df = df.applymap(str)
df.symbol = df.symbol.astype('str')
df.avg_volume = df.avg_volume.apply(convert_decimal_to_float)
df.high = df.high.apply(convert_decimal_to_float)
df.low = df.low.apply(convert_decimal_to_float)
df.open = df.open.apply(convert_decimal_to_float)
df.close = df.close.apply(convert_decimal_to_float)
df.price_change = df.price_change.apply(convert_decimal_to_float)
df.high_price = df.high_price.apply(convert_decimal_to_float)
df.low_price = df.low_price.apply(convert_decimal_to_float)
df.price = df.price.apply(convert_decimal_to_float)
df.bid_side_total_vol = df.bid_side_total_vol.astype('int')
df.ask_side_total_vol = df.ask_side_total_vol.astype('int')
df.tick_time = df.tick_time.astype('str')
df.bid_continuous_times = df.bid_continuous_times.astype('int')
df.ask_continuous_times = df.ask_continuous_times.astype('int')
df.bid_continuous_volumn = df.bid_continuous_volumn.astype('int')
df.ask_continuous_volumn = df.ask_continuous_volumn.astype('int')
df.origin_ask_times = df.origin_ask_times.astype('int')
df.origin_bid_times = df.origin_bid_times.astype('int')
df.origin_ask_volumn = df.origin_ask_volumn.astype('int')
df.origin_bid_volumn = df.origin_bid_volumn.astype('int')
return df
此檔案負責將 DataFrame 中的各欄位資料轉換為適合存儲至資料庫的資料型態。在處理股市資料時,某些欄位需要轉換為浮點數或整數,以確保資料庫中存儲的資料能夠正確地進行查詢和計算。
產生結果
- 註:sqlite檔案可以用db browser for sqlite開啟
使用資料庫分析與判斷買賣訊號
寫入的table: stock_data
,我將擷取的資料做了一些運算,可以用來查看各股票買賣盤力道:
欄位名稱 | 說明 |
---|---|
bid_continuous_times | 連續外盤成交量(若出現內盤成交則從 0 開啟計算) |
ask_continuous_times | 連續內盤成交量(若出現外盤成交則從 0 開啟計算) |
bid_continuous_volumn | 連續外盤成交筆數(若出現內盤成交則從 0 開啟計算) |
ask_continuous_volumn | 連續內盤成交筆數(若出現外盤成交則從 0 開啟計算) |
origin_ask_volumn | 外盤成交之前,連續內盤成交量 |
origin_bid_volumn | 內盤成交之前,連續外盤成交量 |
origin_ask_times | 外盤成交之前,連續內盤成交筆數 |
origin_bid_times | 內盤成交之前,連續外盤成交筆數 |
bid_side_total_vol | 外盤成交總量 |
ask_side_total_vol | 內盤成交總量 |
high_price | 程式開始執行後的最高股價 |
low_price | 程式開始執行後的最低股價 |
這些資料可用來判斷股市的即時買賣訊號。舉例來說,當股價接近今日低點並且出現大量買盤,可能是反彈的訊號,這部分就留給大家自行運用。
結語
這篇文章介紹了如何使用永豐證券 API,透過盤中即時交易資訊來進行短線交易分析。
我們透過訂閱tick api,在新的成交紀錄回傳即時更新SQLite 資料庫
,用來分析買賣盤力道,從而判斷即時的交易訊號。
若想進一步實現自動下單
,可串接永豐下單function,並透過多執行緒同時更新資料庫並使用;或可考慮串接 Telegram API,實現即時推播功能
,可參考【Telegram API】使用 Python 打造 Telegram Notify 新手教學。
Hi,我是在開盤前先執行歷史資料抓清單
2025-05-13 17:02開盤後用清單的股票代碼,訂閱即時資料,把資料寫入SQLite
再做後續應用
Hi PJ你好,
2025-05-13 16:41想請問你的SQLite 資料庫內容是如何規劃的,如何將歷史資料和目前及時跑的資料互相結合呢?
謝謝