前言
近期由於武漢肺炎影響,使的自己的投資部位嚴重虧損,
因此決定開始研究一些策略,看看能不能改善自己的一些觀念與操作模式,
既然要研究策略,那乾脆搭配程式實做順便練練手。
網路上看了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
| public class Datas { public List<List<string>> data9 { get; set; } public List<string> fields9 { get; set; } }
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); } }
|
簡單輕鬆!到目前為止似乎還沒有什麼大問題..
不過真要說缺點,大概就是沒有像 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) 密碼必須符合以下規則:
- 至少 8 個字元
- 必需包含英文大寫、英文小寫、數字、非字母數字符號四者中的其中三種即可
其實也不能說是雷,只能怪自己沒看好文件,導致每次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; 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 * *"); }); }
|
這樣就設好了一個StockPriceCrawlerSchedule
的排程,
可以簡單的用 Cron 來指定要執行排程的時間,
不懂的朋友可以參考 維基百科:Cron
這裡要注意的是 Coravel 的 Cron 是UTC時間,為了對應台灣時間,所以我們要+8小時
接著來看看StockPriceCrawlerSchedule
根據官方文件,我們只需要繼承 IInvocable 這個 interface
然後只要排程時間一到就會執行 Invoke()
方法
所以接下來我需要在Invoke()
方法內做這些事:
- 從資料庫取出所有資料的最大日期
- 從最大日期到今天日期,都執行剛剛寫好的
CrawlerStockByDate()
方法
- 週末跳過
- 每次爬取加入間格時間 (避免太頻繁造成被擋)
直接附上程式碼:
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