方格精選

技術筆記-Coding for Myself,python gui 我來了

閱讀時間約 59 分鐘

1.動機源起

開始玩 python gui,首先稍微交代一下背景,這種 client-server 的應用架構,一直以來是我所唾棄的方式,因為:

  1. 每個前端都需要安裝,所以若要部署給多人使用,很麻煩,每個人的電腦環境千奇百怪,難保平台與各種軟體均能相容。所以出問題時原因難查,若軟體是銷售的,也容易有爭議,或是售後服務的成本居高不下。
  2. 前端的運算需求比較大,對於老舊電腦,容易遇到跑不動的窘境。

因此,從業 20 多年以來,總是擁抱 web-based 架構。後來 mobile-first 風潮興起,開始接受手機 app 為另一個重要的前端型態。

但是但是....四年來,雖然脫離了企業環境,依然很累的開發著 web 系統,也學了一些 ios app,搞了好像很屌的架構,像之前分享過「玩轉系統架構」,「Google SignIn」的方法等等,到頭來才發現:使用者,依然只有我一個人!要賣軟體這麼深奧的東西,靠一己之力太難了啦,哈哈哈,真是想太多了。什麼帳號登入,token 傳輸,安全控管機制,一切都是想太多了。既然掌握了「富邦新一代 api」的各種功能,要快速建立應用系統,直接寫在本機執行的 gui app 是最簡單的架構,享受為自己量身訂做的樂趣。

2.一磚一瓦不嫌慢

python gui 從零開始,有眾多 ai 工具陪伴 (chat gpt / cloude / gemini 任你挑),滿滿的幸福感。學習一套新技術的方式已經完全改變,連看書或上課都免了 (為講師業捏把冷汗),只要把想法具體化描述為一個問題, ai 助手是問不倒的,就這樣從一磚一瓦開始建立系統,竟然頗有進展。原來關於 python gui,已經有內建套件 tkinter,連任何額外安裝都不需要:

import tkinter as tk
root = tk.Tk()
root.mainloop()

把這三行指令存成一個 .py 檔,假設是 gui.py,然後在命令列輸入 python gui.py,立刻一個視窗就跑起來了。這其中當然要先準備一個 python 執行環境,在「在 mac 中建立 python 虛擬環境」已經分享過了。

raw-image

一個標準的視窗,中規中矩,非常好。如果採用「傳統的學習方式」,不外乎會開始介紹視窗的各種屬性,參數,和許多設定方法,但且慢,多學而無用就是浪費時間,ai 為此而生,是友非敵,「他」知道就好,有需要再問即可。


有了視窗,下一步就是加一個按鈕,執行一些動作,這牽涉到

  1. 如何加入按鈕
  2. 如何佈局 layout
  3. 如何串聯動作

這都是問一問就有了:

import tkinter as tk

def do_something():
print('hello')

root = tk.Tk()
btn = tk.Button(root, text="做動作", command = do_something)
btn.config(pady=5, padx=5)
btn.pack()

root.mainloop()

以上採用最簡單的 layout,pack,就是按照順序從上而下擺放。按下按鈕,果然在 console 印出 hello 了。

raw-image

再來,下一步,希望把 hello 顯示在視窗,而不是在 console,那就牽涉到第二個 ui 元件,經查可以顯示成為一個 label,也可以顯示在 listbox 中,這兩者是最基本簡單的顯示元件,我選擇擺一個 listbox。

import tkinter as tk

def do_something():
lb.insert(0, 'hello')

root = tk.Tk()

btn = tk.Button(root, text="做動作", command = do_something)
btn.config(pady=5, padx=5)
btn.pack()

lb = tk.Listbox(root)
lb.pack(expand=True, fill="both", padx=5, pady=5)

root.mainloop()

按鈕結果如下:

raw-image

然後就要接進一些真實資料了,從富邦 api 取得的資料,先顯示一項比較關心的資料「交易紀錄」,取得方法已經紀錄在上一篇文章了,把它整理成一個 class nTrade,實作兩個方法 get_filled_hist_stock(),get_filled_hist_futopt():

from datetime import datetime, timedelta
import os
import numpy as np
from fubon_neo.sdk import FubonSDK
from dotenv import load_dotenv
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

class nTrade:
def __init__(self):
self.init_sdk()

def init_sdk(self):
load_dotenv()
id = os.getenv('id')
pwd = os.getenv('pwd')
self.sdk = FubonSDK()
dir = os.path.dirname(os.path.abspath(__file__)) # get abs folder location of execution file
accounts = self.sdk.login(id, pwd, f"{dir}/{id}.pfx" , f"{pwd}")
self.acc = accounts.data
self.get_symbol_name()
print(self.acc)

