昨天我們已經把下載的股票清單存進 LiteDB,並且可以從本地載入。
今天要進一步加上 搜尋與篩選功能,讓使用者可以:
我們將 IStockRepository
與 IKdSignalService
結合,先把股票載入 ObservableCollection
,再利用 ICollectionView
做即時篩選。
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;
using System.Threading.Tasks;
public class StockFilterViewModel : INotifyPropertyChanged
{
private readonly IStockRepository _repo;
private readonly IKdSignalService _kdService;
public ObservableCollection<StockProfile> Stocks { get; } = new();
public ICollectionView View { get; }
private string _query;
public string Query
{
get => _query;
set { _query = value; OnPropertyChanged(nameof(Query)); View.Refresh(); }
}
private bool _onlyGoldenCross;
public bool OnlyGoldenCross
{
get => _onlyGoldenCross;
set { _onlyGoldenCross = value; OnPropertyChanged(nameof(OnlyGoldenCross)); View.Refresh(); }
}
public ICommand LoadFromLocalCommand { get; }
public ICommand ClearFilterCommand { get; }
public StockFilterViewModel(IStockRepository repo, IKdSignalService kdService)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
_kdService = kdService ?? throw new ArgumentNullException(nameof(kdService));
View = CollectionViewSource.GetDefaultView(Stocks);
View.Filter = FilterPredicate;
LoadFromLocalCommand = new RelayCommand(_ => LoadFromLocal());
ClearFilterCommand = new RelayCommand(_ => { Query = string.Empty; OnlyGoldenCross = false; });
}
private void LoadFromLocal()
{
Stocks.Clear();
foreach (var s in _repo.LoadStocks())
{
Stocks.Add(s);
}
_ = RefreshGoldenCrossCacheAsync(); // 異步計算 KD 訊號
View.Refresh();
}
private bool FilterPredicate(object obj)
{
if (obj is not StockProfile s) return false;
// 1) 關鍵字搜尋
if (!string.IsNullOrWhiteSpace(Query))
{
var q = Query.Trim();
if (!s.Code.Contains(q, StringComparison.OrdinalIgnoreCase) &&
!s.Name.Contains(q, StringComparison.OrdinalIgnoreCase))
return false;
}
// 2) KD 黃金交叉過濾
if (OnlyGoldenCross)
{
if (!_goldenCache.TryGetValue(s.Code, out var isGolden) || !isGolden)
return false;
}
return true;
}
// --- KD 快取 ---
private readonly Dictionary<string, bool> _goldenCache = new();
private async Task RefreshGoldenCrossCacheAsync()
{
_goldenCache.Clear();
foreach (var s in Stocks)
{
bool isGolden = await _kdService.IsGoldenCrossAsync(s.Code);
_goldenCache[s.Code] = isGolden;
}
View.Refresh();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
我們沿用昨天的設計,IKdSignalService
從 日線資料計算 KD,判斷是否為黃金交叉。
(此處簡化為示範版本,正式版請接 API 或從本地資料庫讀日線 K 線來算。)
public interface IKdSignalService
{
Task<bool> IsGoldenCrossAsync(string stockCode);
}
public class DemoKdSignalService : IKdSignalService
{
public Task<bool> IsGoldenCrossAsync(string stockCode)
{
// 這裡應從 LiteDB 或 API 取近 9~20 日收盤價計算 KD
// 範例:直接回傳 false,請在正式版替換
return Task.FromResult(false);
}
}
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="8" Spacing="8">
<TextBox Width="200"
Text="{Binding Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ToolTip="輸入代號或名稱搜尋"/>
<CheckBox Content="只顯示 KD 黃金交叉"
IsChecked="{Binding OnlyGoldenCross, Mode=TwoWay}"/>
<Button Content="載入本地資料"
Command="{Binding LoadFromLocalCommand}" Width="120"/>
<Button Content="清除篩選"
Command="{Binding ClearFilterCommand}" Width="100"/>
</StackPanel>
<DataGrid ItemsSource="{Binding View}" Margin="8"
AutoGenerateColumns="False" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="代號" Binding="{Binding Code}" Width="100"/>
<DataGridTextColumn Header="名稱" Binding="{Binding Name}" Width="200"/>
<DataGridTextColumn Header="產業" Binding="{Binding Industry}" Width="*"/>
<DataGridTextColumn Header="更新時間"
Binding="{Binding LastUpdatedUtc, StringFormat=\{0:yyyy-MM-dd HH:mm\}}"
Width="150"/>
</DataGrid.Columns>
</DataGrid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var repo = new LiteDbStockRepository("StockData.db");
var kdService = new DemoKdSignalService(); // 正式版換成日線資料計算
this.DataContext = new StockFilterViewModel(repo, kdService);
}
}
今天我們完成了: