iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0

今日目標

今天我們把前幾天的成果串起來,完成 選股工具的資料基礎建設

  1. 使用 IStockApiService 抓取股票的 日 K 線資料(近一年或增量補齊)。
  2. 計算 KD / MACD 指標,並存回 LiteDB。
  3. 在 DB 中同時記錄「缺漏日」。

1) 資料存取:LiteDbStockRepository

以下是最新版本的 Repository,專責管理:

  • 股票清單
  • 日 K 線
  • 缺漏日
  • 技術指標
// Repositories/LiteDbStockRepository.cs
using LiteDB;
using MyStockApp.Model;
using WpfTestApp.Model;

public class LiteDbStockRepository : IStockRepository
{
    private readonly string _dbPath;

    public LiteDbStockRepository(string dbPath = "StockData.db")
    {
        _dbPath = dbPath;
    }

    public void SaveStocks(IEnumerable<StockProfile> stocks)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<StockProfile>("stocks");
        col.DeleteAll();
        col.InsertBulk(stocks);
    }

    public IEnumerable<StockProfile> LoadStocks()
    {
        using var db = new LiteDatabase(_dbPath);
        return db.GetCollection<StockProfile>("stocks").FindAll();
    }

    public DateTime? LoadLatestQuoteDate(string code)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<DailyQuote>("daily_quotes");
        var last = col.Find(x => x.Code == code)
                      .OrderByDescending(x => x.Date)
                      .FirstOrDefault();
        return last?.Date;
    }

    public void UpsertDailyQuotes(string code, IEnumerable<DailyQuote> quotes)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<DailyQuote>("daily_quotes");
        col.EnsureIndex(x => new { x.Code, x.Date }, unique: true);

        foreach (var q in quotes)
        {
            col.Upsert(q);
        }
    }

    public void UpsertMissingDays(string code, IEnumerable<MissingQuoteDay> items)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<MissingQuoteDay>("missing_days");
        col.EnsureIndex(x => new { x.Code, x.Date }, unique: true);

        foreach (var item in items)
        {
            col.Upsert(item);
        }
    }

    public IEnumerable<DailyQuote> LoadDailyQuotes(string code, DateTime from, DateTime to)
    {
        using var db = new LiteDatabase(_dbPath);
        return db.GetCollection<DailyQuote>("daily_quotes")
                 .Find(x => x.Code == code && x.Date >= from && x.Date <= to)
                 .OrderBy(x => x.Date);
    }

    public void UpsertIndicators(string code, IEnumerable<TechIndicator> items)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<TechIndicator>("indicators");
        col.DeleteMany(x => x.Code == code && x.Date >= DateTime.UtcNow.AddYears(-1));
        col.InsertBulk(items);
        col.EnsureIndex(x => new { x.Code, x.Date });
    }

    public TechIndicator? LoadIndicator(string code, DateTime date)
    {
        using var db = new LiteDatabase(_dbPath);
        var col = db.GetCollection<TechIndicator>("indicators");

        return col.Find(x => x.Code == code && x.Date <= date)
                  .OrderByDescending(x => x.Date)
                  .FirstOrDefault();
    }
}

2) API 服務:IStockApiService

將原本Day 12做的在 ICrawler 的方法改個名稱IStockApiService:

    /// <summary>
    /// Data crawler capable of fetching stock lists and historical quotes.
    /// 資料擷取介面:用於取得股票清單與歷史報價。
    /// </summary>
    public interface 的方法改個名稱IStockApiService
    {
        /// <summary>
        /// Retrieves all stock profiles.
        /// 取得所有股票主檔資料。
        /// </summary>
        IEnumerable<StockProfile> GetStockList();

        /// <summary>
        /// Crawls historical daily quotes for a single stock.
        /// 取得單一股票於指定期間的每日行情。
        /// </summary>
        /// <param name="code">Stock code.</param>
        /// <param name="startDate">Inclusive start date.</param>
        /// <param name="endDate">Inclusive end date.</param>
        IEnumerable<DailyQuote> GetDailyQuotes(string code, DateTime startDate, DateTime endDate);
    }

}

實作的部分,這邊建議使用FinMind或Fugule的API來取得所有股票的基本資料,因TWSE(台灣證交所)的API資料只有上市公司的股票,並未包含上櫃(要去TPEX的API取),使用上FinMind或Fugule的API較為方便


3) QuoteFetchService:增量抓取 + 缺漏日記錄

這個服務會:

  1. 檢查 DB 最新一筆日線日期。
  2. 無資料 → 抓近一年。
  3. 有資料 → 從最新一日後補抓到今天。
  4. 將抓到的日線寫入 DB。
  5. 找出缺漏日並記錄到 DB。
public class QuoteFetchService : IQuoteFetchService
{
    private readonly IStockApiService _api;
    private readonly IStockRepository _repo;

    public QuoteFetchService(IStockApiService api, IStockRepository repo)
    {
        _api = api;
        _repo = repo;
    }

    public Task<int> FetchLastYearAsync(CancellationToken ct = default)
    {
        int updated = 0;
        var stocks = _repo.LoadStocks();
        var today = DateTime.UtcNow.Date;

        foreach (var s in stocks)
        {
            ct.ThrowIfCancellationRequested();

            var latest = _repo.LoadLatestQuoteDate(s.Code);

            DateTime from = latest?.AddDays(1) ?? today.AddYears(-1);
            DateTime to = today;

            if (from > to) continue;

            var quotes = _api.GetDailyQuotes(s.Code, from, to).OrderBy(q => q.Date).ToList();
            if (quotes.Count > 0)
            {
                _repo.UpsertDailyQuotes(s.Code, quotes);
                updated++;
            }

            var expected = EnumerateBusinessDays(from, to).ToHashSet();
            var returned = quotes.Select(q => q.Date.Date).ToHashSet();
            var missing = expected.Except(returned)
                                  .Select(d => new MissingQuoteDay
                                  {
                                      Code = s.Code,
                                      Date = d,
                                      Reason = "NoQuoteReturned"
                                  });

            _repo.UpsertMissingDays(s.Code, missing);
        }

        return Task.FromResult(updated);
    }

    private static IEnumerable<DateTime> EnumerateBusinessDays(DateTime from, DateTime to)
    {
        for (var d = from.Date; d <= to.Date; d = d.AddDays(1))
        {
            if (d.DayOfWeek != DayOfWeek.Saturday && d.DayOfWeek != DayOfWeek.Sunday)
                yield return d;
        }
    }
}

4) IndicatorService:計算 KD / MACD

IndicatorService 提供 BuildIndicators,根據日 K 計算 KD / MACD。
(程式碼與前一篇相同,這裡不再重複,重點是:算出來的結果存回 DB → UpsertIndicators。)


小結

今天完成了:

  • IStockApiService 接管 API 呼叫責任,讓程式結構更清晰。
  • LiteDbStockRepository 完整支援:股票清單、日線、缺漏日、技術指標。
  • QuoteFetchService 實作「增量抓取」與「缺漏日記錄」。
  • IndicatorService 計算 KD / MACD,並將結果存回 DB。

到這裡,我們已經有一個完整的「資料流」:
API → Repository → DB → 指標計算。



上一篇
Day23 WPF Style 與介面美化
系列文
30天快速上手製作WPF選股工具 — 從C#基礎到LiteDB與Web API整合24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言