def get_filled_hist_stock(self, symbol=''):
endDate = datetime.today()
startDate = endDate - timedelta(days = 30)
result = self.sdk.stock.filled_history(self.acc[0],
start_date= startDate.strftime("%Y%m%d"),
end_date=endDate.strftime("%Y%m%d"))
if len(result.data)>0:
filled_orders = result.data
if symbol=='':
return filled_orders
else:
return [obj for obj in filled_orders if obj.symbol==symbol]
else:
return []

def get_filled_hist_futopt(self, symbol=''):
endDate = datetime.today()
startDate = endDate - timedelta(days = 30)
result = self.sdk.futopt.filled_history(self.acc[1], FutOptMarketType.Future,
start_date= startDate.strftime("%Y%m%d"),
end_date=endDate.strftime("%Y%m%d"))
result_night = self.sdk.futopt.filled_history(self.acc[1], FutOptMarketType.FutureNight,
start_date= startDate.strftime("%Y%m%d"),
end_date=endDate.strftime("%Y%m%d"))
filled_hist = [*result.data, *result_night.data]
filled_hist = sorted(filled_hist, key=lambda obj: f"{obj.date} {obj.filled_time}")
if len(filled_hist)>0:
if symbol=='':
return filled_hist
else:
return [obj for obj in filled_hist if obj.symbol==symbol]
else:
return []

在 gui.py 中引用 nTrade,並在程式流程開頭建立實體 nt,就可以透過它呼叫相關 method:

from nTrade import  nTrade
nt = nTrade()

在取得資料之前,先把 ui 元件準備好,這次挑戰比較複雜的顯示方式,就是「表格式」的,不同於 listbox 一行只能顯示一串文字,在 tkinter 中有能力顯示表格式資料的就是 Treeview,以下程式碼建立一個 Treeview,配置四個欄位:

btn = tk.Button(root, text="更新交易紀錄", command = refresh_trade_hist)
btn.config(pady=5)
btn.pack()

columns = ('datetime', 'symbol', 'qty', 'price')
tree_trades = ttk.Treeview(root, columns=columns, show='headings', padding=5)
tree_trades.heading('datetime', text = '日期時間')
tree_trades.heading('symbol', text = '標的/買賣')
tree_trades.heading('qty', text = '數量')
tree_trades.heading('price', text = '價格')

tree_trades.column('datetime', anchor=tk.W, width=150)
tree_trades.column('symbol', anchor=tk.E, width=100)
tree_trades.column('qty', anchor=tk.E, width=40)
tree_trades.column('price', anchor=tk.E, width=80)
tree_trades.pack(expand=True, fill="both")

然後去實作 refresh_trade_hist,呼叫 nTrade 的實體 nt 作事:

def refresh_trade_hist():
filled_order_stock = nt.get_filled_hist_stock('')
filled_order_futopt = nt.get_filled_hist_futopt('')
for item in tree_trades.get_children():
tree_trades.delete(item)
for obj in filled_order_stock:
bs = f'{obj.buy_sell}'
bs = bs.split('.')[1]
text1 = f'{obj.date} {obj.filled_time.split('.')[0]}'
text2 = f'{obj.stock_no}({nt.symbols.get(obj.stock_no, obj.stock_no)}),{bs}'
text3 = f'{obj.filled_qty}'
text4 = f'{obj.filled_price}'
tree_trades.insert('', 0, values = (text1, text2, text3, text4))
for obj in filled_order_futopt:
bs = f'{obj.buy_sell}'
bs = bs.split('.')[1]
filled_time_hour = int(obj.filled_time.split(':')[0])
if filled_time_hour < 5:
dd = datetime.strptime(obj.date, '%Y/%m/%d') + timedelta(days=1)
else:
dd = datetime.strptime(obj.date, '%Y/%m/%d')
text1 = f'{dd.strftime('%Y/%m/%d')} {obj.filled_time.split('.')[0]}'
text2 = f'{obj.symbol},{bs}'
text3 = f'{obj.filled_lot}'
text4 = f'{obj.filled_price}'
tree_trades.insert('', 0, values = (text1, text2, text3, text4))
raw-image

成功顯示表格形式的資料。另外一些重要的帳戶資料,如庫存,委託訂單狀態,都可以依樣葫蘆製作完成。

當要顯示的資料越來越多,layout 就無法這麼單純了,因此引進 grid layout,先把主視窗分成左右兩邊,也就是一個 row 兩個 column,然後把上述所有 ui 元件的父元件,全部改為 frame_left。

