2025年12月5日 星期五

Windows 設定 Anydesk 開機啟動的方法

我平常都使用 Ultraviewer 連線選端 Windows 主機桌面, 但有時會因不明原因連不上 (應該不是 Ultraviewer 有問題), 有必要以 Anydesk 做為備用方案. 設定好無人值守密碼後還必須設定當 Windows 重啟時自動執行 Anydesk, 搜尋谷歌找到如下作法 :




按 Windows + R, 輸入 shell:startup 開啟 Windows 的啟動資料夾 :




然後複製 Anydesk.exe 執行檔 (我使用免安裝版) :




貼到啟動資料夾下即可 :



2025年12月4日 星期四

Python 學習筆記 : 回測框架 backtrader (一)

Backtrader 是 Daniel Rodriguez 於 2013 年基於個人交易需求所開發的策略回測和實盤交易框架, 是 Python 量化交易領域最經典, 架構最完整的開放原始碼回測 (Backtesting) 框架之一, 後來經由社群貢獻了大量文檔, 範例和功能擴展. 隨著框架趨近穩定更新頻率隨之降低, 原作者轉移重心於 2021 年宣布不再主動維護和更新此框架, 但由於 Backtrader 的架構極其成熟且功能齊全, 使用社群仍非常活躍, 是入門到進階量化研究不可或缺的工具, 參考 :  


iT 邦幫忙也有兩篇值得參考的文章 : 


以下摘要整理從谷歌與 AI 搜尋到的 backtrader 資料. 


1. 功能簡介 : 

Backtrader 的核心功能是讓量化交易者可用 Python 清晰地描述一個交易策略, 並在歷史資料上精確模擬實際交易的結果, 其模擬的內容如下 : 
  • K 線逐根推進 (逐 tick 逐 bar)
  • 依照策略邏輯決定買賣點
  • 手續費, 稅, 滑價
  • 部位, 資金, 風險控管
  • 訂單與成交機制 (市價單, 限價單, 停損單)
  • 資金單位, 合約單位
  • 多部位, 空單
  • 多股票組合 (Portfolio) 
  • 交易日, 非交易日跳空
  • 交易時間同步, 資料對齊
它是真正模擬交易過程, 而非簡單地計算 "如果收盤價 > MA20 就買進". 這些功能若自行使用 pandas 來寫非常繁雜 (事實上 backtrader 內部就使用了 pandas 的 DataFrame 來處理和管理交易資料), 而這些功能 backtrader 都已實作出來. 它提供如下功能 :
  • 回測 (Backtest) 
  • 多策略比較
  • 多個股票同時回測 (Portfolio)
  • 參數最佳化 (Optimizer)
  • 技術指標 (150+)
  • 自訂指標
  • 自訂手續費, 稅
  • 市價單, 限價單, 停損單
  • 部分成交 (matching engine)
  • 實盤交易 (支援 Interactive Brokers 與 CCXT crypto)
  • 視覺化 (K 線, 指標, 買賣訊號)
Backtrader 是一個非常成熟的交易模擬器, 幾乎能涵蓋 95% 的量化交易研究需求. 

一個完整的量化交易流程如下 :

資料抓取 → 資料清洗 → 策略設計回測參數優化 → 風險控管 → 策略組合 → 監控 → 實盤下單

而 backtrader 負責其中的策略設計, 回測, 參數優化, 再加上交易模擬與石盤交易 :  

策略設計 → 回測 → 參數優化 → 交易模擬實盤交易

Backtrader 資料來源支援下列四種 : 
  • Yahoo Finance 
  • FinLab
  • pandas-datareader
  • API
不過 backtrader 並非依賴 yfinance 從 Yahoo Finance 取得 K 線資料, 而是官方自己寫的爬蟲. 

在回測領域除 backtrader 外還有諸如 vectorbt, backtesting.py, 與 Zipline 等類似的框架, 優缺點比較如下表 : 


 套件 優點 缺點 適合對象
Backtrader最完整的交易模擬 + 實盤官方停更 (2021)策略研究者、工程師
vectorbt超快(NumPy/Numba)不易寫出複雜策略量化團隊、研究速度
backtesting.py超簡單、學習快功能少新手
Zipline以前由 Quantopian 使用安裝極難、老舊不推薦


Backtrader 架構具有如下優點 : 
  • API 設計優美 (物件導向), 語法清楚可讀性高
  • 極完整的回測功能 :多策略, 多股票, Portfolio 都能一次回測
  • 相容性佳且可擴充性高 : 可自訂指標, 策略, Broker, 下單方法, 手續費與滑價模型
  • 內建圖表有進場箭頭, K 線, 指標 : 不需寫任何 matplotlib 程式碼
  • 教學文件非常豐富
其他框架常缺少像交易一樣的細節, backtrader 模擬交易細緻度是最頂級的.


2. 核心架構 :  

Backtrader 將回測模擬成一個正在運轉的交易系統, 其核心由下列五個元件組成 : 
  • Cerebro : 
    Cerebro 是 Backtrader 的主引擎, 負責整合所有的策略, 資料源, 分析結果等. 它是執行回測和實盤交易的核心組件, 並且管理著整個交易過程.
  • Strategy :
    Strategy 是用戶定義的策略邏輯, 包含了交易的決策過程. 用戶可以在這個類別中定義進出場的條件 (買賣點), 技術指標, 倉位控管 (風險管理) 等.
  • Data Feed :
    DataFeed 是用來載入和管理股票或其他金融資產的 K 線資料 (Pandas DataFrame). Backtrader 支持多種資料源, 包括 CSV 文件, Yahoo Finance, Interactive Brokers 等.
  • Indicators :
    Indicators 是用來計算和使用技術指標的元件, backtrader 提供了許多內建的技術指標, 例如移動平均, 相對強弱指標 (RSI) 等, 用戶也可以自定義指標.
  • Analyzer
    Analyzers 用於分析和評估策略的績效, 這些分析器可以計算各種指標, 例如年化報酬率, 夏普比率 (Sharpe Ratio), 最大回撤 (Maximum Drawdown) 等, 幫助用戶評估策略的表現.
  • Observers (附加元件) :
其中 Cerebro 是 backtrader 的大腦, 是整個系統的控制中心, 相當於是一台車的引擎, 而 Strategy 則是負責交易策略邏輯, 內建了技術指標, 下單買賣, 與部位控管功能, 其上下關係結構如下 :

Cerebro (大腦)
 ├── Strategy (買賣邏輯=策略)
 │     ├── Indicators (技術指標)
 │     ├── Orders (Buy/Sell) (下單)
 │     └── Position (持倉部位)
 ├── DataFeed (K 線量價資料來源)
 ├── Broker (Cash/Value) (資金管理)
 ├── Sizers (下單大小)
 ├── Analyzers (Sharpe/Drawdown) (績效分析例如 Sharpe, 回撤)
 └── Plot (視覺化)


3. 安裝 backtrader 套件 :  

只要用 pip 即可安裝此套件 : 

D:\python\test>pip install backtrader   
Collecting backtrader
  Downloading backtrader-1.9.78.123-py2.py3-none-any.whl.metadata (6.8 kB)
Downloading backtrader-1.9.78.123-py2.py3-none-any.whl (419 kB)
Installing collected packages: backtrader
Successfully installed backtrader-1.9.78.123

安裝好後匯入 backtrader 檢視版本 :

>>> import backtrader as bt 
>>> bt.__version__  
'1.9.78.123'

然後用自訂模組 members 的 list_members() 函式檢視 backtrader 內容, 參考 :


