## 回測環境開發流程
#### <font style="color: #30598c;">🐞完整程式碼範例</font>
<a class="button" href="https://drive.google.com/file/d/1UonyRRj5Q1zMkxv8wbVrLofDWI7r_8de/view?usp=sharing">
<font class="button_text">程式碼下載</font>
</a>
</br>
#### 1. 下載歷史股價
```python
def fetch_history_data(stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
"""
獲取歷史股價資料
"""
# 透過 yfinance 下載歷史股價
source_data = yf.download(tickers=f"{stock_code}.TW", start=start_date, end=end_date, auto_adjust=False)
# 回傳下載後的資料
return source_data
```
</br>
#### 2. 調整資料欄位 (def format_data_columns)
```python
def format_data_columns(data: pd.DataFrame) -> pd.DataFrame:
"""
格式化資料欄位
"""
# 取出欄位名稱的最上層
data.columns = data.columns.get_level_values(0)
# 去掉欄位名稱 (Price)
data.columns.name = None
# 重設索引,將 Date 從 index 轉成欄位
pre_data = data.reset_index()
# 依需求的欄位順序
pre_data = pre_data[["Date", "Open", "High", "Low", "Close", "Volume"]]
# 回傳格式化後的資料
return pre_data
```
#### <font style="color: #30598c;">🐞如果沒有辦法成功透過 `yfinance` 獲取資料,點擊連結下載</font>
<a class="button" href="https://drive.google.com/file/d/1zX0sczmbLkC35UcbQ72Loodo-P_7guhJ/view?usp=sharing">
<font class="button_text">歷史資料下載</font>
</a>
</br>
#### 3. 計算技術指標 (def fetch_TA_data)
```python
def fetch_TA_data(data: pd.DataFrame) -> pd.DataFrame:
"""
計算技術指標
"""
# 計算移動平均線
data["MA10"] = data["Close"].rolling(window=10).mean()
data["MA60"] = data["Close"].rolling(window=60).mean()
# 四捨五入到小數點後兩位
ta_df = data.round(2)
# 回傳包含技術指標的資料
return ta_df
```
</br>
#### 4. 資料逐筆回傳 (def recieve_kBar)
```python
# 將資料逐筆拆分給回測系統
for index, row in ta_data.iterrows():
# 將 Series 轉成 DataFrame 格式
chart = row.to_frame().T.reset_index(drop=True)
# 將資料傳遞給回測系統
run_backtest(chart)
```
</br>
#### 5. 新增接收行情通道
```python
def receive_kBar(chart: pd.DataFrame):
"""
執行回測
"""
# 打印行情資訊
cs.print("接收行情資訊:", style="bold yellow")
cs.print(f"{chart}\n")
```
</br>
#### 6. 判斷交易信號 (fetch_trade_singnal)
```python
# 交易信號: Buy, Sell, Clear, Hold
trade_signal = "Hold"
```
```python
def fetch_trade_signal(chart: pd.DataFrame):
"""
獲取交易信號
"""
global trade_signal, trade_signal_data, trade_order
# 將最新 k 棒資料加入交易信號資料集
trade_signal_data = pd.concat([trade_signal_data, chart], ignore_index=True)
# 確保資料數大於 2 以便後續判斷
if len(trade_signal_data) < 2:
trade_signal = "Hold"
return
# 僅保留最近兩筆資料
trade_signal_data = trade_signal_data.tail(2)
# 擷取今日、昨日 k 棒資訊
curr_date = trade_signal_data.iloc[-1]["Date"]
prev_close = trade_signal_data.iloc[0]["Close"]
curr_close = trade_signal_data.iloc[-1]["Close"]
prev_ma10 = trade_signal_data.iloc[0]["MA10"]
curr_ma10 = trade_signal_data.iloc[-1]["MA10"]
curr_ma60 = trade_signal_data.iloc[-1]["MA60"]
# 更新委託單資訊
trade_order["date"] = curr_date.strftime("%Y-%m-%d")
trade_order["price"] = float(curr_close)
# 判斷買賣訊號
if curr_close < curr_ma60:
trade_signal = "Clear"
elif (prev_close <= prev_ma10) and (curr_close > curr_ma10) and (position_count == 0):
trade_signal = "Buy"
elif (prev_close >= prev_ma10) and (curr_close < curr_ma10) and (position_count > 0):
trade_signal = "Sell"
else:
trade_signal = "Hold"
```
</br>
#### 7. 執行交易 (trading)
```python
# 建立委託單格式
trade_order = {"action": None, "price": 0, "quantity": 0, "fee": 0, "tax": 0, "ar": 0, "date": None}
```
```python
def trading() -> str:
"""
執行交易
"""
global position_count
# 根據交易信號決定交易動作
if trade_signal == "Buy":
action = "Buy"
trade_order["quantity"] = 1
elif trade_signal == "Sell":
action = "Sell"
trade_order["quantity"] = 1
elif trade_signal == "Clear" and position_count > 0:
action = "Sell"
trade_order["quantity"] = position_count
else:
action = "Hold"
return action
# 更新委託單資訊
trade_order["action"] = action
# 計算委託價金
cost = trade_order["price"] * trade_order["quantity"] * 1000
# 計算交易稅
if action == "Buy":
trade_order["tax"] = 0
else:
trade_order["tax"] = int(cost * 0.003)
# 計算手續費
trade_order["fee"] = int(cost * 0.001425)
trade_order["ar"] = cost + trade_order["fee"] + trade_order["tax"]
# 紀錄整張委託單
trade_order_list.append(trade_order.copy())
# 更新庫存數量
if trade_signal == "Buy":
position_count += 1
elif trade_signal == "Sell":
position_count -= 1
else:
position_count = 0
# 回傳交易動作
return action
```
</br>
#### 8. 更新損益紀錄
```python
# 建立損益紀錄格式
trade_record = {"trade_count": {"Buy": 0, "Sell": 0, "Total": 0}, "pnl": {"Realized": 0, "Unrealized": 0}, "pnl_count": {"Profit": 0, "Loss": 0, "Total": 0}, "transaction_cost": {"Fee": 0, "Tax": 0, "Total": 0}}
```
```python
def update_pnl_record(action: str):
"""
更新交易、損益資訊
"""
global backtest_capital, trade_record
# 更新回測本金
if action == "Buy":
backtest_capital -= trade_order["ar"]
elif action == "Sell":
backtest_capital += trade_order["ar"]
else:
return
# 更新交易次數
trade_record["trade_count"][action] += 1
trade_record["trade_count"]["Total"] = trade_record["trade_count"]["Buy"] + trade_record["trade_count"]["Sell"]
# 更新已實現損益紀錄
if action == "Buy":
trade_record["pnl"]["Unrealized"] += trade_order["ar"]
# 更新未實現損益紀錄
else:
curr_realized = (trade_order["ar"] - trade_record["pnl"]["Unrealized"])
trade_record["pnl"]["Realized"] += curr_realized
trade_record["pnl"]["Unrealized"] = 0
# 更新獲利/虧損次數
if curr_realized > 0:
trade_record["pnl_count"]["Profit"] += 1
elif curr_realized < 0:
trade_record["pnl_count"]["Loss"] += 1
trade_record["pnl_count"]["Total"] = trade_record["pnl_count"]["Profit"] + trade_record["pnl_count"]["Loss"]
# 更新交易成本
trade_record["transaction_cost"]["Fee"] += trade_order["fee"]
trade_record["transaction_cost"]["Tax"] += trade_order["tax"]
trade_record["transaction_cost"]["Total"] = trade_record["transaction_cost"]["Fee"] + trade_record["transaction_cost"]["Tax"]
```
<style>
.tips {
display: flex;
gap: 2px;
padding-bottom: 16px;
}
.tips-content {
font-family: "Noto Sans TC";
color: #3f628a;
font-weight: 600;
}
.button {
display: flex;
justify-content: center;
background-color: #3f628a;
padding: 3px 5px 5px 5px;
width: 20%;
border-radius: 6px;
}
.red_btn {
background-color: #b00d0d;
}
.button_text {
font-family: "Noto Sans TC";
font-weight: 500;
color: #FFFFFF;
}
</style>