# 在 root 視窗上面疊一個 frame,在其上再疊上 frame_left, frame_right
main_frame = tk.Frame(root)
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=2)
main_frame.rowconfigure(0, weight=1)
frame_left = tk.Frame(main_frame, bg='#d6eaf8', padx=5, pady=5 )
frame_left.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
frame_right = tk.Frame(main_frame, bg='#fad7a0', padx=5, pady=5)
frame_right.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))

另外隨著 ui 元件越放越多,程式變得有臭又長,所有變數都成為 global variable,發展下去肯定會副作用失控,因此較好的做法是把主視窗,包成一個 class,只把有必要的元件放出能見度,減少出錯的機會。

class AppInterface:
def __init__(self, root):
# 處理 layout
# 放置 ui 元件
# 只把需要跟資料互動的 ui,掛在 self,也就是成為此類別的屬性

def refresh_trade_hist(self):
xxxx
xxxx

def get_order_results(self):
xxxx
xxxx

# 程式進入點,變成短短幾行
if __name__ == "__main__":
nt = nTrade()
root = tk.Tk()
app = AppInterface(root)
root.mainloop()

3.顯示即時行情

以上視窗左邊已經顯示了一些帳戶相關的資料,右邊準備來看些跳動的數字了。基本的 matplotlib 沒有現成的 k 線圖可用,必須很辛苦的畫出每條線段和每個方塊,而這麼複雜的任務,竟然隨便問一問 ai 就得到詳細的程式碼,稍微修一修就能跑了:

# 取得當天日夜盤連在一起的 k 線
def refresh_candles(self):
symbol = self.en.get()
interval = self.en_interval.get()
candles = nt.get_candles(symbol, False, interval)
candles_afterhour = nt.get_candles(symbol, True, interval)
candles.extend(candles_afterhour)
candles = sorted(candles, key=lambda obj: obj['date'])

self.lb_candles.delete(0, tk.END)
for obj in candles:
date_string = obj['date'].split('T')[1][0:5]
text = f'{date_string}, {obj['open']}, {obj['high']}, {obj['low']}, {obj['close']}, {obj['volume']}'
self.lb_candles.insert(0, text)

self.draw_chart(candles)

def draw_chart(self, candles):
self.fig = Figure(figsize=(6,4))
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_chart)
self.canvas.get_tk_widget().grid(row=0, column=0)
self.ax.clear()
colors = ['#e74c3c' if obj['close'] >= obj['open'] else '#17a589' for obj in candles]

for i in range(len(candles)):
self.ax.plot([i,i], [candles[i]['low'], candles[i]['high']],
color='#85929e', linewidth=1, zorder=1)
bar_bottom = candles[i]['open'] if candles[i]['close'] >= candles[i]['open'] else candles[i]['close']
self.ax.bar(i, abs(candles[i]['close'] - candles[i]['open']), # bar body height
bottom = bar_bottom, color=colors[i],
width=0.8, zorder=2 )
self.ax.set_xlabel('Time')
self.ax.set_ylabel('Price')
interval = self.en_interval.get()
self.ax.set_title(f'Realtime {interval} min K')
self.ax.set_xticks(range(0, len(candles), 1))
self.ax.set_xticklabels([obj['date'].split('T')[1][0:5] for obj in candles])
self.ax.grid(True, linestyle='--', alpha=0.3, zorder=0)
self.fig.tight_layout()
self.canvas.draw()
raw-image

我跟我的 ai 助理抱怨,程式碼太長了,且圖好像很陽春!他就介紹另一個好用的套件 mplfinance,程式碼更精簡,繪圖效果也更好,也可很容易的加入成交量:

import mplfinance as mpf
def draw_chart_mpl(self, candles):
if len(candles) == 0:
return

self.df_candles = pd.DataFrame(
candles,
index = [datetime.strptime(obj['date'], '%Y-%m-%dT%H:%M:%S.%f%z') for obj in candles],
columns=['open', 'high', 'low', 'close', 'volume']
)

addplots = [mpf.make_addplot(self.df_candles['volume'],
panel=1,
type='bar',
width=0.7,
alpha=1.0, color='#5499c7',
#color=self.get_volume_colors()
)]

self.fig, self.ax = mpf.plot(self.df_candles, type='candle', # style='charles',
style = mpf.make_mpf_style(
marketcolors=mpf.make_marketcolors(
up='#e74c3c',
down='#17a589',
edge='inherit',
wick='inherit'
),
gridstyle=":",
gridcolor='#e6e6e6',
),
figsize=(6, 4), tight_layout=True,
volume=False, returnfig=True,
volume_panel=1, panel_ratios=(3,1),
addplot = addplots,
)
interval = self.en_interval.get()
self.ax[0].text(0.5, 0.98, f'Realtime {interval}-min K',
horizontalalignment='center',
verticalalignment='top',
transform=self.ax[0].transAxes)