>>> from members import list_members   
>>> list_members(bt)  
AbstractDataBase <class 'backtrader.feed.MetaAbstractDataBase'>
All <class 'backtrader.linebuffer.MetaLineActions'>
Analyzer <class 'backtrader.analyzer.MetaAnalyzer'>
And <class 'backtrader.linebuffer.MetaLineActions'>
Any <class 'backtrader.linebuffer.MetaLineActions'>
AutoDictList <class 'type'>
AutoInfoClass <class 'type'>
AutoOrderedDict <class 'type'>
BackBroker <class 'backtrader.broker.MetaBroker'>
BacktraderError <class 'type'>
BrokerBase <class 'backtrader.broker.MetaBroker'>
BuyOrder <class 'backtrader.metabase.MetaParams'>
CSVDataBase <class 'backtrader.feed.MetaCSVDataBase'>
CSVFeedBase <class 'backtrader.metabase.MetaParams'>
Cerebro <class 'backtrader.metabase.MetaParams'>
Cmp <class 'backtrader.linebuffer.MetaLineActions'>
CmpEx <class 'backtrader.linebuffer.MetaLineActions'>
CommInfoBase <class 'backtrader.metabase.MetaParams'>
CommissionInfo <class 'backtrader.metabase.MetaParams'>
DTFaker <class 'type'>
DataAccessor <class 'backtrader.lineiterator.MetaLineIterator'>
DataBase <class 'backtrader.feed.MetaAbstractDataBase'>
DataClone <class 'backtrader.feed.MetaAbstractDataBase'>
DataSeries <class 'backtrader.lineseries.MetaLineSeries'>
DivByZero <class 'backtrader.linebuffer.MetaLineActions'>
DivZeroByZero <class 'backtrader.linebuffer.MetaLineActions'>
DotDict <class 'type'>
FeedBase <class 'backtrader.metabase.MetaParams'>
Filter <class 'backtrader.metabase.MetaParams'>
If <class 'backtrader.linebuffer.MetaLineActions'>
Indicator <class 'backtrader.indicator.MetaIndicator'>
IndicatorBase <class 'backtrader.lineiterator.MetaLineIterator'>
ItemCollection <class 'type'>
LineActions <class 'backtrader.linebuffer.MetaLineActions'>
LineAlias <class 'type'>
LineBuffer <class 'backtrader.lineroot.MetaLineRoot'>
LineCoupler <class 'function'>
LineDelay <class 'function'>
LineIterator <class 'backtrader.lineiterator.MetaLineIterator'>
LineMultiple <class 'backtrader.lineroot.MetaLineRoot'>
LineNum <class 'function'>
LineOwnOperation <class 'backtrader.linebuffer.MetaLineActions'>
LinePlotterIndicator <class 'backtrader.indicator.MtLinePlotterIndicator'>
LineRoot <class 'backtrader.lineroot.MetaLineRoot'>
LineSeries <class 'backtrader.lineseries.MetaLineSeries'>
LineSeriesMaker <class 'function'>
LineSeriesStub <class 'backtrader.lineseries.MetaLineSeries'>
LineSingle <class 'backtrader.lineroot.MetaLineRoot'>
Lines <class 'type'>
LinesCoupler <class 'function'>
LinesOperation <class 'backtrader.linebuffer.MetaLineActions'>
Lines_LineSeries <class 'type'>
Lines_LineSeries_LineSeriesStub <class 'type'>
List <class 'type'>
Logic <class 'backtrader.linebuffer.MetaLineActions'>
MAXINT <class 'int'>
Max <class 'backtrader.linebuffer.MetaLineActions'>
MetaAbstractDataBase <class 'type'>
MetaAnalyzer <class 'type'>
MetaBroker <class 'type'>
MetaCSVDataBase <class 'type'>
MetaIndicator <class 'type'>
MetaLineActions <class 'type'>
MetaLineIterator <class 'type'>
MetaLineSeries <class 'type'>
MetaObserver <class 'type'>
MetaParams <class 'type'>
MetaSigStrategy <class 'type'>
MetaStrategy <class 'type'>
MetaTimeFrameAnalyzerBase <class 'type'>
Min <class 'backtrader.linebuffer.MetaLineActions'>
MtLinePlotterIndicator <class 'type'>
MultiCoupler <class 'backtrader.lineiterator.MetaLineIterator'>
MultiLogic <class 'backtrader.linebuffer.MetaLineActions'>
MultiLogicReduce <class 'backtrader.linebuffer.MetaLineActions'>
NAN <class 'float'>
OHLC <class 'backtrader.lineseries.MetaLineSeries'>
OHLCDateTime <class 'backtrader.lineseries.MetaLineSeries'>
Observer <class 'backtrader.observer.MetaObserver'>
ObserverBase <class 'backtrader.lineiterator.MetaLineIterator'>
OptReturn <class 'type'>
Or <class 'backtrader.linebuffer.MetaLineActions'>
Order <class 'backtrader.metabase.MetaParams'>
OrderBase <class 'backtrader.metabase.MetaParams'>
OrderData <class 'type'>
OrderExecutionBit <class 'type'>
OrderedDict <class 'type'>
PandasMarketCalendar <class 'backtrader.metabase.MetaParams'>
Position <class 'type'>
PseudoArray <class 'type'>
Reduce <class 'backtrader.linebuffer.MetaLineActions'>
Replayer <class 'backtrader.metabase.MetaParams'>
ReplayerDaily <class 'backtrader.metabase.MetaParams'>
ReplayerMinutes <class 'backtrader.metabase.MetaParams'>
ReplayerMonthly <class 'backtrader.metabase.MetaParams'>
ReplayerSeconds <class 'backtrader.metabase.MetaParams'>
ReplayerTicks <class 'backtrader.metabase.MetaParams'>
ReplayerWeekly <class 'backtrader.metabase.MetaParams'>
Resampler <class 'backtrader.metabase.MetaParams'>
ResamplerDaily <class 'backtrader.metabase.MetaParams'>
ResamplerMinutes <class 'backtrader.metabase.MetaParams'>
ResamplerMonthly <class 'backtrader.metabase.MetaParams'>
ResamplerSeconds <class 'backtrader.metabase.MetaParams'>
ResamplerTicks <class 'backtrader.metabase.MetaParams'>
ResamplerWeekly <class 'backtrader.metabase.MetaParams'>
ResamplerYearly <class 'backtrader.metabase.MetaParams'>
SESSION_END <class 'int'>
SESSION_START <class 'int'>
SESSION_TIME <class 'int'>
SIGNAL_LONG <class 'int'>
SIGNAL_LONGEXIT <class 'int'>
SIGNAL_LONGEXIT_ANY <class 'int'>
SIGNAL_LONGEXIT_INV <class 'int'>
SIGNAL_LONGSHORT <class 'int'>
SIGNAL_LONG_ANY <class 'int'>
SIGNAL_LONG_INV <class 'int'>
SIGNAL_NONE <class 'int'>
SIGNAL_SHORT <class 'int'>
SIGNAL_SHORTEXIT <class 'int'>
SIGNAL_SHORTEXIT_ANY <class 'int'>
SIGNAL_SHORTEXIT_INV <class 'int'>
SIGNAL_SHORT_ANY <class 'int'>
SIGNAL_SHORT_INV <class 'int'>
SellOrder <class 'backtrader.metabase.MetaParams'>
Signal <class 'backtrader.indicator.MetaIndicator'>
SignalStrategy <class 'backtrader.strategy.MetaSigStrategy'>
SignalTypes <class 'list'>
SimpleFilterWrapper <class 'type'>
SingleCoupler <class 'backtrader.linebuffer.MetaLineActions'>
Sizer <class 'backtrader.metabase.MetaParams'>
SizerBase <class 'backtrader.metabase.MetaParams'>
SizerFix <class 'backtrader.metabase.MetaParams'>
StopBuyOrder <class 'backtrader.metabase.MetaParams'>
StopLimitBuyOrder <class 'backtrader.metabase.MetaParams'>
StopLimitSellOrder <class 'backtrader.metabase.MetaParams'>
StopSellOrder <class 'backtrader.metabase.MetaParams'>
Store <class 'backtrader.store.MetaSingleton'>
Strategy <class 'backtrader.strategy.MetaStrategy'>
StrategyBase <class 'backtrader.lineiterator.MetaLineIterator'>
StrategySkipError <class 'type'>
Sum <class 'backtrader.linebuffer.MetaLineActions'>
TimeFrame <class 'type'>
TimeFrameAnalyzerBase <class 'backtrader.analyzer.MetaTimeFrameAnalyzerBase'>
Timer <class 'backtrader.metabase.MetaParams'>
Trade <class 'type'>
TradeHistory <class 'type'>
TradingCalendar <class 'backtrader.metabase.MetaParams'>
TradingCalendarBase <class 'backtrader.metabase.MetaParams'>
WriterBase <class 'backtrader.metabase.MetaParams'>
WriterFile <class 'backtrader.metabase.MetaParams'>
WriterStringIO <class 'backtrader.metabase.MetaParams'>
absolute_import <class '__future__._Feature'>
analyzer <class 'module'>
analyzers <class 'module'>
array <class 'module'>
backtrader <class 'module'>
broker <class 'module'>
brokers <class 'module'>
bt <class 'module'>
calendar <class 'module'>
cerebro <class 'module'>
cmp <class 'function'>
collections <class 'module'>
collectionsAbc <class 'module'>
comminfo <class 'module'>
commissions <class 'module'>
comms <class 'module'>
copy <class 'module'>
dataseries <class 'module'>
date <class 'type'>
date2num <class 'function'>
datetime <class 'module'>
division <class '__future__._Feature'>
errors <class 'module'>
feed <class 'module'>
feeds <class 'module'>
filler <class 'module'>
fillers <class 'module'>
filter <class 'type'>
filters <class 'module'>
findowner <class 'function'>
flt <class 'module'>
functions <class 'module'>
functools <class 'module'>
ind <class 'module'>
indicator <class 'module'>
indicators <class 'module'>
inspect <class 'module'>
integer_types <class 'tuple'>
io <class 'module'>
islice <class 'type'>
iteritems <class 'function'>
itertools <class 'module'>
itervalues <class 'function'>
keys <class 'function'>
linebuffer <class 'module'>
lineiterator <class 'module'>
lineroot <class 'module'>
lineseries <class 'module'>
map <class 'type'>
math <class 'module'>
mathsupport <class 'module'>
metabase <class 'module'>
multiprocessing <class 'module'>
num2date <class 'function'>
num2time <class 'function'>
obs <class 'module'>
observer <class 'module'>
observers <class 'module'>
operator <class 'module'>
order <class 'module'>
os <class 'module'>
position <class 'module'>
pp <class 'module'>
print_function <class '__future__._Feature'>
range <class 'type'>
resamplerfilter <class 'module'>
signal <class 'module'>
signals <class 'module'>
sizer <class 'module'>
sizers <class 'module'>
store <class 'module'>
stores <class 'module'>
strategies <class 'module'>
strategy <class 'module'>
strats <class 'module'>
string_types <class 'tuple'>
studies <class 'module'>
sys <class 'module'>
talib <class 'module'>
time2num <class 'function'>
timedelta <class 'type'>
timer <class 'module'>
trade <class 'module'>
tradingcal <class 'module'>
tzparse <class 'function'>
unicode_literals <class '__future__._Feature'>
utils <class 'module'>
version <class 'module'>
with_metaclass <class 'function'>
writer <class 'module'>
zip <class 'type'>

