# 專案概述
預測個股未來是否為當沖股
* 專案目標:將當沖熱門股的數據包裝成產品,販賣給客戶
* 例如:券商可透過當沖熱門股預測結果,告訴散戶,吸引散戶當沖
* 專案任務:開發一個模型來預測個股未來幾天是否會成為當沖熱門股
* 額外可以知道,那些因子會影響成為當沖熱門股
* 專案受眾:
* 主要:法人(券商等)
* 次要:當沖投資者等
## 模型概述
* 輸入資料:個股歷史交易數據、技術指標、基本面資料等。
* 輸出結果:個股是否為當沖股的標籤。
### 功能模組
```
資料收集與清洗模組
特徵工程模組
預測模型模組
可視化與報表模組
```
### 技術架構
開發語言:Python
主要工具與框架:
資料處理:Pandas、NumPy
機器學習:隨機森林
# 模型製作
開發一個模型來預測個股未來1天是否會成為當沖熱門股
## 當沖股定義與判斷方法
當日的當沖成交量排名前 **n** 名的個股被視為當沖股。其中,**n** 的取值由「Knee Point」方法決定。

### Knee Point 的定義
Knee Point 是在一組數據中,曲率最大的位置,表示數據趨勢發生顯著變化的點。
在本專案中,透過將每個交易日中所有個股的當沖成交量由高到低排序,找出當沖成交量「爆發上升」的臨界點(Knee Point),作為 **n** 的值。