self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_chart)
self.canvas.get_tk_widget().grid(row=0, column=0, sticky='nsew', pady=5, padx=5)

self.frame_chart.update_idletasks()
self.canvas.draw()
raw-image

4.加入一些變化式

a.標示成交紀錄

在 k 線上標出成交紀錄,這是我在 mt5 和 binance 平台非常激賞的功能,可以一目瞭然檢討以往的操作,一陣暢快的交易之後,留下密密麻麻的軌跡,看起來很爽:

raw-image

可惜我開戶的國內幾家券商軟體,都沒有提供,所以就試著來做做看。... 沒錯,
問一問就有了,哈哈:

# prepare data for trade hist scatter plot
# 這段程式插入 draw_chart_mpl 的 addplots 前面
filled_order_futopt = nt.get_filled_hist_futopt('')
buy_scatter = pd.Series(index = self.df_candles.index, dtype=float)
sell_scatter = pd.Series(index = self.df_candles.index, dtype=float)
for row in filled_order_futopt:
ss = f'{row.date} {row.filled_time.split('.')[0]}'
filled_time = datetime.strptime(ss, '%Y/%m/%d %H:%M:%S')
filled_time_hour = int(row.filled_time.split(':')[0])
if filled_time_hour < 5: # in case of the early morning
filled_time = filled_time + timedelta(days=1)
filled_price = row.filled_price
buy_sell = (f"{row.buy_sell}").split('.')[1]

candle_index = self.find_index(self.df_candles, filled_time)
if buy_sell == 'Buy' and candle_index!=None:
buy_scatter.loc[candle_index] = filled_price
elif buy_sell == 'Sell' and candle_index!=None:
sell_scatter.loc[candle_index] = filled_price

# 這段程式插入 draw_chart_mpl 的 addplots 後面
# for buy hist
if (buy_scatter.count()>0):
addplots.append(mpf.make_addplot(buy_scatter,
type='scatter',
marker='^', # up arrow
markersize=60,
color='#0000FF',
panel=0))
# for sell hist
if sell_scatter.count()>0:
addplots.append(mpf.make_addplot(sell_scatter,
type='scatter',
marker='v', # down arrow
markersize=60,
color='#0000FF',
panel=0))
raw-image

顯示效果,差強人意。 比較可惜的是,富邦 api 只能查兩天的交易紀錄,這已經抱怨過了,也已經反應給公司了。

b.標示最新價格,畫出委託單

# 這一段插入 draw_chart 的 df_candles 產生之後
last_price = self.df_candles['close'].iloc[-1]

# 這一段插入 draw_chart_mpl 的 addplots 後面
addplots.append(mpf.make_addplot(pd.Series([last_price] * len(self.df_candles),
index=self.df_candles.index),
panel=0,
color='#ccd1d1',
linestyle='-',
width=1, alpha=0.8))

# 這一段插入 draw_chart_mpl 的 addplots 後面
# for active orders
order_results_futopt = nt.get_order_result_futopt()
price_min = self.df_candles['low'].min()
price_max = self.df_candles['high'].max()
market_type_count = len(np.unique([f'{obj.market_type}' for obj in order_results_futopt]))
for obj in order_results_futopt:
bs = f'{obj.buy_sell}'
bs = bs.split('.')[1]
market_type = f'{obj.market_type}'
market_type = market_type.split('.')[1]
active_lot = obj.lot-obj.filled_lot
if market_type_count > 1 and market_type == 'Future': # skip day market when night market open
continue
if active_lot > 0:
if obj.after_price > price_max:
price_max = obj.after_price
if obj.after_price < price_min:
price_min = obj.after_price
addplots.append(mpf.make_addplot(pd.Series([obj.after_price] * len(self.df_candles),
index=self.df_candles.index),
panel=0,
color='lightgreen' if bs=='Buy' else 'pink',
linestyle='-',
width=1, alpha=0.8))
price_margin = (price_max - price_min) * 0.1

# 下面這行參數設定,加入 mpl.plot() 裡面
# 為了正確顯示,預掛賣單高過市價範圍,或預掛買單低過市價範圍
ylim = (price_min - price_margin, price_max + price_margin)
raw-image

c.圖表需要一些互動性

以上圖表雖然已經裝了很多資訊,但若想看較精確的數字就無法了,tooltip 是必要的, ai 同樣沒有讓我失望,滑鼠滑過,成功顯示 tooltip。

# 在 draw_chart_mpl 的最後,加入事件處理機制
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)