其中 Cerebro 是在使用 Backtrader 時最常接觸的類別. 


4. 回測程式結構 :  

撰寫回測程式必須繼承 backtrader.Strategy 類別自訂一個策略類別, 其基本結構如下 :

# my_strategy.py
import backtrader as bt

class MyStrategy(bt.Strategy)

    # 1. 定義參數
    params=()
    
    # 2. 定義會用到的資料
    def __init__(self):
        pass

    # 3. 輸出策略運行訊息 (可有可無)
    def log():
        pass    

    # 4. 追蹤訂單狀態 & 發出通知 (可有可無)
    def notify_order(self, order):
        pass
    
    # 5. 交易結果通知 (可有可無)
    def notify_trade(self, trade):
        pass
    
    # 6. 交易策略 (核心部分)
    def next(self):
        pass
    
其中初始化方法 __init__(), 與交易策略方法 next() 兩者為必要的; 類別變數 params 與 log(), notify_order(), 與 notify_trade() 則是可有可無的, 其功用說明如下 :
  • params 變數 : 
    用來定義策略的可配置參數, 讓使用者在建立策略物件時可以傳遞不同的參數值, 例如定義移動平均 ma 的週期或幾點停損, 風險管理參數等, 這樣在撰寫回測或實盤交易策略時可以輕鬆調整這些值, 而不必修改策略的程式碼. 另外一個常見用途是設定一個 range() 值來跑最優參數. 此類別變數值為一個 tuple, 元素也是一個 tuple, 例如 :
    params=(('maperiod', 5),)
  • __init__() 方法 :
    此為呼叫類別建構式時首先執行的函式, 用來初始化物件, 傳入參數 self 為物件本身, 在此方法中會利用此 self 定義物件的屬性並賦值 (此即初始化), 以便在其它方法中可以使用這些屬性值. 
  • log() 方法 :
    用來在策略運行時輸出重要的日誌訊息, 以便於觀察並記錄策略的表現, 提升策略的可讀性和可維護性. 可根據需求來定義 log() 方法, 以便將訊息輸出到控制台, 文件或其他日誌系統中.
  • notify_order() 方法 :
    用於接收訂單狀態的通知, 當訂單的狀態發生變化時 (例如已提交, 已執行, 已取消等), 此方法會被自動呼叫, 功用類似券商軟體的委託單查詢. 
  • notify_trade() 方法 :
    用於接收交易狀態的通知, 當交易的狀態發生變化時, 例如開倉 (股票為買入) 平倉 (股票為賣出) 等, 此方法會被自動呼叫, 平倉時可以輸出獲利訊息; 開倉時可以輸出庫存價格與持倉量, 也可以不做任何動作. 此方法功用類似券商軟體的已/未實現損益查詢. 
  • next() 方法 :
    此為策略的核心方法, 用來定義交易邏輯, 例如進出場的條件, 可根據當前價格與指標值進行交易決策, 每當有新的資料點進入時就會自動呼叫 next() 方法. 此方法相當於一個隱形的迴圈, Backtrader 程式會從資料起始處逐一讀取每根 K 線, 然後呼叫 next() 執行一次交易策略, 在此方法中可以用 self.data 屬性取得量價資料與技術指標 (都是 Series), 例如 :
    self.data.close[0] : 最新一根 K 線收盤價
    self.data.close[-1] : 前一根 K 線收盤價
    self.data.close[-2] : 前兩根 K 線收盤價
    self.sma[0] : 最新一根 K 線 SMA 值
    self.rsi[-1] : 前一根 K 線 SRI 值
建立一個 Backtrader 回測程式的步驟如下 :

(1). 定義策略類別 :
(2). 建立 Cerebro 物件 :
(3). 載入 K 線資料
(4). 設定 Broker (資金, 手續費)
(5). 加入 Strategy
(6). 加入 Analyzer / Sizer / Observer
(7). 執行 cerebro.run()
(8). 輸出績效
(9). 繪圖
       
下面是一個簡單的 5ma 向上突破 20 ma 買進, 向下跌破賣出台積電的回測範例 :

# backtrader_test_1.py
import backtrader as bt
import yfinance as yf

class MyStrategy(bt.Strategy):
    def __init__(self):
        self.fast=bt.ind.SMA(self.data.close, period=5)
        self.slow=bt.ind.SMA(self.data.close, period=20)
        self.crossover=bt.ind.CrossOver(self.fast, self.slow)

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
        else:
            if self.crossover < 0:
                self.close()

# 建立 Cerebro 主引擎物件
cerebro=bt.Cerebro()

# 加上策略
cerebro.addstrategy(MyStrategy)

# 加上 K 線資料
df=yf.download('2330.TW', start='2024-01-01', end='2025-01-01', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0])  # 欄位單層化
data=bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)

# 設定初始資金
cerebro.broker.setcash(1_000_000)
print('初始資金: %.2f' % cerebro.broker.getvalue())

# 跑回測
result=cerebro.run()

# 輸出期末資產總額
print('期末資金: %.2f' % cerebro.broker.getvalue())

# 畫圖
cerebro.plot()

此例繼承 bt.Strategy 定義一個 MyStrategy 類別, 在 __init__() 初始化時呼叫內建的 SMA() 與 CrossOver() 函式計算移動平均線的快線 (5ma), 慢線 (20 ma), 以及是否發生交越的屬性 slow, fast, 與 crossover; 並於 next() 中定義交易策略, cross 屬性黃金交叉時買進; 死亡交叉時賣出. 接著呼叫 bt.Cerebro() 建立 Cerebro 物件後呼叫其 addstrategy() 方法並傳入 MyStrategy 類別. 

然後利用 yfinance 取得台積電 (2330.TW) 2024 年的量價資料並轉成 PandasData 物件, 呼叫 Cerebro 物件的 adddata() 方法傳入此 PandasData 物件做為資料源, 最後設定初始資金 100 萬元, 呼叫 Cerebro 物件的 run() 方法跑回測, 結果如下 : 

>>> %Run backtrader_test_1.py   
[*********************100%***********************]  1 of 1 completed
初始資金: 1000000.00
期末資金: 1000157.00




哈哈, 用這策略交易了一年才賺到 157 元!

如果納入手續費會更少, 例如 : 


# backtrader_test_2.py
import backtrader as bt
import yfinance as yf

class MyStrategy(bt.Strategy):
    def __init__(self):
        self.fast=bt.ind.SMA(self.data.close, period=5)
        self.slow=bt.ind.SMA(self.data.close, period=20)
        self.crossover=bt.ind.CrossOver(self.fast, self.slow)

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
        else:
            if self.crossover < 0:
                self.close()

# 建立 Cerebro 主引擎物件
cerebro=bt.Cerebro()

# 加上策略
cerebro.addstrategy(MyStrategy)

# 加上 K 線資料
df=yf.download('2330.TW', start='2024-01-01', end='2025-01-01', auto_adjust=True)
df.columns=df.columns.map(lambda x: x[0])  # 欄位單層化
data=bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)

# 設定初始資金
cerebro.broker.setcash(1_000_000)
print('初始資金: %.2f' % cerebro.broker.getvalue())

