0%

用 C# .NET Core 自動爬取台股每日股價 | 用程式打造選股策略(1)

前言

近期由於武漢肺炎影響,使的自己的投資部位嚴重虧損,
因此決定開始研究一些策略,看看能不能改善自己的一些觀念與操作模式,
既然要研究策略,那乾脆搭配程式實做順便練練手。

網路上看了Google了一下資料,發現用 Python 來做股票分析的人越來越多,
猛然回首,發現其實去年我也有買了 Hahow的這門課程: 用Python理財:打造小資族選股策略


既然用Python的人這麼多,那我就偏不用!
因此決定用自己最熟悉的 C# 來做爬蟲試試看,到底比起 Python 有沒有比較困難?

觀察網站

這次要爬的網站就是大家都知道的: 台灣證券交易所
爬取網頁: 首頁 > 交易資訊 > 盤後資訊 > 每日收盤行情
分類: 全部(不含權證、牛熊證、可展延牛熊證)

查詢網頁後,按下F12觀察Resquest:

由於是JSON格式,看起來不是太難處理,
因此到 Response 將資料複製到 Json Parser Online 做觀察

可以發現 field9 就是表格的欄位標題,data9 就是的對應的個股資訊
這樣的話其實還蠻容易的,馬上開始動工。

爬取股價資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 存放股價的Model
public class Datas
{
public List<List<string>> data9 { get; set; }
public List<string> fields9 { get; set; }
}

// 爬取資料的method
public async Task CrawlerStockByDate(DateTime date)
{
using (var client = new HttpClient())
{
string json = await client.GetStringAsync($"https://www.twse.com.tw/exchangeReport/MI_INDEX?response=json&date={date.ToString("yyyyMMdd")}&type=ALLBUT0999&_=1586529875476");
var resDatas = JsonSerializer.Deserialize<Datas>(json); //直接用.NET Core 的 System.Text.Json 來解析資料
}
}

簡單輕鬆!到目前為止似乎還沒有什麼大問題..
不過真要說缺點,大概就是沒有像 Python + Jupyter 那麼方便,可以馬上呈現視覺化的表格讓我們看見…
但對於一個碼農來說,這些根本不是問題!

存入資料庫

用 Docker 開啟 SQL Server 資料庫

接著我打算將資料存進資料庫,這樣方便以後分析,
為了方便起見,這裡直接在雲端空間使用Docker開一個SQL Server的資料庫

1
2
3
4
5
docker run -e 'ACCEPT_EULA=Y' \
-e 'MSSQL_SA_PASSWORD=<YourStrong!Passw0rd>' \
-p 1433:1433 \
-v sqlvolume:/var/opt/mssql \
-d mcr.microsoft.com/mssql/server:2017-latest

這裡遇到了一個小雷:
根據官方文件: A strong system administrator (SA) 密碼必須符合以下規則:

  1. 至少 8 個字元
  2. 必需包含英文大寫、英文小寫、數字、非字母數字符號四者中的其中三種即可

其實也不能說是雷,只能怪自己沒看好文件,導致每次run起來馬上就停止QQ…

安裝套件

1
2
3
4
dotnet add package Microsoft.Data.SqlClient

dotnet add package Dapper
dotnet add package Dapper.Contrib

在.NET Core 3.0 版本後,微軟已將原本的 System.Data.SqlClient 遷移到 Microsoft.Data.SqlClient
官方表示: 這意味著發展重點已經改變。但目前還不會立即放棄對System.Data.SqlClient的支持。
詳細情形可以參考: Introducing the new Microsoft.Data.SqlClient

這裡選用了Dapper + Dapper.Contrib當作 ORM 的工具
比起 Entity Framework, Dapper更加輕量化,使用上也非常方便。

程式部分

首先,建立我們要存的欄位Model
(資料庫Table建立的部分我就省略了…XD,反正應該不是什麼大問題)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Table("TWStockPrice_history_1990")]
public class Stock
{
[ExplicitKey]
public int id { get; set; }
public string stock_id { get; set; }
public string trading_volume { get; set; }
public string trading_money { get; set; }
public string open { get; set; }
public string max { get; set; }
public string min { get; set; }
public string close { get; set; }
public string spread { get; set; }
public string trading_turnover { get; set; }
public DateTime trading_date { get; set; }
}

先整理一下剛剛的欄位對應到data的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[0]  :"證券代號"
[1] :"證券名稱"
[2] :"成交股數"
[3] :"成交筆數"
[4] :"成交金額"
[5] :"開盤價"
[6] :"最高價"
[7] :"最低價"
[8] :"收盤價"
[9] :"漲跌(+/-)"
[10] :"漲跌價差"
[11] :"最後揭示買價"
[12] :"最後揭示買量"
[13] :"最後揭示賣價"
[14] :"最後揭示賣量"
[15] :"本益比"