# 事件處理函數
def on_mouse_move(self, event):
"""Handle mouse movement for tooltip"""
if event.inaxes == self.ax[0]:
# Convert x position to date index
date_idx = int(event.xdata)
x_lim = self.ax[0].get_xlim()
if date_idx <= (x_lim[1] - x_lim[0]) * 0.2: # 如果在左側20%範圍內
self.tooltip.xyann = (60, 20) # 將tooltip移到右上方
else:
self.tooltip.xyann = (-60, 20) # 保持在左上方

if 0 <= date_idx < len(self.df_candles):
# Get data for the current position
row = self.df_candles.iloc[date_idx]
day = self.df_candles.index[date_idx].strftime('%m-%d')
hm = self.df_candles.index[date_idx].strftime('%H:%M')

# Create tooltip text
tooltip_text = f'date: {day}\ntime:{hm}\n'
tooltip_text += f'open: {row["open"]:.0f}\n'
tooltip_text += f'high: {row["high"]:.0f}\n'
tooltip_text += f'low: {row["low"]:.0f}\n'
tooltip_text += f'close: {row["close"]:.0f}\n'
tooltip_text += f'volume: {row["volume"]:,.0f}'

# Update tooltip
self.tooltip.set_text(tooltip_text)
self.tooltip.xy = (event.xdata, event.ydata)
self.tooltip.set_visible(True)
self.canvas.draw_idle()
else:
self.tooltip.set_visible(False)
self.canvas.draw_idle()
else:
self.tooltip.set_visible(False)
self.canvas.draw_idle()
raw-image


d.探索選擇權的機會

程式化看盤與下單的優勢,有人說是「戰勝人性」,依我看這是鬼話。因為人終究會保有亂調參數和「關掉程式」的權限!真正的優勢應該是「快速反應」。這讓我想到我屢戰屢敗的選擇權,想起「瞬間暴衝」的波動率,所產生的超高不合理的權利金肥肉,這用程式抓應該很有戲吧!

用程式去抓選擇權報價,複雜度爆表,到期日有周,月,目標價有的 50 點一格,有的 100 點,ticker 名稱編碼還要自己揣摩,抓到快瘋掉:

raw-image

你看這個 TX420400K4 是什麼鬼,TXO29400X4 ...,編碼原則真的很爛。好不容易理出一些頭緒,先抓最新期貨價格,找正負兩百點價外 ticker,和下兩期到期日等等,終於抓到這幾個數字:

def get_option_quote(self):
quote = nt.get_quote('TMFK4', True) # 202411 微台指
closePrice = quote["closePrice"]
self.lb_left_leg.insert(0, f'TMFK4(202411微台指) 最新報價 {quote["closeTimeAsDatetime"].strftime("%m/%d %H:%M")}: {quote["closePrice"]}')
# 取得正負 200 點的選擇權價位
lowerLeg = round((closePrice - 200) / 100) * 100
lowerLeg2 = round((closePrice - 300) / 100) * 100
upperLeg = round((closePrice + 200) / 100) * 100
upperLeg2 = round((closePrice + 300) / 100) * 100
self.lb_left_leg.insert(0, f'左右腳({lowerLeg}, {upperLeg})')
# 取得 ticker symbol
my_code = nt.get_monthyear_code()
lowerLegSymbol = f'TX4{lowerLeg}{my_code[1]}'
lowerLegSymbol2 = f'TX4{lowerLeg2}{my_code[1]}'
upperLegSymbol = f'TX4{upperLeg}{my_code[0]}'
upperLegSymbol2 = f'TX4{upperLeg2}{my_code[0]}'
# 取得最新選擇權報價
self.lb_left_leg.insert(0, f'{lowerLegSymbol}, {lowerLegSymbol2}, {upperLegSymbol}, {upperLegSymbol2}')
quote = nt.get_quote(lowerLegSymbol, True)
pLower = quote["closePrice"]
self.lb_left_leg.insert(0, f'{quote["name"]} 最新報價 {quote["closeTimeAsDatetime"].strftime("%m/%d %H:%M")}: {quote["closePrice"]}')
quote = nt.get_quote(lowerLegSymbol2, True)
pLower2 = quote["closePrice"]
self.lb_left_leg.insert(0, f'{quote["name"]} 最新報價 {quote["closeTimeAsDatetime"].strftime("%m/%d %H:%M")}: {quote["closePrice"]}')
quote = nt.get_quote(upperLegSymbol, True)
pUpper = quote["closePrice"]
self.lb_left_leg.insert(0, f'{quote["name"]} 最新報價 {quote["closeTimeAsDatetime"].strftime("%m/%d %H:%M")}: {quote["closePrice"]}')
quote = nt.get_quote(upperLegSymbol2, True)
pUpper2 = quote["closePrice"]
self.lb_left_leg.insert(0, f'{quote["name"]} 最新報價 {quote["closeTimeAsDatetime"].strftime("%m/%d %H:%M")}: {quote["closePrice"]}')
self.lb_left_leg.insert(0, f'左價差:{pLower-pLower2}')
self.lb_left_leg.insert(0, f'右價差:{pUpper-pUpper2}')
raw-image