# 跑回測
cerebro.broker.setcommission(commission=0.001425)  # 設定手續費率
result=cerebro.run()

# 輸出期末資產總額
print('期末資金: %.2f' % cerebro.broker.getvalue())

# 畫圖
cerebro.plot()

回測結果如下 :

>>> %Run backtrader_test_2.py   
[*********************100%***********************]  1 of 1 completed
初始資金: 1000000.00
期末資金: 1000146.17

momo 買書 1 本 : Claude Code Vibe Coding 開發手冊

最近被明賢表弟說服, 考慮付費玩玩看 Claude Code, 上 momo 買了旗標這本 :





上回買蓉億記累積了 4170 元 momo 幣, 先用掉 170 元, 實付 422 元. 

2025年12月3日 星期三

申裝光世代 300M

高雄家的社區網路上周五停止服務, 水某無法看 Netflix, 我兩台樹莓派主機上的爬蟲也爬不出去, 我周一早上去中華電信申請安裝光世代網路, 原本想裝 100M 就好, 櫃台說與 300M 價錢沒差多少, 300M 還附全屋通 WiFi, 於是決定改裝 300M. 

原定今日下午安裝, 但早上接到工程師電話, 說剛好在住家大樓附近, 詢問是否方便安裝, 因下午有會議, 所以就請 1.5 小時假回家, 拉線安裝測試不到一小時就完成了, 實測確實有 300M :




下班先去做 QRV 定檢, 回來測試小米盒子連線 OK, 順便看了迪士尼頻道李鐘碩與文佳煐主演的 "瑞草洞", 哈哈, 好久沒看韓劇了. 

2025年12月2日 星期二

GitHub Copilot 初體驗

雖然現在我已使用 AI 協作方式產出程式碼與 debug, 主控權與選擇權仍然在我. 但是最近跟明賢表弟在 LINE 上交流軟體經驗後, 發現我在這部分似乎定格在石器時代了, 我原先對要花錢的 Cursor, GitHub Copilot, 或 Claude Code 等 Vibe coding 工具抱持觀望態度, 認為能透過 ChatGPT 交互問答得到答案又何必要花錢? 直到看過他展示目前的作業模式後, 我的態度與觀點就開始動搖了, 於是決定從 GitHub Copilot 著手來了解 Vibe coding. 

我上周末從母校圖書館借到下面這本書, 依照書中說明啟用 GitHub Copilot 功能 :


登入 GitHub 後, 按右上角人頭, 點選 "Copilot settings" :




在 GitHub Copilot 頁面中按 "Start using Copilot Free" :




出現交談頁面, 輸入提示詞按 Enter 或右方三角形按鈕提交詢問 :




預設模型是 GPT-5 mini, 點下拉式選單可選擇模型, 免費版只能選 GPT-5 mini, GPT 4.1, GPT 4o, 以及 Claude Haiku 4.1 四種模型 :




我用經典的九九乘法表來測試 : 




生成如下程式碼 : 




#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
multiplication_table.py
印出傳統的九九乘法表(1 到 9)
"""

def multiplication_table(n: int = 9) -> None:
    """印出 1..n 的乘法表,每列只顯示到當前被乘數(傳統九九表格式)。"""
    for i in range(1, n + 1):
        for j in range(1, i + 1):
            # i*j 以 2 個字元寬度對齊,方便看齊
            print(f"{j}×{i}={i*j:2}", end="  ")
        print()

if __name__ == "__main__":
    multiplication_table()

我把程式丟到 Thonny 去執行, 結果如下 :

>>> %Run multiplication_table.py  
1×1= 1  
1×2= 2  2×2= 4  
1×3= 3  2×3= 6  3×3= 9  
1×4= 4  2×4= 8  3×4=12  4×4=16  
1×5= 5  2×5=10  3×5=15  4×5=20  5×5=25  
1×6= 6  2×6=12  3×6=18  4×6=24  5×6=30  6×6=36  
1×7= 7  2×7=14  3×7=21  4×7=28  5×7=35  6×7=42  7×7=49  
1×8= 8  2×8=16  3×8=24  4×8=32  5×8=40  6×8=48  7×8=56  8×8=64  
1×9= 9  2×9=18  3×9=27  4×9=36  5×9=45  6×9=54  7×9=63  8×9=72  9×9=81  


看起來還不錯, 接下來改用難一點的, 叫它幫我寫一個用 pandas-ta 套件計算 DMI 指標並繪製 K 線圖的程式, 提示詞如下 :

"請用 pandas-ta 套件的函式 ta.adx() 寫一個計算趨向指標 DMI (Directional Movement Index), 並使用 kbar 套件繪製 K 線圖 (顯示成交量) 的 Python 程式"

生成的程式碼看起來有點複雜 : 

# dmi_with_pandas_ta_and_kbar.py
from __future__ import annotations
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

# plotting imports will be attempted lazily
import matplotlib.pyplot as plt

warnings.filterwarnings("ignore", category=FutureWarning)

def download_ohlcv(ticker: str = "AAPL", period: str = "6mo", interval: str = "1d") -> pd.DataFrame:
    """
    從 yfinance 下載 OHLCV 資料並回傳 DataFrame,index 為 DatetimeIndex。
    """
    df = yf.download(ticker, period=period, interval=interval, progress=False)
    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} {period} {interval}")
    # 確保欄位小寫且名稱為 open, high, low, close, volume
    df = df.rename(columns={c: c.lower() for c in df.columns})
    # 有些 yfinance 回傳 'adj close',確保 'close' 存在
    if "close" not in df.columns and "adj close" in df.columns:
        df["close"] = df["adj close"]
    return df[["open", "high", "low", "close", "volume"]]

def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 的 DataFrame accessor .ta.adx() 計算 ADX 與 DMP/DMN(+DI, -DI)。
    會 append 欄位到原始 DataFrame:
      - ADX_{length}
      - DMP_{length}  (通常視為 +DI)
      - DMN_{length}  (通常視為 -DI)
    另外建立便於使用的標準欄位: '+DI'、'-DI'、'ADX'
    """
    # pandas_ta 的 DataFrame accessor
    df = df.copy()
    df.ta.adx(length=length, append=True)  # 會生成 ADX_{len}, DMP_{len}, DMN_{len}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"
    if adx_col not in df.columns or dmp_col not in df.columns or dmn_col not in df.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本與使用方法。")
    # 方便使用的別名
    df["+DI"] = df[dmp_col]
    df["-DI"] = df[dmn_col]
    df["ADX"] = df[adx_col]
    return df