接著將剛剛爬下來的資料轉成List<Stock>的格式,存入資料庫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public async Task CrawlerStockByDate(DateTime date)
{
using (var client = new HttpClient())
{
string json = await client.GetStringAsync($"https://www.twse.com.tw/exchangeReport/MI_INDEX?response=json&date={date.ToString("yyyyMMdd")}&type=ALLBUT0999&_=1586529875476");
var resDatas = JsonSerializer.Deserialize<Datas>(json);
List<Stock> stockList = new List<Stock>();
if (resDatas.data9 != null)
{
var id = _stockRepository.GetMaxId() + 1; // 這個方法就省略了..就只是從資料庫裡爬找出最大Id而已
resDatas.data9.ForEach(data =>
{
stockList.Add(new Stock()
{
id = id++,
stock_id = data[0],
trading_volume = data[2],
trading_money = data[4],
open = data[5],
max = data[6],
min = data[7],
close = data[8],
spread = data[10],
trading_turnover = data[3],
trading_date = date.Date
});
});
_stockRepository.Insert(stockList);
}
}
}

StockRepository.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StockRepository
{
private readonly SqlConnection _conn;
private readonly ILogger<StockRepository> _logger;
public StockRepository(ILogger<StockRepository> logger, SqlConnection conn)
{
_logger = logger;
_conn = conn;
}

public void Insert(List<Stock> stockList)
{
try
{
using(var scope = new TransactionScope())
{
foreach(var stock in stockList)
{
_conn.Insert(stock);
}
scope.Complete();
}
}
catch(Exception ex)
{
_logger.LogError(ex.Message);
}
}
}

由於.NET Core大量使用了依賴注入(Dependency Injection),如果不是很了解的人可以參考看看我這篇:
為什麼要使用Dependency Injection(依賴注入)? ASP.NET Core 開發者必學!

每日自動抓取排程

然後我希望把這隻程式變成一個排程,讓它每天自動更新資料,這樣以後在做分析的時候比較方便..

安裝套件 Coravel

1
dotnet add package coravel

這裡選用了 Coravel 這個套件,其實我也沒使用過,
不過稍微看了一下文件,感覺蠻容易的,比起 Quartz.Net,設定相對簡化很多。

程式部分

首先,根據官方文件,在 Startup.cs 加入 ConfigureServices() method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScheduler();
services.AddTransient<StockPriceCrawlerSchedule>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.ApplicationServices.UseScheduler(scheduler =>
{
scheduler
.Schedule<StockPriceCrawlerSchedule>()
.Cron("30 10 23 * *"); //UTC 23:10:30 => 15:10:30
});
}

這樣就設好了一個StockPriceCrawlerSchedule的排程,
可以簡單的用 Cron 來指定要執行排程的時間,
不懂的朋友可以參考 維基百科:Cron
這裡要注意的是 Coravel 的 Cron 是UTC時間,為了對應台灣時間,所以我們要+8小時

接著來看看StockPriceCrawlerSchedule
根據官方文件,我們只需要繼承 IInvocable 這個 interface
然後只要排程時間一到就會執行 Invoke() 方法
所以接下來我需要在Invoke()方法內做這些事:

  1. 從資料庫取出所有資料的最大日期
  2. 從最大日期到今天日期,都執行剛剛寫好的 CrawlerStockByDate()方法
  3. 週末跳過
  4. 每次爬取加入間格時間 (避免太頻繁造成被擋)

直接附上程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class StockPriceCrawlerSchedule : IInvocable
{
private StockRepository _stockRepository;

public StockPriceCrawlerSchedule(StockRepository stockRepository)
{
this._stockRepository = stockRepository;
}

public async Task Invoke()
{
DateTime date = _stockRepository.GetMaxDate().AddDays(1);
while(DateTime.Compare(date.Date, DateTime.Now.Date) <= 0)
{
//假日不抓
if(date.DayOfWeek == DayOfWeek.Sunday || date.DayOfWeek == DayOfWeek.Saturday)
{
date = date.AddDays(1);
continue;
}
else {
await CrawlerStockByDate(date);
date = date.AddDays(1);
Thread.Sleep(7000);
}
}
}

...
}

搞定收工!

心得

到目前為止,使用 C# 爬資料感覺並沒有那麼困難,
如果自己是工程師,使用自己熟悉的程式語言實作應該也是輕鬆容易。

我認為 Python 的優勢是很容易就能產生視覺化的圖表,這點確實讓新手比較有感!才知道自已目前的資料到底處理的怎麼樣了,
然後在 AI 方面,似乎也有比較多的資源可以使用。
有興趣的朋友們可以參考看看 用 Python 理財:打造自己的 AI 股票理專


另外這位作者也有一個 FinLab 量化實驗室 的部落格,裡面分享了許多不錯的策略,值得推薦!

之後我也會繼續使用 C# 來實作看看一些策略,雖然不一定比較方便,不過就當成是一種強迫練習吧!

↓↓↓ 如果喜歡我的文章,可以幫我按個Like! ↓↓↓
>> 或者,請我喝杯咖啡,這樣我會更有動力唷! <<<
街口支付

街口支付

街口帳號: 901061546

歡迎關注我的其它發布渠道