關鍵的最後一步:下單。但這需要監控即時的買賣盤掛單,...殘念!api 竟然沒有提供,只有最後一筆成交價,但那怎麼夠呢?富邦又搞這種飛機,真是可惜,有種一路過關斬將,到魔王關前卻說,魔王在睡覺不應戰,很糟糕的感覺。整個 quote 的內容如下,股票報價反而資料比較完整,但我要玩選擇權啊。

# 期權報價的回傳欄位,缺乏 bids, asks 欄位
{'date': '2024-11-19',
'type': 'OPTION_AH',
'exchange': 'TAIFEX',
'symbol': 'TX422400W4',
'name': '臺指選擇權W4114;22400賣權',
'previousClose': 275,
'openPrice': 245,
'openTime': 1731913261296000,
'highPrice': 335,
'highTime': 1731922927371000,
'lowPrice': 245,
'lowTime': 1731913261296000,
'closePrice': 325,
'closeTime': 1731931454936000,
'avgPrice': 318.1,
'change': 67,
'changePercent': 25.97,
'amplitude': 34.88,
'lastPrice': 325,
'lastSize': 1,
'total': {'tradeVolume': 82,
'totalBidMatch': 35,
'totalAskMatch': 35,
'time': 1731931454936000},
'lastTrade': {'bid': 325,
'ask': 329,
'price': 325,
'size': 1,
'time': 1731931454936000,
'serial': '00047551'},
'serial': 3365651,
'lastUpdated': 1731932114430000}

# 股票報價的回傳欄位,有完整的 bids, asks 欄位
{'date': '2024-11-18',
'type': 'EQUITY',
'exchange': 'TWSE',
'market': 'TSE',
'symbol': '2330',
'name': '台積電',
'referencePrice': 1035,
'previousClose': 1035,
'openPrice': 1030,
'openTime': 1731891604272912,
'highPrice': 1035,
'highTime': 1731891607329087,
'lowPrice': 1020,
'lowTime': 1731894264422743,
'closePrice': 1025,
'closeTime': 1731907800000000,
'avgPrice': 1024.95,
'change': -10,
'changePercent': -0.97,
'amplitude': 1.45,
'lastPrice': 1025,
'lastSize': 5085,
'bids': [{'price': 1025, 'size': 688},
{'price': 1020, 'size': 1847},
{'price': 1015, 'size': 2189},
{'price': 1010, 'size': 1984},
{'price': 1005, 'size': 1370}],
'asks': [{'price': 1030, 'size': 1246},
{'price': 1035, 'size': 1600},
{'price': 1040, 'size': 828},
{'price': 1045, 'size': 726},
{'price': 1050, 'size': 431}],
'total': {'tradeValue': 32506385000,
'tradeVolume': 31715,
'tradeVolumeAtBid': 23002,
'tradeVolumeAtAsk': 6934,
'transaction': 7270,
'time': 1731907800000000},
'lastTrade': {'bid': 1025,
'ask': 1030,
'price': 1025,
'size': 5085,
'time': 1731907800000000,
'serial': 9204544},
'lastTrial': {'bid': 1025,
'ask': 1030,
'price': 1025,
'size': 5046,
'time': 1731907795119134,
'serial': 9203603},
'isClose': True,
'serial': 9204544,
'lastUpdated': 1731907800000000}

先玩到這裡,已經很多東西了,消化消化,沈澱沈澱。


Newman 2024/11/18

導覽頁:紐曼的技術筆記-索引


後記:經詢問富果群組服務人員,得到親切的回應,說明需要用「socket」連線方式才可以取得買賣五檔掛單,想說都到臨門一腳了,因此被迫再打延長賽:

from fubon_neo.sdk import FubonSDK, Mode
import os
from dotenv import load_dotenv

def handle_message(message):
print(message)

def handle_connect(message):
print(message)

def handle_error(message):
print(message)