def plot_with_kbar_if_available(df: pd.DataFrame, ticker: str = "AAPL") -> None:
    """
    嘗試使用 kbar 繪圖;若失敗則退回到 mplfinance。
    kbar API 在不同版本可能有差異,所以這邊採用保守的嘗試方式:
    - 嘗試呼叫常見的 kbar.plot 或 kbar.kline.plot 等函式
    - 若都不存在或呼叫失敗,使用 mplfinance 繪圖
    """
    try:
        import kbar
    except Exception:
        kbar = None

    # 準備要在副圖上畫的線
    add_indicator_lines = {
        "+DI": df["+DI"],
        "-DI": df["-DI"],
        "ADX": df["ADX"],
    }

    # 嘗試 kbar 的常見畫法(多數 kbar 版本會接受 DataFrame)
    if kbar is not None:
        try:
            # 嘗試幾種常見的 kbar API 風格
            # 1) kbar.plot(df, ...) 或 kbar.kline.plot(...)
            if hasattr(kbar, "plot"):
                try:
                    # 這種呼叫是最簡短直觀的嘗試
                    kbar.plot(df, type="candle", volume=True, title=f"{ticker} - K 線 (kbar)")
                    # kbar.plot 不一定支援 overlay 指標,若需要可另外畫
                    # 畫 +DI, -DI, ADX 在新視窗
                    fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                    ax.plot(df.index, df["+DI"], label="+DI", color="green")
                    ax.plot(df.index, df["-DI"], label="-DI", color="red")
                    ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                    ax.set_title(f"{ticker} - +DI / -DI / ADX")
                    ax.legend()
                    plt.show()
                    return
                except Exception:
                    # 如果 kbar.plot 存在但執行失敗,嘗試其他 API
                    pass
            # 2) kbar.kline.plot
            if hasattr(kbar, "kline") and hasattr(kbar.kline, "plot"):
                kbar.kline.plot(df, volume=True, title=f"{ticker} - K 線 (kbar.kline)")
                fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                ax.plot(df.index, df["+DI"], label="+DI", color="green")
                ax.plot(df.index, df["-DI"], label="-DI", color="red")
                ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                ax.set_title(f"{ticker} - +DI / -DI / ADX")
                ax.legend()
                plt.show()
                return
            # 3) kbar.KLine 或其他類別介面
            if hasattr(kbar, "KLine"):
                try:
                    k = kbar.KLine(df)
                    if hasattr(k, "plot"):
                        k.plot(volume=True, title=f"{ticker} - K 線 (kbar.KLine)")
                        fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                        ax.plot(df.index, df["+DI"], label="+DI", color="green")
                        ax.plot(df.index, df["-DI"], label="-DI", color="red")
                        ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                        ax.set_title(f"{ticker} - +DI / -DI / ADX")
                        ax.legend()
                        plt.show()
                        return
                except Exception:
                    pass
            # 如果 kbar 存在但上述方法都失敗,拋出例外讓程式使用 mplfinance
            raise RuntimeError("kbar 已安裝但常見 API 呼叫失敗或不相容,改回使用 mplfinance。")
        except Exception as e:
            print("kbar 繪圖失敗,原因:", e, file=sys.stderr)
            print("將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # --- mplfinance 備援繪圖 ---
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # 準備 mplfinance 的 addplot
    ap = [
        mpf.make_addplot(df["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    # 長條圖顏色根據漲跌
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df,
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線 (mplfinance 備援)",
        figsize=(12, 8),
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def main():
    # 這裡可以改成從 CLI 參數讀入
    TICKER = "AAPL"
    PERIOD = "6mo"
    INTERVAL = "1d"
    ADX_LENGTH = 14

    print(f"下載 {TICKER} 的 OHLCV 資料 ({PERIOD}, {INTERVAL})...")
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
    print(f"資料筆數: {len(df)},樣本時間區間: {df.index[0]} ~ {df.index[-1]}")

    print(f"計算 ADX / DMI (length={ADX_LENGTH})...")
    df = compute_adx_dmi(df, length=ADX_LENGTH)

    # 顯示最後幾列結果以供檢視
    print(df[["open", "high", "low", "close", "volume", "+DI", "-DI", "ADX"]].tail(8))

    print("嘗試以 kbar 繪圖(若 kbar 不可用或 API 不相容,將退回到 mplfinance)...")
    plot_with_kbar_if_available(df, ticker=TICKER)


if __name__ == "__main__":
    main()

執行結果出現錯誤 : 

>>> %Run dmi_with_pandas_ta_and_kbar.py   
下載 AAPL 的 OHLCV 資料 (6mo, 1d)...
Traceback (most recent call last):
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 177, in <module>
    main()
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 163, in main
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in download_ohlcv
    df = df.rename(columns={c: c.lower() for c in df.columns})
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in <dictcomp>
    df = df.rename(columns={c: c.lower() for c in df.columns})
AttributeError: 'tuple' object has no attribute 'lower'

顯然它並不知道 yfinance 已經改版了, 新版的傳回值是多層 columns 結構, 必須先轉成傳統的單層 DataFrame, 我將此錯誤訊息回饋給它 :

"上面的程式執行出現如下錯誤, 請 debug 並修正"

但是仍然出現錯誤, 乾脆提供我之前寫的 AOBV 指標測試程式給它 :

"請參考下面 AOBV 指標的寫法 :
# pandas_ta_aobv.py
import yfinance as yf     
import pandas_ta as ta
from kbar import KBar  

df=yf.download('0050.TW', start='2024-11-06', end='2025-01-09', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0])
df.ta.aobv(close='Close', append=True)
kb=KBar(df)
kb.addplot(df['OBV'], panel=2, color='red', width=1.2, ylabel='OBV')
kb.addplot(df['AOBV_LR_2'], panel=3, color='red', width=1.2, ylabel='AOBV')
kb.addplot(df['AOBV_SR_2'], panel=3, color='blue', width=1.2)
kb.plot(volume=True, mav=(5, 10), title='K 線圖與 OBV, AOBV 指標')"

終於給出可以正確執行的程式碼 : 

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dmi_with_pandas_ta_and_kbar.py

說明:
- 參考你提供的 AOBV 範例寫法,使用 kbar.KBar 來繪製 K 線(顯示成交量)並把 +DI / -DI / ADX 加到副圖。
- 使用 pandas-ta 的 .ta.adx() 計算 ADX / +DI / -DI。
- 若欄位為 MultiIndex(yfinance 在某些情況會回傳),會取每個 tuple 的第一個元素,如同你的 AOBV 範例。
- 若 kbar 不可用,會退回到 mplfinance 繪圖(與前一版行為一致)。

用法:
python dmi_with_pandas_ta_and_kbar.py --ticker AAPL --period 6mo --interval 1d --length 14
或用 start/end:
python dmi_with_pandas_ta_and_kbar.py --ticker 0050.TW --start 2024-11-06 --end 2025-01-09 --interval 1d --length 14
"""
from __future__ import annotations
import argparse
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

warnings.filterwarnings("ignore", category=FutureWarning)


def download_ohlcv(ticker: str, period: str | None, start: str | None, end: str | None, interval: str) -> pd.DataFrame:
    """
    下載 OHLCV,並針對 MultiIndex 欄位做簡單 normalize(取 tuple[0])。
    最後確保至少有 Open/High/Low/Close/Volume (TitleCase 或 Close/close) 可用。
    """
    # yfinance: 若有 start/end 則通常不用 period
    if start or end:
        df = yf.download(ticker, start=start, end=end, interval=interval, auto_adjust=False, progress=False)
    else:
        df = yf.download(ticker, period=period or "6mo", interval=interval, auto_adjust=False, progress=False)

    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} (period={period}, start={start}, end={end}, interval={interval})")

    # 如果是 MultiIndex,像 (('Open',''),('High','')) 等情況,取 tuple 的第 0 個元素 (與你的 AOBV 範例相同)
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.map(lambda x: x[0])

    # 有時候欄位是小寫、有時是 Title Case,統一處理:
    cols = {c: str(c) for c in df.columns}

    # 若只有 'Adj Close' 而沒有 'Close',建立 Close 欄位(跟 AOBV 範例做法一致)
    if "Close" not in df.columns and "Adj Close" in df.columns:
        df["Close"] = df["Adj Close"]
    if "close" not in df.columns and "adj close" in [c.lower() for c in df.columns]:
        # handle lowercase adj close
        # find actual column name that lower() == 'adj close'
        for c in df.columns:
            if str(c).lower() == "adj close":
                df["close"] = df[c]
                break

    # 最後檢查至少能找到 High/Low/Close (或 high/low/close)
    found_high = any(c.lower() == "high" for c in df.columns)
    found_low = any(c.lower() == "low" for c in df.columns)
    found_close = any(c.lower() == "close" for c in df.columns)
    found_open = any(c.lower() == "open" for c in df.columns)
    found_volume = any(c.lower() == "volume" for c in df.columns)

    if not (found_high and found_low and found_close):
        raise RuntimeError(f"yfinance 回傳欄位缺少 high/low/close (available: {list(df.columns)})")

    # 為了方便 pandas-ta 與 kbar,建立常見 TitleCase 欄位 (Open/High/Low/Close/Volume) 如果尚未有
    def ensure_title(col_lower, title):
        if title not in df.columns:
            for c in df.columns:
                if str(c).lower() == col_lower:
                    df[title] = df[c]
                    return

    ensure_title("open", "Open")
    ensure_title("high", "High")
    ensure_title("low", "Low")
    ensure_title("close", "Close")
    ensure_title("volume", "Volume")

    return df


def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 計算 ADX/DMP/DMN,並增加 '+DI','-DI','ADX' 欄位 (以 length 為 suffix)。
    需要欄位 High/Low/Close(不論大小寫)。
    """
    # pandas-ta 在 DataFrame accessor 上運作,接受 high/low/close 欄位名稱參數
    # 我們先嘗試用 TitleCase 欄位 (High/Low/Close)(符合 yfinance 常態)
    df2 = df.copy()

    # 指定欄位名稱(若沒有 TitleCase,改用小寫)
    high_col = "High" if "High" in df2.columns else next((c for c in df2.columns if c.lower() == "high"), None)
    low_col = "Low" if "Low" in df2.columns else next((c for c in df2.columns if c.lower() == "low"), None)
    close_col = "Close" if "Close" in df2.columns else next((c for c in df2.columns if c.lower() == "close"), None)

    if not (high_col and low_col and close_col):
        raise RuntimeError("需要 high/low/close 欄位才能計算 ADX/DMI")

    # pandas-ta 的 ta.adx 可以接受參數 high/low/close
    df2.ta.adx(length=length, high=high_col, low=low_col, close=close_col, append=True)

    # pandas-ta 會建立 ADX_{length}, DMP_{length}, DMN_{length}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"

    if adx_col not in df2.columns or dmp_col not in df2.columns or dmn_col not in df2.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本 (生成 ADX_{len}, DMP_{len}, DMN_{len})")

    # 建立更方便的欄位名稱(與你先前程式一致)
    df2["+DI"] = df2[dmp_col]
    df2["-DI"] = df2[dmn_col]
    df2["ADX"] = df2[adx_col]

    return df2


def plot_with_kbar(df: pd.DataFrame, ticker: str = "AAPL", mav: tuple | None = (5, 10)):
    """
    依照 AOBV 的寫法使用 kbar.KBar:
      - kb = KBar(df)
      - kb.addplot(series, panel=..., color=..., width=..., ylabel=...)
      - kb.plot(volume=True, mav=(...), title=...)
    若 kbar 不可用,退回到 mplfinance。
    """
    try:
        from kbar import KBar
    except Exception as e:
        KBar = None
        print("kbar 無法匯入,將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # kbar 範例通常以 TitleCase 欄位 (Open/High/Low/Close/Volume)
    # 確保 DataFrame 中有 TitleCase 欄位,否則建立對應欄位
    df_plot = df.copy()
    for lower, title in (("open", "Open"), ("high", "High"), ("low", "Low"), ("close", "Close"), ("volume", "Volume")):
        if title not in df_plot.columns:
            for c in df_plot.columns:
                if str(c).lower() == lower:
                    df_plot[title] = df_plot[c]
                    break

    if KBar is not None:
        try:
            kb = KBar(df_plot)
            # +DI / -DI 放 panel=2,ADX 放 panel=3(參照你給的 AOBV 範例 panel 用法)
            kb.addplot(df_plot["+DI"], panel=2, color="green", width=1.2, ylabel="+DI")
            kb.addplot(df_plot["-DI"], panel=2, color="red", width=1.2)
            kb.addplot(df_plot["ADX"], panel=3, color="blue", width=1.2, ylabel="ADX")
            kb.plot(volume=True, mav=mav, title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX)")
            return
        except Exception as e:
            print("使用 kbar 繪圖時發生錯誤,將退回到 mplfinance。錯誤:", e, file=sys.stderr)

    # mplfinance 備援繪圖
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # mplfinance 需要 TitleCase 欄位
    df_mpf = df_plot.copy()

    ap = [
        mpf.make_addplot(df_mpf["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df_mpf["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df_mpf["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df_mpf.rename(columns={c: (c if isinstance(c, str) else str(c)) for c in df_mpf.columns}),
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX) (mplfinance)",
        figsize=(12, 8),
        mav=mav,
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def parse_args():
    p = argparse.ArgumentParser(description="計算 DMI (ADX/+DI/-DI) 並繪製 K 線 (kbar 或 mplfinance)")
    p.add_argument("--ticker", "-t", default="AAPL", help="股票代號,例如 AAPL 或 0050.TW")
    p.add_argument("--period", "-p", default="6mo", help="若未指定 start/end,使用 yfinance period (ex: 6mo, 1y)")
    p.add_argument("--start", help="開始日期 (YYYY-MM-DD),若指定則使用 start/end 而非 period")
    p.add_argument("--end", help="結束日期 (YYYY-MM-DD)")
    p.add_argument("--interval", "-i", default="1d", help="yfinance interval,例如 1d, 1h")
    p.add_argument("--length", "-l", type=int, default=14, help="ADX 計算長度 (預設 14)")
    p.add_argument("--mav", nargs="*", type=int, default=(5, 10), help="移動平均線 mav,例如 --mav 5 10")
    return p.parse_args()


def main():
    args = parse_args()
    try:
        print(f"下載 {args.ticker} 的 OHLCV 資料 (period={args.period}, start={args.start}, end={args.end}, interval={args.interval})...")
        df = download_ohlcv(args.ticker, args.period, args.start, args.end, args.interval)
        print(f"資料筆數: {len(df)},時間區間: {df.index[0]} ~ {df.index[-1]}")
        print(f"計算 ADX / DMI (length={args.length})...")
        df2 = compute_adx_dmi(df, length=args.length)
        # 顯示最後幾列確認
        display_cols = [c for c in ["Open", "High", "Low", "Close", "Volume", "+DI", "-DI", "ADX"] if c in df2.columns]
        print(df2[display_cols].tail(8))
        print("繪圖(優先使用 kbar,如不可用則使用 mplfinance)...")
        plot_with_kbar(df2, ticker=args.ticker, mav=tuple(args.mav) if args.mav else None)
    except Exception as e:
        print("執行發生錯誤:", e, file=sys.stderr)
        raise

if __name__ == "__main__":
    main()

執行結果 :




唉, 明明 11 行程式碼就能搞定, 為何要寫得這麼複雜呢? 


WiFi 無線網路信號強度測量

最近打算著手建造鄉下老家門口安裝智慧信箱監控器, 週日下午先進行了 WiFi 網路的信號強度測試, 紀錄如下. 

目前老家有兩個無線網路, 一個光世代升 300 MB 時購置的 WiFi 6 ASUS AX3000 無線基地台, 另一個是很久以前的 Edimax WiFi 基地台 (仍保留繼續使用). 此外手中還有 Toto 與穿牆王這兩塊 WiFi 延伸器. 我本來打算用穿牆王插在前門插座上, 將 AX3000 訊號延伸至大門口, 測試結果雖然穿牆王確實強度比距離較遠的 AX3000 高了 10 幾 dBm, 但速率卻遠遠比不上 AX3000. 


測試一 : 在穿牆王 WiFi 延伸器附近測量





強度 -33dBm, 上下載速率 10 幾 MB. ASUS WiFi 6 基地台 2.4G 強度比它少了近 30 dBm, 約為 -62 dBm. 


測試二 : 在大門口測量穿牆王 WiFi 延伸器信號




在距離延伸器約 10 公尺的大門口, 信號約衰減了 20 dBm, 來到 -53dBm, 速率還是差不多 10 幾 MB. ASUS WiFi 6 基地台 5G 強度還有 -67 dBM, 而 2.4G 強度則衰減了 9 dBm, 約 -71 dBm. 


測試三 : 在大門口測量 ASUS WiFi  6 基地台信號

最後切換連線改到 ASUS WiFi 6 基地台, 此處 5G 信號還有 -65dBm, 而 2.4G 信號較弱, 為 -70dBM, 實際連線為較強的 5G : 




測試速率下載居然有 170MB, 上傳有 60MB : 




結論當然是拔掉穿牆王, 直接連線 ASUS WiFi 6 啦!

2025年11月30日 星期日

2025 年第 48 周記事

周五下班時遇到樓上張先生, 突然告知社區網路要結束了, 當天已斷網, 我驚訝地問原因, 原來參加的用戶只剩五戶, 應該是入不敷出, 再經營下去會賠錢了. 雖然社區網速不高, 大概只有 30MB, 但看 Netflix 與上網下載都夠用, 且每個月才 250 元而已, 還算堪用啦, 只是偶而路由器故障斷網要等張先生回家處理才行有點小困擾. 替代方案就是用光世代了, 我問中華電信客服目前 100M 優惠價每月 799 元, 下周臨櫃申請安裝.  

申請居家服務到府煮菜後, 我週日上市場購買周一至周五的食材, 固定買魚與三塊豆腐, 以及青菜, 週三下班回鄉下一趟, 主要是煮新的一鍋飯, 並清點冰箱食材, 將周四周五的肉品從冷凍移到冷藏. 小舅周三來菜園遇到我剛好要上班, 說不用跑一趟, 需要青菜或白飯他可以帶過來, 他樓上也有種菜, 但我又不想太麻煩他, 其實一周的中間回一趟老家其實時間也不趕. 

過去一個多月由於事情多都沒去河堤快走, 周四終於有空去運動, 路上遇到一對老夫婦叫住我 (他們可能以前有注意到我幾乎每天會去運動兼餵貓), 問我有沒有看到河堤小咪, 我說已經超過一個月沒來河堤了, 他說河堤小咪好像失蹤了, 都沒看到了, 又說明誠橋這邊的黑貓也被一隻毒蛇咬死了, 所以最近這一帶都沒半隻貓咪. 我走了兩圈呼喚牠果真都沒蹤影, 讓我好失望. 我已經餵牠一年多了, 我去河堤有時並非為了運動 (有時很累或很懶), 而是餵貓. 

自從 10 月官升一級後工作就比較忙了, 本周起已被通知要接第一主管代理人, 之前代理的學長得知我升官訊息後, 迫不及待地向上面要求要卸除第一代理人, 於是我就從第二代理人變成第一代理人了. 批假單是還好啦, 問題是主管不在時我得代理參加各種會議很令我頭疼, 我最討厭開會了. 

今天早上終於看到鄉下的貓婆婆小灰回來了, 自從她女兒毛小妹生了四隻小貓後, 似乎就讓出地盤出外去流浪 (以前幾乎都在家), 只有餓了時才回家吃乾糧. 但最近兩周都沒遇到她, 我以為再也不回來了. 今天看到她時她正要穿過菜園離開, 我叫住她問她要不要吃飯, 她看我去倒乾糧就走回來吃了一陣, 我想到冰箱有已開的貓罐罐, 進去舀了一盤四湯匙出來, 她看了看盤子後就跳下田消失在荒草中. 





傍晚去菜園巡了一下芭樂樹, 發現被小舅修枝過後已開花, 已結出四顆果實, 趕緊將其套袋起來, 我已好久沒吃到芭樂了 :




本周的學習研究放在 pandas-ta 的測試, 覺得此套件比 Ta-Lib 還好上手. 最近與明賢表弟 LINE 聊天時得知他最近在用 Claude Code 開發量化投資軟體, 並勸我放棄用詢問 AI 協作的 coding 方式, 因為時間比較寶貴, 利用 AI 可以快速且專業地完成想做的專案, 我前陣子才把 GitHub Copilot 書籍還回圖書館哩! 但見識到他所展示的成果也認同了他的論點, 唉, 軟體工程真的要走入歷史了 (y資工的前景有點 ...), Vibe coding 讓人人都能當程式員了. 

2025 剩下一個月了, 希望年底前能把 LINE Bot 的學習結束掉. 

2025年11月27日 星期四

Python 學習筆記 : 技術分析套件 pandas-ta 用法 (九)

本篇旨在測試 pandas-ta 套件的趨向指標 DMI (Directional Movement Index) 的 ta.adx() 函式與 df.ta.adx() 方法. 

本系列全部文章索引參考 :



11. 計算 DMI (Directional Movement Index) 指標 :  

DMI 指標是美國著名的技術分析大師 Welles Wilder 在 1978 年出版的 "New Concepts in Technical TRading Systems" 一書中所提出 (他也是 RSI 與 ATR 指標的發明人), 是唯一具有最高價, 最低價, 與收盤價相對關係, 可分開探討多空力道, 完整分析股價波動情形的指標. DMI 的核心概念是透過比較每天最高價與最低價的變動來了解買賣力量誰佔優勢, 能在多空循環中探求價格上升與下降的均衡點, 適時引導投資人進場或出場. 

DMI 由三個主要組件組成, 通常在圖表上以三條線呈現, 摘要整理如下 : 
  • 正向趨向指標 (+DI) : 
    用途 : 衡量多頭力量或向上的壓力 
    意義 : +DI 線上升時表示價格向上移動的強度增加
  • 負向趨向指標 (-DI) :
    用途 : 衡量空頭力量或向下的壓力
    意義 : -DI 線上升表示價格向下移動的強度增加
  • 平均趨向指數 (ADX) :
    用途 : 衡量整體趨勢的強度 (或稱動能方向無關。
    意義 : 這是 DMI 系統中最關鍵的指標. ADX 線上升只表示趨勢 (無論漲跌) 正在變強, ADX 線下降則表示趨勢正在減弱或市場進入盤整. 注意, ADX 只代表趨勢強弱, 不判斷多空方向.
首先計算方向移動 (Directional Movement) +DM 與 -DM, 公式如下 : 

+DM = max(High_today − High_yesterday, 0)
−DM = max(Low_yesterday − Low_today, 0)

若 +DM < −DM 則使用 −DM 
若 −DM < +DM 則使用 +DM 
若同時不成立, 則 +DM 與 −DM 設為 0

其次計算真實波幅 TR (True Range) :

TR = max(
    High_today − Low_today,
    abs(High_today − Close_yesterday),
    abs(Low_today − Close_yesterday)
)

然後用 +DM, -DM, 與 TR 計算方向指標 DI (Directional Indicator) :

 +DI = ( 平滑(+DM) / 平滑(TR) ) × 100
−DI = ( 平滑(−DM) / 平滑(TR) ) × 100

最後計算平均趨向指數 ADX (Average Directional indeX) :

DX = abs(+DI − −DI) / (+DI + −DI) × 100
ADX = DX 的平滑移動平均(常用 14 天)

DMI 指標的 +DI 與 -DI 線的相對位置提供了趨勢方向的交易信號 : 


 +DX 與 -DX 位置  趨勢  交易信號
 +DI > -DI 多頭趨勢 買盤力量佔優,可做多或持有多頭倉位
 -DI > +DI 空頭趨勢 賣盤力量佔優,可做空或持有空頭倉位
 DI 交叉 趨勢發生轉變 +DI 上穿 -DI 為買入;-DI 上穿 +DI 為賣出


DMI 指標的 ADX 線數值用來判斷當前市場狀態並選擇適合的策略 : 


 ADX 值  市場狀態  策略
 ADX > 25 強勢趨勢 適合順勢交易、趨勢追蹤或突破策略
 ADX 介於 20–25 趨勢形成中 (中等強度) 需謹慎確認,趨勢可能正在醞釀
 ADX < 20 弱勢趨勢 (盤整) 避免順勢交易,可使用震盪指標策略


整合 +DX, -DX, 與 ADX 交易信號可得下表之交易策略 : 


 指標訊號  市場狀態  交易策略與建議
+DI 上穿 −DI
(黃金交叉)
ADX > 25 強勢多頭 多頭進場訊號成立, 可偏積極布局, 適合加碼與順勢操作
ADX 介於 20–25 趨勢醞釀 可小量佈局做多, 須等待 ADX 持續上升確認趨勢避免躁進
ADX < 20 弱勢/盤整 不建議進場, 多頭訊號容易失敗, 適合等待突破或震盪策略
−DI 上穿 +DI
(死亡交叉)
ADX > 25 強勢空頭 空頭進場訊號成立, 可做空或減碼持股, 適合追空與破底策略
ADX 介於 20–25 趨勢醞釀 可小量部位做空, 須等 ADX 持續升高確認空頭行情
ADX < 20 弱勢/盤整 不建議追空, 行情可能反覆震盪, 適合區間交易策略


pandas-ta 套件提供 ta.adx() 計算 DMI 指標, 傳回值均為 DataFrame 物件. 下面程式碼利用 yfinance 取得台股 0050 價量資料來計算 DMI 指標, 並於 K 線圖的副圖中繪製 +DI, -DI, 與 ADX 這 3 條曲線 :

# pandas-ta-dmi.py
import yfinance as yf
import pandas_ta as ta
from kbar import KBar
import pandas as pd

#=========================
# 下載資料
#=========================
df=yf.download('0050.TW', start='2024-11-06', end='2025-01-09', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0] if isinstance(x, tuple) else x) # 欄位單層化

#=========================
# 計算 DMI 系統 (+DI, -DI, ADX)
# pandas-ta 預設 length=14 天
#=========================
# ta.adx() 函數會返回包含 ADX, +DI (DMP), -DI (DMN) 的 DataFrame
dmi_df=ta.adx(
    high=df['High'],
    low=df['Low'],
    close=df['Close'],
    length=14 # 使用預設 14 週期
    )

# 將 DMI 結果合併回主 DataFrame
# 欄位名稱預設為 ADX_14, DMP_14, DMN_14
df=df.join(dmi_df) 

#=========================
# 使用 pandas 創建常數序列模擬水平線 
#=========================
# 創建數值為 20 的常數序列 (用於模擬水平線)
df['HLine_20'] = pd.Series(20, index=df.index)
# 創建數值為 25 的常數序列
df['HLine_25'] = pd.Series(25, index=df.index)

#=========================
# 建 KBar + 繪圖設定
#=========================
kb=KBar(df)

# Panel 2 ➜ DMI 系統
# 繪製 ADX (趨勢強度,通常設為黑色或較粗線)
kb.addplot(df['ADX_14'], panel=2, color='black', width=1.5, ylabel='DMI / ADX')

# 繪製 +DI (多頭力量,綠色)
kb.addplot(df['DMP_14'], panel=2, color='green', width=1.0)

# 繪製 -DI (空頭力量,紅色)
kb.addplot(df['DMN_14'], panel=2, color='red', width=1.0)

# *** 繪製參考線 (使用常數序列) ***
kb.addplot(df['HLine_20'], panel=2, color='gray', linestyle='--', width=0.8)
kb.addplot(df['HLine_25'], panel=2, color='gray', linestyle='--', width=0.8)

#=========================
# 畫圖
#=========================
kb.plot(
    volume=True,      # Panel 1=Volume
    mav=(5, 10),      # K線主圖疊 5 & 10 MA
    title='K 線圖與 DMI (趨向指標) 系統'
    )

結果如下 :




可見 ADX 強度 (左側 Y 軸) 都在 10~12 之間, 遠低於 20, 表示趨勢不明顯, 股價處於盤整狀態, 市場處於弱勢震盪狀態, 交易者應避免使用強勢的趨勢追蹤策略. 

2025年11月26日 星期三

Python 學習筆記 : 技術分析套件 pandas-ta 用法 (八)

本篇旨在測試 pandas-ta 套件的 CO (Chaikin Oscillator, 柴京震盪) 指標與 CMF (Chaikin Money Flow, 柴京資金流) 指標函式 ta.adosc() 與 ta.cmf(). 

本系列全部文章索引參考 :



10. 計算 CO (Chaikin Oscillator) 與 CMF (Chakin Money Flow) 指標 :  

華爾街著名的技術分析師 Marc Chaikin 於 1960~1980 年代陸續發明了 A/D, CMF, 與 CO 等技術指標, 成為技術分析從價格導向轉向成交量導向的領航人物, 其核心理念為 "量先價行" (成交量領先於價格, volume precedes price), 認為在股價大幅波動之前, 通常會先看到主力資金的進出. 

最早將成交量與價格漲跌結合的指標是 Joe Granville 於 1963 年提出的 OBV (能量潮) 指標, 但 OBV 只看當天收盤價是漲還是跌, 卻忽略了當天價格在 "日內波幅" 中的位置 (例如收盤價雖然是漲了, 但如果是收在當天最低點附近的話其實是弱勢). Marc Chaikin 看到了 OBV 的這個缺點, 便於 1966 年提出了 A/D 指標, 用來判斷市場是 "買盤吸收" 還是 "賣壓出貨". 

但是 A/D 是一個累積性指標, 其值沒有固定的上下限, 可以無限增加或減少, 這使其難以進行標準化比較, 無法單憑 A/D 的數值來判斷當前的買盤壓力是強勁還是微弱. 而且 A/D 對短期的價格變化反應緩慢, 它只呈現資金總量是增加還是減少, 無法告訴我們資金流動的動能是否正在加速或減速. 

Marc Chaikin 後來在 A/D 的基礎上提出了 CO 和 CMF 指標用來解決 A/D 指標的局限性, 他引入類似 MACD 的概念, 將 A/D 進行快速與慢速 EMA 差分 (3 日 EMA 減去 10 日 EMA), 提出了 Chaikin Oscillator (CO) 指標, 解決 A/D 缺乏短期動能信號問題, 透過背離可以提前捕捉趨勢的反轉, 從而預測價格趨勢的轉折, 成為一個領先的動能指標. 

另外 Marc Chaikin 還設計了 Chaikin Money Flow (CMF) 指標, 透過將資金流動成交量除以總成交量使 CMF 數值被限制在 +1 到 -1 之間, 解決了 A/D 缺乏標準化與壓力強度衡量的問題, 讓交易者可以設定閾值 (如 +0.25 或 -0.25) 輕鬆判斷機構資金流向的持續性和強勁度. 注意, CMF 並非累加, 而是在固定期間 (例如 20 天或 21 天) 計算資金流入/流出比例, 可用來觀察趨勢轉折和背離. 

不過他提出 CO 和 CMF 的原因並不是要否定 A/D 的價值, 而是將 A/D 的原始資料進行二次加工, 使其具備動能分析 (CO) 和強度分析 (CMF) 的功能, 從而形成一個更全面更具實戰意義的資金流分析體系. 簡言之, A/D 用來計算累積資金流動的總量; CMF 用來測量資金流動的強度 (有界區間震盪指標); 而 CO 則是用來測量 A/D 累積總量的動能 (無界震盪指標). 

CMF 是基於 A/D 加權的流動資金比率, 反映資金在一段期間內的進出強弱, 公式如下 :

先計算出 MFM :

MFM = ((Close − Low) − (High − Close)) / (High − Low)
    = ((Close − Low) / (High − Low) × 2 − 1)

再計算 CMF :

CMF(n) = (∑(MFM × Volume) 過去 n 根) / (∑ Volume 過去 n 根)

CO 為 A/D 指標的 MACD 化振盪版本, 先定義兩條 A/D 移動平均線, 再比較短期與長期 A/D 平滑趨勢差異 : 

先計算 A/D 值 : 

A/D = ∑ (MFM × Volume)

再計算 A/D 的長短期 EMA 後求其差分 :

EMA_s = A/D 的短期 EMA (通常 s = 3)
EMA_l = A/D 的長期 EMA (通常 l = 10)
CHO = EMA_s(A/D) − EMA_l(A/D)

pandas-ta 套件提供 ta.adosc() 與 ta.cmf() 函式分別計算 CO 與 CMF 指標, 傳回值均為 Series 物件. 下面程式碼利用 yfinance 取得台股 0050 價量資料來計算 CO 與 CMF 指標, 並於 K 線圖的副圖中分別繪製這兩條曲線 :

# pandas-ta-co-cmf.py
import yfinance as yf
import pandas_ta as ta
from kbar import KBar

#=========================
# 下載資料
#=========================
df=yf.download('0050.TW', start='2024-11-06', end='2025-01-09', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0])  # 欄位單層化

#=========================
# 計算 Chaikin Oscillator(CO)
# pandas-ta 預設 fast=3 day, slow=10 day
#=========================
df['CO']=ta.adosc(
    high=df['High'],
    low=df['Low'],
    close=df['Close'],
    volume=df['Volume']
    )

#=========================
# 計算 Chaikin Money Flow(CMF)
# pandas-ta 預設 length=20 天
#=========================
df['CMF']=ta.cmf(
    high=df['High'],
    low=df['Low'],
    close=df['Close'],
    volume=df['Volume']
    )

#=========================
# 建 KBar + 繪圖設定
#=========================
kb=KBar(df)

# Panel 2 ➜ CO(類似 A/D 的 MACD)
kb.addplot(df['CO'], panel=2, color='darkorange', width=1.2, ylabel='CO')

# Panel 3 ➜ CMF(判斷資金多空比)
# 設顏色:>0 用綠、<0 用紅
cmf_colors=['g' if v >= 0 else 'r' for v in df['CMF']]
kb.addplot(df['CMF'], panel=3, type='bar', color=cmf_colors,
           alpha=0.4, ylabel='CMF')

#=========================
# 畫圖
#=========================
kb.plot(
    volume=True,       # Panel 1=Volume
    mav=(5, 10),       # K線主圖疊 5 & 10 MA
    title='K 線圖與 CO, CMF 指標'
    )

結果如下 :




從觀察 CO 和 CMF 曲線變化可知 : 
  • 早期確認信號 :
    12 月上旬時 CO 率先穿越零軸, 發出動能轉強訊號; 隨後 CMF 轉正, 確認累積強度, 證明價格上漲是健康的.
  • 後期警示信號 :  
    2025 年初雖然價格創下新高, 但 CMF 迅速轉負 (表示強度不足), 且 CO 動能快速衰退並轉負 (速度減慢), 兩者同時發出強烈的資金背離警訊, 提示交易者應警惕潛在的回調風險. 
總之, CMF 的核心意義反映某期間多空資金佔比強弱, 表示趨勢偏向多或空; 而 CO 則顯示資金動能是加速或減速, 可用來抓轉折訊號. 

CO 指標的核心在於測量 A/D 線的資金流動動能而非價格動能, 因此它常被視為一個領先指標 (Leading Indicator), 用於提前預測趨勢的轉折. CO 的用法如下 : 
  • 從 0 軸穿越確認趨勢方向 : 
    當 CO 穿越 0 軸由負轉正, 表示買盤動能開始主導, 確認上漲趨勢或反彈, 適合順勢做多; 當 CO 由正轉負表示賣盤動能開始主導, 確認下跌趨勢或回調, 適合減倉或做空. 
  • 從動能背離預測反轉轉折 : 
    價格創新低但 CO 未創新低, 表示資金流出動能已減弱, 預示底部即將反轉, 這是高勝率的買入信號; 當價格創新高但CO 未創新高, 表示資金流入動能已衰竭, 預示頂部即將反轉, 這是高勝率的賣出信號. 
  • 從極值與回歸評估市場情緒 : 
    當 CO 達高點後回落, 表示資金累積動能達到極致, 須需警惕市場超買或動能放緩; 當 CO 達低點後回升, 表示資金派發動能達到極致, 須警惕市場超賣或動能轉強, 要觀察是否出現零軸穿越. 
CO 被認為是最高品質的反轉訊號之一, 當價格創下新高, 但 CO 卻創下更低的波峰, 這不僅是價格動能減弱, 更是實質的機構投資人資金流入動能減弱. 當價格上漲中卻出現 CO 背離, 通常意味著主力資金已經停止追捧或正在派發 (抽離資金).