### 演算法來源
我們採用了開源工具 [kneed](https://github.com/arvkevi/kneed) 來實現 Knee Point 的判定。該工具提供了可靠的曲率計算方法,適合用於大規模數據的分析。
### 應用步驟
1. 收集當日所有個股的當沖成交量。
2. 按照當沖成交量進行降序排序。
3. 利用 Knee Point 演算法,找出曲率最大的位置,並確定排名值 **n**。
4. 將排名前 **n** 的個股標記為當沖股。
## 資料準備
```
數據來源:歷史交易數據(如開高低收成交量、當沖比例)、市場指數、基本面數據。
數據格式:CSV、JSON、API 返回格式。
資料處理過程:缺失值處理、異常值檢測、標準化。
```
### **數據來源**
透過WCFAdox,取自CMoney資料庫的歷史資料
- **歷史交易數據**:包含每日交易的開高低收、成交量、當沖成交量與非當沖成交量。
- **基本面數據**:如本益比、成交量變動百分比、產業分類等。
- **技術指標數據**:如各種技術分析指標及衍生指標。
- **特徵工程**:基於歷史資料生成可能有用的特徵
時間長度:
2023/01~2024/10
| 資料表 | 欄位 |
| ----- | --- |
| 上市櫃公司基本資料 | 年度,股票代號,股票名稱,產業名稱,產業指數名稱,指數彙編分類 |
| 日收盤表排行 | 日期,股票代號,開盤價,最高價,最低價,收盤價,漲跌,漲幅(%),振幅(%),漲跌停,成交量,成交量變動(%),成交筆數,總市值(億),本益比,股價淨值比,週轉率(%) |
| 日當日沖銷交易 | 日期,股票代號,當沖成交量,非當沖成交量 |
| 日融資券排行 | 日期,股票代號,資買,資賣,資現償,資餘,資增減,資限,券買,券賣,券賣金額(千),券現償,券餘,券增減,資券相抵,券資比,資使用率,券使用率,當沖比率,融資維持率(%),整體維持率(%) |
| 日報酬率比較表 | 日期,股票代號,還原收盤價,日報酬率(%),週報酬率(%),月報酬率(%),開盤價(相對大盤),最高價(相對大盤),最低價(相對大盤),收盤價(相對大盤),漲跌(相對大盤),漲幅%(相對大盤),與大盤比日報酬率(%),與大盤比週報酬率(%),與大盤比月報酬率(%),殖利率(%) |
| 日常用技術指標表 | 日期,股票代號,K(9),D(9),RSI(5),RSI(10),DIF,MACD,DIF-MACD,W%R(5),W%R(10),+DI(14),-DI(14),ADX(14),週K(9),週D(9),週RSI(10),週DIF,週MACD,週DIF-週MACD,週+DI(14),週-DI(14),週ADX(14),Alpha(250D),Beta係數(21D),Beta係數(65D),乖離率(20日),乖離率(60日),相對強弱比(日),相對強弱比(週),近一月歷史波動率(%),EWMA波動率(%) |
| 日常用技術指標表Ⅱ | 日期,股票代號,保力加通道–頂部(20),保力加通道–均線(20),保力加通道–底部(20),SAR,TR(1),ADXR(14),+DM(14),-DM(14),週TR(14),週ADXR(14),週+DM(14),週-DM(14),紅三兵,黑三兵,一星二陽,一星二陰,孤島晨星,孤島夜星,長紅吞噬,長黑吞噬,低檔長紅,高檔長黑,低檔槌子,高檔吊人 |
| 特徵工程(股票類型) | 台灣50, 台灣100, 上市櫃 |
| 特徵工程(股票產業類型) | 產業名稱`One-Hot encoder`, 指數彙編分類`One-Hot encoder` |
| 特徵工程(時間特徵) | 年,月,月中日,年中日,星期,年週,季度,月初,月底,月中,季初月,季末月,年初月,年末月,month_sin,month_cos,weekday_sin,weekday_cos,monthday_sin,monthday_cos,yearday_sin,yearday_cos,週一,週五 |
| 特徵工程(滯後特徵) | 當沖成交量_lag_{1~5}, 當沖股(膝點)_lag_{1~5}, rolling_mean_{3, 5, 10, 15, 20} |
| 特徵工程(當沖相關) | 當沖成交比例, 當沖成交比例(市場) |
| 特徵工程(膝點) | 目標, 膝點標註 |
| | |
### **數據處理過程**
#### 1. 資料合併與處理
* 利用 `pd.merge` 合併不同數據來源,通過日期、股票代號等關鍵字段進行匹配
#### 2. 數據型態處理
- 日期欄位轉換為 `datetime` 格式並排序
- 類別型特徵(如股票代號、產業名稱)轉換為類別型數據以節省記憶體
#### 3. 缺失值處理
- 特定欄位如本益比、成交量變動百分比補 `0`
- 剩餘含有缺失值的行直接移除,確保數據完整性
- 移除約xxx筆資料,占總資料xxxx%
#### 4. 特徵工程
加入適合解釋當沖股的特徵
1. **新增特徵**
- **當沖成交比例**:計算當沖成交量占個股總成交量的比例
- **當沖成交比例(市場)**:計算個股當沖成交量占當日市場總成交量的比例
- **市場總當沖成交量**:按日期計算每日市場總當沖成交量
- **時間特徵**:新增時間相關特徵,包括季度、週數、月初標記等。
- **類別型特徵 One-Hot 編碼**:對產業名稱及指數分類進行 One-Hot 編碼。
2. **膝點檢測**
- 按每日個股當沖成交量排序,==標準化==後使用 [kneed](https://github.com/arvkevi/kneed) 演算法計算膝點
- 膝點之前的個股標記為「當沖股」
3. **滯後與滑動特徵**
- 當沖成交量、當沖股(膝點):新增 1-5 天的滯後特徵
- 收盤價:計算 3、5、10、15、20 天的移動平均成交量
4. **設定目標值**
- 滯前一天的膝點標註作為預測目標。
#### 5. 清理數據
- 移除因滯後特徵等原因產生的缺失值行,生成最終訓練數據集。
## 模型設計
模型選型:隨機森林
模型評估指標:
準確率、召回率、F1 分數、AUC。
### 模型驗證
測試數據:劃分訓練集與測試集,進行交叉驗證。
效能測試:模型推斷速度、系統響應時間。
固定移動視窗,訓練資料使用60天,並測試5天效果最好
| 訓練集天數 | 測試集天數 | 平均 F1 scores | F1標準差 |
|---|---|---|---|
| 60 | 5 | 0.6357 | 0.04945 |
以測試集60天,測試集5天效果通常最好
表示 資料時間用太長反而會有噪音出現
> 符合常理,當沖股通常只是短期,非長期
:::info
故模型可只使用`60`天訓練,並每`5`個交易日更新
但當然,每天更新模型的效果一定最好
:::
:::spoiler 其餘測試天數組合 結果
| 訓練集天數 | 測試集天數 | 平均 F1 scores |
|---|---|---|
| 20 | 10 | 0.6176 |
| 20 | 5 | 0.6209 |
| 30 | 10 | 0.6203 |
| 30 | 5 | 0.6239 |
| 60 | 20 | 0.6254 |
| 60 | 10 | 0.6270 |
| 60 | 5 | 0.6310 |
| 90 | 10 | 0.6281 |
| 90 | 5 | 0.6298 |
| 訓練集天數 | 測試集天數 | 平均 F1 scores | F1標準差 |
|---|---|---|---|
| 60 | 5 | 0.6310 | 0.05504 |
| 60 | 3 | 0.6275 | 0.06808 |
| 60 | 1 | 0.6299 | 0.09436 |
| 70 | 5 | 0.6280 | 0.05865 |
| 70 | 3 | 0.6302 | 0.06430 |
| 70 | 1 | 0.6294 | 0.09582 |
| 80 | 5 | 0.6287 | 0.05979 |
| 80 | 3 | 0.6301 | 0.06650 |
| 80 | 1 | 0.6311 | 0.09659 |
| 90 | 5 | 0.6298 | 0.06028 |
| 90 | 3 | 0.6292 | 0.06999 |
| 90 | 1 | 0.6340 | 0.09538 |
:::
# 結果
## 當沖股特性
- 通常是成交量大、周轉率高
- 股價變動大的
- 短期
## 當沖股預測結果分析

總共253個特徵,特徵重要性和為100%,前20個特徵占**72%**,前30個特徵占**81%**
| 特徵名稱 | 特徵重要性 |
|---|---|
| 當沖成交比例(市場) | 7.59% |
| 當沖成交量 | 7.23% |
| 平均3日當沖成交量 (rolling_mean_3) | 6.22% |
| 成交量 | 5.87% |
| 膝點標註 | 4.97% |
| 平均5日當沖成交量 (rolling_mean_5) | 4.89% |
| 資買 | 4.36% |
| 平均10日當沖成交量 (rolling_mean_10) | 4.13% |
| 成交筆數 | 3.95% |
| 當沖成交量_lag_1 | 2.93% |
| 資賣 | 2.85% |
| 當沖成交量_lag_2 | 2.75% |
| 非當沖成交量 | 2.71% |
| 當沖股(膝點)_lag_1 | 2.43% |
| 券賣 | 2.15% |
| 當沖股(膝點)_lag_2 | 1.81% |
| 當沖成交量_lag_4 | 1.55% |
| 當沖成交量_lag_3 | 1.36% |
| 資券相抵 | 1.35% |
| 券餘 | 1.32% |
以其中一天(20250108),預測隔天(20250109)其預測值

| 股票代號 | 股票名稱 | 被當沖概率 | 指數彙編分類 | 上市櫃 |
|---|---|---|---|---|
| 4931 | 新盛力 | 94.43% | 電子中游-NB與手機零組件 | 0 |
| 6558 | 興能高 | 90.72% | 電子中游-NB與手機零組件 | 1 |
| 3645 | 達邁 | 86.17% | 電子上游-PCB-製造 | 1 |
| 3450 | 聯鈞 | 83.88% | 電子上游-IC-封測 | 1 |
| 2374 | 佳能 | 83.55% | 電子下游-數位相機 | 1 |
| 4979 | 華星光 | 80.83% | 電子中游-通訊設備 | 0 |
| 3078 | 僑威 | 80.46% | 電子中游-電源供應器 | 0 |
| 6191 | 精成科 | 79.05% | 電子上游-PCB-製造 | 1 |
| 3211 | 順達 | 78.69% | 電子中游-NB與手機零組件 | 0 |
| 3013 | 晟銘電 | 77.11% | 電子中游-機殼 | 1 |
| 4991 | 環宇-KY | 75.31% | 電子上游-IC-其他 | 0 |
| 2498 | 宏達電 | 74.64% | 電子下游-手機製造 | 1 |
| 6215 | 和椿 | 74.53% | 軟體-系統整合 | 1 |
| 2618 | 長榮航 | 69.58% | 傳產-航運 | 1 |
| 1524 | 耿鼎 | 68.61% | 傳產-汽車零組件 | 1 |
| 6125 | 廣運 | 68.58% | 電子中游-儀器設備工程 | 0 |
| 4977 | 眾達-KY | 68.06% | 電子中游-網通 | 1 |
| 3706 | 神達 | 67.75% | 電子中游-EMS | 1 |
| 2609 | 陽明 | 64.92% | 傳產-航運 | 1 |
| 3323 | 加百裕 | 64.70% | 電子中游-NB與手機零組件 | 0 |
## 覆蓋率
若以今日當沖成交量排名前30名,預測隔日也為前30名的當沖股,覆蓋率平均60%

若以模型採取機率排名前30當作隔日預測,平均覆蓋率約85.8%(最高100%,最低63.33%)

化成每日的覆蓋率圖如下:
(https://hackmd.io/_uploads/rkgZgYDD1x.png)

下半年模型平均預測較準確(尤其是7、8月)
上半年則相對較差(尤其是2月連假後)
---
單一個月的結果如下:
![當沖_覆蓋率20241209_20250108]
# 完整程式碼
```python=
# 套件
import numpy as np
import pandas as pd
from WCFAdox import PCAX # CMoney套件(獲取資料用)
import matplotlib
import matplotlib.pyplot as plt
from datetime import datetime
from kneed import KneeLocator # 膝點
from sklearn.metrics import roc_curve, auc , f1_score
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.preprocessing import OneHotEncoder
import logging
import warnings # 處理警告訊息
warnings.simplefilter(action='ignore', category=FutureWarning) # category類型
# Windows 處理中文字體
# # 設定字體預設是"serif", "sans-serif", "cursive", "fantasy", "monospace"
# plt.rcParams['font.family'] = 'sans-serif'
# # 設定MPL的font.sans-serif
# plt.rcParams['font.sans-serif']=['Microsoft YaHei', 'DejaVu Sans', 'Bitstream Vera Sans', 'Computer Modern Sans Serif', 'Lucida Grande', 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica', 'Avant Garde', 'sans-serif'] #用来正常显示中文标签
# plt.rcParams['axes.unicode_minus'] = False #用来正常显示负号
# 取得資料
#設定連線主機IP並產生物件; 請依實際狀況調整http或https
PX=PCAX("http://192.168.10.30")
PX._returnAdoxVer()
#上市個股2年基本資料
tse_info_data=PX.Pal_Data("上市櫃公司基本資料","Y","2023","2024",
colist="年度,股票代號,股票名稱,產業名稱,產業指數名稱,指數彙編分類",
ps="<CM代號,1>")
#上櫃個股2年基本資料
otc_info_data=PX.Pal_Data("上市櫃公司基本資料","Y","2023","2024",
colist="年度,股票代號,股票名稱,產業名稱,產業指數名稱,指數彙編分類",
ps="<CM代號,2>")
#上市個股2年開高低收
tse_price_data=PX.Pal_Data("日收盤表排行","D","20230101","20241031",
colist="日期,股票代號,開盤價,最高價,最低價,收盤價,漲跌,[漲幅(%)],[振幅(%)],漲跌停,成交量,[成交量變動(%)],成交筆數,[總市值(億)],本益比,股價淨值比,[週轉率(%)]",
ps="<CM代號,1>")
#上櫃個股2年開高低收
otc_price_data=PX.Pal_Data("日收盤表排行","D","20230101","20241031",
colist="日期,股票代號,開盤價,最高價,最低價,收盤價,漲跌,[漲幅(%)],[振幅(%)],漲跌停,成交量,[成交量變動(%)],成交筆數,[總市值(億)],本益比,股價淨值比,[週轉率(%)]",
ps="<CM代號,2>")
#上市個股2年沖銷交易
tse_target_data=PX.Pal_Data("日當日沖銷交易","D","20230101","20241031",
colist="日期,股票代號,當沖成交量,非當沖成交量",
ps="<CM代號,1>")
#上櫃個股2年沖銷交易
otc_target_data=PX.Pal_Data("日當日沖銷交易","D","20230101","20241031",
colist="日期,股票代號,當沖成交量,非當沖成交量",
ps="<CM代號,2>")
#上市個股2年日融資券排行
tse_marginTrad_data=PX.Pal_Data("日融資券排行","D","20230101","20241031",
colist="日期,股票代號,資買,資賣,資現償,資餘,資增減,資限,券買,券賣,[券賣金額(千)],券現償,券餘,券增減,資券相抵,券資比,資使用率,券使用率,當沖比率,[融資維持率(%)],[整體維持率(%)]",
ps="<CM代號,1>")
#上市個股2年日報酬率比較表
tse_return_data=PX.Pal_Data("日報酬率比較表","D","20230101","20241031",
colist="日期,股票代號,還原收盤價,[日報酬率(%)],[週報酬率(%)],[月報酬率(%)],[開盤價(相對大盤)],[最高價(相對大盤)],[最低價(相對大盤)],[收盤價(相對大盤)],[漲跌(相對大盤)],[漲幅%(相對大盤)],[與大盤比日報酬率(%)],[與大盤比週報酬率(%)],[與大盤比月報酬率(%)],[殖利率(%)]",
ps="<CM代號,1>")
#上市個股2年日常用技術指標表
tse_techIndex_data=PX.Pal_Data("日常用技術指標表","D","20230101","20241031",
colist="日期,股票代號,[K(9)],[D(9)],[RSI(5)],[RSI(10)],[DIF],[MACD],[DIF-MACD],[W%R(5)],[W%R(10)],[+DI(14)],[-DI(14)],[ADX(14)],[週K(9)],[週D(9)],[週RSI(10)],[週DIF],[週MACD],[週DIF-週MACD],[週+DI(14)],[週-DI(14)],[週ADX(14)],[Alpha(250D)],[Beta係數(21D)],[Beta係數(65D)],[乖離率(20日)],[乖離率(60日)],[相對強弱比(日)],[相對強弱比(週)],[近一月歷史波動率(%)],[EWMA波動率(%)]",
ps="<CM代號,1>")
#上市個股2年日常用技術指標表Ⅱ
tse_techIndex2_data=PX.Pal_Data("日常用技術指標表Ⅱ","D","20230101","20241031",
colist="日期,股票代號,[保力加通道–頂部(20)],[保力加通道–均線(20)],[保力加通道–底部(20)],[SAR],[TR(1)],[ADXR(14)],[+DM(14)],[-DM(14)],[週TR(14)],[週ADXR(14)],[週+DM(14)],[週-DM(14)],紅三兵,黑三兵,一星二陽,一星二陰,孤島晨星,孤島夜星,長紅吞噬,長黑吞噬,低檔長紅,高檔長黑,低檔槌子,高檔吊人",
ps="<CM代號,1>")
#上櫃個股2年日融資券排行
otc_marginTrad_data=PX.Pal_Data("日融資券排行","D","20230101","20241031",
colist="日期,股票代號,資買,資賣,資現償,資餘,資增減,資限,券買,券賣,[券賣金額(千)],券現償,券餘,券增減,資券相抵,券資比,資使用率,券使用率,當沖比率,[融資維持率(%)],[整體維持率(%)]",
ps="<CM代號,2>")
#上櫃個股2年日報酬率比較表
otc_return_data=PX.Pal_Data("日報酬率比較表","D","20230101","20241031",
colist="日期,股票代號,還原收盤價,[日報酬率(%)],[週報酬率(%)],[月報酬率(%)],[開盤價(相對大盤)],[最高價(相對大盤)],[最低價(相對大盤)],[收盤價(相對大盤)],[漲跌(相對大盤)],[漲幅%(相對大盤)],[與大盤比日報酬率(%)],[與大盤比週報酬率(%)],[與大盤比月報酬率(%)],[殖利率(%)]",
ps="<CM代號,2>")
#上櫃個股2年日常用技術指標表
otc_techIndex_data=PX.Pal_Data("日常用技術指標表","D","20230101","20241031",
colist="日期,股票代號,[K(9)],[D(9)],[RSI(5)],[RSI(10)],[DIF],[MACD],[DIF-MACD],[W%R(5)],[W%R(10)],[+DI(14)],[-DI(14)],[ADX(14)],[週K(9)],[週D(9)],[週RSI(10)],[週DIF],[週MACD],[週DIF-週MACD],[週+DI(14)],[週-DI(14)],[週ADX(14)],[Alpha(250D)],[Beta係數(21D)],[Beta係數(65D)],[乖離率(20日)],[乖離率(60日)],[相對強弱比(日)],[相對強弱比(週)],[近一月歷史波動率(%)],[EWMA波動率(%)]",
ps="<CM代號,2>")
#上櫃個股2年日常用技術指標表Ⅱ
otc_techIndex2_data=PX.Pal_Data("日常用技術指標表Ⅱ","D","20230101","20241031",
colist="日期,股票代號,[保力加通道–頂部(20)],[保力加通道–均線(20)],[保力加通道–底部(20)],[SAR],[TR(1)],[ADXR(14)],[+DM(14)],[-DM(14)],[週TR(14)],[週ADXR(14)],[週+DM(14)],[週-DM(14)],紅三兵,黑三兵,一星二陽,一星二陰,孤島晨星,孤島夜星,長紅吞噬,長黑吞噬,低檔長紅,高檔長黑,低檔槌子,高檔吊人",
ps="<CM代號,2>")
#上市台灣50個股2年開高低收
tse_t50_data=PX.Pal_Data("日收盤表排行","D","20230101","20241031",
colist="日期,股票代號",
ps="<CM特殊,1>")
#上市台灣100個股2年開高低收(不含台灣50)
tse_t100_data=PX.Pal_Data("日收盤表排行","D","20230101","20241031",
colist="日期,股票代號",
ps="<CM特殊,3>")
# 合併資料
tse_t50_data["台灣50"] = 1
tse_t100_data["台灣100"] = 1
tse_info_data["上市櫃"] = 1
tse_price_data["上市櫃"] = 1
tse_target_data["上市櫃"] = 1
tse_marginTrad_data["上市櫃"] = 1
tse_return_data["上市櫃"] = 1
tse_techIndex_data["上市櫃"] = 1
tse_techIndex2_data["上市櫃"] = 1
otc_info_data["上市櫃"] = 0
otc_price_data["上市櫃"] = 0
otc_target_data["上市櫃"] = 0
otc_marginTrad_data["上市櫃"] = 0
otc_return_data["上市櫃"] = 0
otc_techIndex_data["上市櫃"] = 0
otc_techIndex2_data["上市櫃"] = 0
info_data=pd.concat([tse_info_data,otc_info_data])
price_data=pd.concat([tse_price_data,otc_price_data])
target_data=pd.concat([tse_target_data,otc_target_data])
marginTrad_data = pd.concat([tse_marginTrad_data,otc_marginTrad_data])
return_data = pd.concat([tse_return_data,otc_return_data])
techIndex_data = pd.concat([tse_techIndex_data,otc_techIndex_data])
techIndex2_data = pd.concat([tse_techIndex2_data,otc_techIndex2_data])
merged_df = pd.merge(price_data, tse_t50_data, on=["日期", "股票代號"], how="left")
# 將 '台灣50' 的缺失值補 0
merged_df['台灣50'] = merged_df['台灣50'].fillna(0)
merged_df = pd.merge(merged_df, tse_t100_data, on=["日期", "股票代號"], how="left")
# 將 '台灣100' 的缺失值補 0
merged_df['台灣100'] = merged_df['台灣100'].fillna(0)
merged_df = pd.merge(merged_df, target_data, on=["日期", "股票代號", "上市櫃"], how="inner")
merged_df = pd.merge(merged_df, marginTrad_data, on=["日期", "股票代號", "上市櫃"], how="inner")
merged_df = pd.merge(merged_df, return_data, on=["日期", "股票代號", "上市櫃"], how="inner")
merged_df = pd.merge(merged_df, techIndex_data, on=["日期", "股票代號", "上市櫃"], how="inner")
merged_df = pd.merge(merged_df, techIndex2_data, on=["日期", "股票代號", "上市櫃"], how="inner")
# 處理月資料併入日資料
# 提取日資料的年度
merged_df['年度'] = merged_df['日期'].str[:4].astype(object)
# 根據年度合併
merged_df = pd.merge(merged_df, info_data, on=['年度', '股票代號', '上市櫃'], how='left').drop(columns="年度")
# 刪除變數釋放記憶體
# del tse_t50_data, tse_t100_data, tse_info_data, tse_price_data, tse_target_data, tse_marginTrad_data, tse_return_data, tse_techIndex_data, tse_techIndex2_data
# del otc_info_data, otc_price_data, otc_target_data, otc_marginTrad_data, otc_return_data, otc_techIndex_data, otc_techIndex2_data
# 處理資料型態
# 設定索引為日期,確保是時間序列
merged_df['日期'] = pd.to_datetime(merged_df['日期'])
merged_df.sort_values(by=['日期', '股票代號'], ascending=[True, True], inplace=True)
# 將"股票代號"欄位轉換為類別型態
merged_df[['股票代號', '股票名稱', '產業名稱', '產業指數名稱', '指數彙編分類']] = merged_df[['股票代號', '股票名稱', '產業名稱', '產業指數名稱', '指數彙編分類']].astype('category')
numeric_cols = merged_df.select_dtypes(include=['object']).columns
merged_df[numeric_cols] = merged_df[numeric_cols].apply(pd.to_numeric, errors='coerce')
merged_df.info()
# 處理殘缺值
# Nan補0
merged_df["本益比"] = merged_df["本益比"].fillna(0)
merged_df["成交量變動(%)"] = merged_df["成交量變動(%)"].fillna(0)
# 刪除Nan的列資料
data_dropna_df = merged_df.dropna()
# 特徵工程-新欄位
data_dropna_df["當沖成交比例"] = data_dropna_df["當沖成交量"] / (data_dropna_df["當沖成交量"] + data_dropna_df["非當沖成交量"])
data_dropna_df["當沖成交比例"] = data_dropna_df["當沖成交比例"].fillna(0)
# 計算每日市場總當沖成交量
market_daily_volume = data_dropna_df.groupby('日期')['當沖成交量'].sum().reset_index()
market_daily_volume = market_daily_volume.rename(columns={'當沖成交量': '市場總當沖成交量'})
# 將市場總當沖成交量合併到原始資料中
data_dropna_df = data_dropna_df.merge(market_daily_volume, on='日期', how='left')
# 計算正確的當沖成交比例
data_dropna_df["當沖成交比例(市場)"] = data_dropna_df["當沖成交量"] / data_dropna_df["市場總當沖成交量"]
def add_time_features(df, date_column='日期'):
"""
為資料集增加時間相關特徵
Parameters:
-----------
df : pandas.DataFrame
含有日期欄位的資料框
date_column : str
日期欄位的名稱
Returns:
--------
pandas.DataFrame
增加時間特徵後的資料框
"""
# 確保日期欄位為datetime格式
df = df.copy()
if not pd.api.types.is_datetime64_any_dtype(df[date_column]):
df[date_column] = pd.to_datetime(df[date_column])
# 基本時間特徵
df['年'] = df[date_column].dt.year
df['月'] = df[date_column].dt.month
df['月中日'] = df[date_column].dt.day # 月中的第幾天(1-31)
df['年中日'] = df[date_column].dt.dayofyear # 年中的第幾天(1-366)
df['星期'] = df[date_column].dt.dayofweek + 1 # 1-7,其中1代表星期一
df['年週'] = df[date_column].dt.isocalendar().week # 年中的第幾週(1-52)
df['季度'] = df[date_column].dt.quarter # 年中的第幾季(1-4)
# 月初/月中/月底標記
df['月初'] = (df['月中日'] <= 5).astype(int)
df['月底'] = (df['月中日'] >= 25).astype(int)
df['月中'] = ((df['月中日'] > 5) & (df['月中日'] < 25)).astype(int)
# 季初/季末月標記
df['季初月'] = df['月'].isin([1, 4, 7, 10]).astype(int)
df['季末月'] = df['月'].isin([3, 6, 9, 12]).astype(int)
# 年初/年末月標記
df['年初月'] = (df['月'] <= 3).astype(int)
df['年末月'] = (df['月'] >= 10).astype(int)
# 周期性特徵 - 使用三角函數
# 月份的周期性(12個月)
df['month_sin'] = np.sin(2 * np.pi * df['月']/12)
df['month_cos'] = np.cos(2 * np.pi * df['月']/12)
# 星期的周期性(5個工作日)
df['weekday_sin'] = np.sin(2 * np.pi * (df['星期']-1)/5)
df['weekday_cos'] = np.cos(2 * np.pi * (df['星期']-1)/5)
# 月中日的周期性
df['monthday_sin'] = np.sin(2 * np.pi * df['月中日']/31)
df['monthday_cos'] = np.cos(2 * np.pi * df['月中日']/31)
# 年中日的周期性
df['yearday_sin'] = np.sin(2 * np.pi * df['年中日']/365)
df['yearday_cos'] = np.cos(2 * np.pi * df['年中日']/365)
# 是否為假日前後的交易日
# 注意:這裡的實現比較簡單,可能需要根據實際的假日日曆進行調整
df['週一'] = (df['星期'] == 1).astype(int) # 假日後
df['週五'] = (df['星期'] == 5).astype(int) # 假日前
return df
# 使用:
data_dropna_df = add_time_features(data_dropna_df)
# 創建 One-Hot Encoder
encoder = OneHotEncoder(sparse_output=False)
# 對類別型特徵進行編碼
encoded_features = encoder.fit_transform(data_dropna_df[['產業名稱', '指數彙編分類']])
# 獲取 One-Hot 編碼後的欄位名稱
encoded_feature_names = encoder.get_feature_names_out(['產業名稱', '指數彙編分類'])
# 建立編碼後的 DataFrame,並將欄位名稱設置為對應名稱
encoded_df = pd.DataFrame(encoded_features, columns=encoded_feature_names, index=data_dropna_df.index)
# 合併原始資料(去掉類別型欄位)和編碼後的特徵
df_encoded = pd.concat([data_dropna_df.drop(columns=['股票名稱', '產業名稱', '產業指數名稱', '指數彙編分類']), encoded_df], axis=1)
# 計算膝點
# 需要按日期分組計算每日的膝點
# 新增膝點標註欄位 (0 或 1)
df_encoded['膝點標註'] = 0
# 儲存每日膝點詳細資訊的 DataFrame
knee_points_df = pd.DataFrame(columns=['日期', '膝點排序後索引', '膝點排序後數值'])
for date in df_encoded['日期'].unique():
# 篩選當天的資料
daily_data = df_encoded[df_encoded['日期'] == date]
# 進行膝點檢測
volume = daily_data["當沖成交量"].values
sorted_indices = np.argsort(volume)[::-1] # 由大到小排序
sorted_volume = volume[sorted_indices]
# 標準化處理
x = np.arange(len(sorted_volume))
normalized_x = (x - x.min()) / (x.max() - x.min())
normalized_y = (sorted_volume - sorted_volume.min()) / (sorted_volume.max() - sorted_volume.min())
# 使用 Kneedle 找膝點
knee_locator = KneeLocator(normalized_x, normalized_y, curve="convex", direction="decreasing")
turning_point = int(round(knee_locator.knee * (len(sorted_volume) - 1))) # 膝點的索引
turning_point_value = sorted_volume[turning_point] # 膝點對應的數值
# 紀錄每日膝點
knee_points_df = pd.concat([knee_points_df, pd.DataFrame({
'日期': [date],
'膝點排序後索引': [turning_point],
'膝點排序後數值': [turning_point_value]
})], ignore_index=True)
# 更新膝點標註
# 找出膝點及之前的位置對應到原始 DataFrame 的索引
indices_to_update = daily_data.iloc[sorted_indices[:turning_point]].index # 找到原始索引
df_encoded.loc[indices_to_update, '膝點標註'] = 1 # 標註膝點及其之前的位置為 1
# 滯後特徵
for lag in range(1, 6): # 添加 1 到 5 天的滯後特徵
df_encoded[f'當沖成交量_lag_{lag}'] = df_encoded.groupby('股票代號')['當沖成交量'].shift(lag)
for lag in range(1, 6): # 添加 1 到 5 天的滯後特徵
df_encoded[f'當沖股(膝點)_lag_{lag}'] = df_encoded.groupby('股票代號')['膝點標註'].shift(lag)
for lag in [3, 5, 10, 15, 20]: # 滑動窗口特徵(移動平均值)添加 3~20 天
df_encoded[f'rolling_mean_{lag}'] = df_encoded.groupby('股票代號')['當沖成交量'].transform(lambda x: x.rolling(window=lag).mean())
df_encoded['目標'] = df_encoded.groupby('股票代號')['膝點標註'].shift(-1)
# 移除因為滯後產生的Nan row
data_df = df_encoded.dropna()
```