sdk = FubonSDK()
load_dotenv()
id = os.getenv('id')
pwd = os.getenv('pwd')
dir = os.path.dirname(os.path.abspath(__file__))
accounts = sdk.login(id, pwd, f"{dir}/{id}.pfx" , f"{pwd}")
sdk.init_realtime(Mode.Normal) # Speed / Normal
futopt = sdk.marketdata.websocket_client.futopt
futopt.on('message', handle_message)
futopt.on("connect", handle_connect)
futopt.on("error", handle_error)
futopt.connect()
futopt.subscribe({
'channel': 'books',
'symbol': 'TX423000K4', # TX423000K4: 23000買權11月第4, TMFK4: 微台指 2024/11, TXFK4: 台指期 2024/11
'afterHours' : True # 夜盤行情
})
-----------------------------------
以下是 console 訊息
-----------------------------------
{"event":"authenticated","data":{"message":"Authenticated successfully"}}
{"event":"pong","data":{"time":1732015067920651,"state":""}}
{"event":"subscribed","data":{"id":"LkglGx2LGPu2DKPvZ2q3T0zXV2Kpg3HgnmzkB3kZTmQXODxXq4uW4K8OA","channel":"books","symbol":"TX423000K4","afterHours":true}}
{"event":"snapshot","data":{"symbol":"TX423000K4","type":"OPTION_AH","exchange":"TAIFEX","bids":[{"price":204,"size":2},{"price":203,"size":20},{"price":191,"size":2},{"price":190,"size":5},{"price":189,"size":2}],"asks":[{"price":210,"size":23},{"price":211,"size":1},{"price":212,"size":5},{"price":213,"size":2},{"price":216,"size":2}],"time":1732015059943000},"id":"xxxxx","channel":"books"}

以上是測試成功的碼,這也是經過一波三折!說來話長,就不囉唆了,總之就是原來以為又要準備一台 windows 電腦,但最後因為社群的幫助,解決了問題,又可以用一台 macbook 打天下了。


Newman 2024/11/19
































avatar-img
22會員
106內容數
漫步是一種境界。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
newman的沙龍 的其他內容
雖然公眾資料庫和網路爬蟲,已經可以玩得不亦樂乎了,但所有研究最終的目的,還是要實施於一個真實賬戶,真金白銀的實際效益才有意義。尤其是「即時」性要求較高的策略,一定需要用程式補上「自動化下單」的部分,快速變動的資料如「行情報價」,「訂單狀態」,「帳戶庫存」等等,也需要程式嚴密監控,這就需要券商提供
「最佳化」是很酷的觀念,因為現實世界中許多問題,並沒有嚴謹一致的公式解,但可以利用計算機高速運算能力,透過巧妙的演算法,迭代式反覆逼近最佳解,應用領域非常廣。若能多瞭解一點原理,一定可以提昇解決問題的能力。今天從網路上發現一堂手把手的教學課程,就來演練一下整個過程。期望徹底了解之後,後面可以
現有的家是 azure,功能多又有免費額度,算是堪用,只是因為雲端產業蓬勃發展,想體驗一下新東西,所以這次來玩玩新東西 fly.io。從開啟以下賞心悅目的官網,第一次接觸到成功佈署上線,大概半小時就搞定了,體驗不錯,以下稍做紀錄。
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
查景點,美食,導航,這些功能已經深深融入我們的生活了,背後著要的技術支柱就是科技巨頭所提供的龐大全球地圖資料庫,和隨身手機上的 GPS 定位功能,這是 App 的強項,非玩不可。 需求情境: 在陌生的城市探索,最常用到的地圖功能是什麼?找星巴克是我的第一名,第二是享受更多功能的便利商店,再其次就
需求情境: 為了讓多人使用 App,必須有驗證程序,以識別特定使用者,存取各自擁有的資源。 解決方案: 引用 google 所提供的雲端服務平台 Firebase,其中有多種驗證功能可選用。基於個人對 google 的偏愛,決定先採用 google signin 的方法,實作 login lo
雖然公眾資料庫和網路爬蟲,已經可以玩得不亦樂乎了,但所有研究最終的目的,還是要實施於一個真實賬戶,真金白銀的實際效益才有意義。尤其是「即時」性要求較高的策略,一定需要用程式補上「自動化下單」的部分,快速變動的資料如「行情報價」,「訂單狀態」,「帳戶庫存」等等,也需要程式嚴密監控,這就需要券商提供
「最佳化」是很酷的觀念,因為現實世界中許多問題,並沒有嚴謹一致的公式解,但可以利用計算機高速運算能力,透過巧妙的演算法,迭代式反覆逼近最佳解,應用領域非常廣。若能多瞭解一點原理,一定可以提昇解決問題的能力。今天從網路上發現一堂手把手的教學課程,就來演練一下整個過程。期望徹底了解之後,後面可以
現有的家是 azure,功能多又有免費額度,算是堪用,只是因為雲端產業蓬勃發展,想體驗一下新東西,所以這次來玩玩新東西 fly.io。從開啟以下賞心悅目的官網,第一次接觸到成功佈署上線,大概半小時就搞定了,體驗不錯,以下稍做紀錄。
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
查景點,美食,導航,這些功能已經深深融入我們的生活了,背後著要的技術支柱就是科技巨頭所提供的龐大全球地圖資料庫,和隨身手機上的 GPS 定位功能,這是 App 的強項,非玩不可。 需求情境: 在陌生的城市探索,最常用到的地圖功能是什麼?找星巴克是我的第一名,第二是享受更多功能的便利商店,再其次就
需求情境: 為了讓多人使用 App,必須有驗證程序,以識別特定使用者,存取各自擁有的資源。 解決方案: 引用 google 所提供的雲端服務平台 Firebase,其中有多種驗證功能可選用。基於個人對 google 的偏愛,決定先採用 google signin 的方法,實作 login lo
你可能也想看
Google News 追蹤
上兩篇有關List的文章,此篇文上兩章的延續,整理一些常用的方法和操作。 [Python]List(列表)新增、修改、刪除元素 [Python基礎]容器 list(列表),tuple(元組) 還有一些常用的 list 方法和操作,讓你能更靈活地處理列表數據
Thumbnail
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
Thumbnail
在離線環境需要安裝Python套件時就相當的麻煩,需要先下載好套件包,在打指令安裝,若套件數量一多時就會相當麻煩。 本文將介紹如何利用兩行指令快速的安裝整個資料夾的套件。
Thumbnail
如果你也是從事軟體相關工作的人,一定會遭遇突然需要你去學習一套你不熟悉的程式語言狀況吧,此時你會怎麼做呢? 是趕快去買書來看嗎? 還是趕快找一門程式課來上? 又或者乾脆去找會的同事來教學?
Thumbnail
你好,在下最近在學習開發web,學了html css js,也得出一些心得,由於網路上已有許多教學,所以我會著重在如何開發出to do List,以及解釋我寫的程式碼。相關的教學我會直接貼網址。如果我有什麼地方出錯,或者是可以寫得更好,歡迎在下方留言,討論。 首先先介紹我的開發環境: 我用了vs
在一次五天的年假中,我決定開始學習Python編程。雖然最初進展順利,但後來發現自己在解題和邏輯方面遇到了困難。經過思考後,我決定轉而學習C/C++,並且制定了一個計畫,希望成為一名後端工程師。這將需要挑戰我的時間管理能力。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
IDE 升級後出現了一樣的錯誤,手上程式碼沒有 pylint black-format 檢查上不了 gitlab,我又點開了那個很小很小的 x 符號,裡面 logs 提示的解決方式是升級..
上兩篇有關List的文章,此篇文上兩章的延續,整理一些常用的方法和操作。 [Python]List(列表)新增、修改、刪除元素 [Python基礎]容器 list(列表),tuple(元組) 還有一些常用的 list 方法和操作,讓你能更靈活地處理列表數據
Thumbnail
打開 jupyter notebook 寫一段 python 程式,可以完成五花八門的工作,這是玩程式最簡便的方式,其中可以獲得很多快樂,在現今這種資訊發達的時代,幾乎沒有門檻,只要願意,人人可享用。 下一步,希望程式可以隨時待命聽我吩咐,不想每次都要開電腦,啟動開發環境,只為完成一個重複性高
Thumbnail
在離線環境需要安裝Python套件時就相當的麻煩,需要先下載好套件包,在打指令安裝,若套件數量一多時就會相當麻煩。 本文將介紹如何利用兩行指令快速的安裝整個資料夾的套件。
Thumbnail
如果你也是從事軟體相關工作的人,一定會遭遇突然需要你去學習一套你不熟悉的程式語言狀況吧,此時你會怎麼做呢? 是趕快去買書來看嗎? 還是趕快找一門程式課來上? 又或者乾脆去找會的同事來教學?
Thumbnail
你好,在下最近在學習開發web,學了html css js,也得出一些心得,由於網路上已有許多教學,所以我會著重在如何開發出to do List,以及解釋我寫的程式碼。相關的教學我會直接貼網址。如果我有什麼地方出錯,或者是可以寫得更好,歡迎在下方留言,討論。 首先先介紹我的開發環境: 我用了vs
在一次五天的年假中,我決定開始學習Python編程。雖然最初進展順利,但後來發現自己在解題和邏輯方面遇到了困難。經過思考後,我決定轉而學習C/C++,並且制定了一個計畫,希望成為一名後端工程師。這將需要挑戰我的時間管理能力。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
IDE 升級後出現了一樣的錯誤,手上程式碼沒有 pylint black-format 檢查上不了 gitlab,我又點開了那個很小很小的 x 符號,裡面 logs 提示的解決方式是升級..