聊到股票或是期貨技術分析,最基本的事情就是將即時報價轉換成K棒,也就是OHLC格式﹔某個時段的開始價 (Open),最高價 (High),最低價 (Low) 與結束價 (Close),時段長短常見的有三分鐘,五分鐘,一小時,或是一天,端看個人喜好。
前文有提到過小弟是透過群益API接收即時報價,他的格式大概就像這樣﹔某一檔股票在幾月幾號幾點幾分幾秒的成交價與成交量。這邊是一秒內所接收的資料量範例。
TX00,20230508 090000 006000,15749.00,12
TX00,20230508 090000 057000,15749.00,3
TX00,20230508 090000 167000,15749.00,2
TX00,20230508 090000 263000,15747.00,1
TX00,20230508 090000 289000,15747.00,1
TX00,20230508 090000 304000,15747.00,1
TX00,20230508 090000 308000,15747.00,1
TX00,20230508 090000 309000,15747.00,4
TX00,20230508 090000 309000,15748.00,1
TX00,20230508 090000 338000,15746.00,2
TX00,20230508 090000 350000,15748.00,2
TX00,20230508 090000 383000,15748.00,1
TX00,20230508 090000 403000,15748.00,1
TX00,20230508 090000 410000,15748.00,1
TX00,20230508 090000 439000,15748.00,1
TX00,20230508 090000 448000,15748.00,1
TX00,20230508 090000 452000,15748.00,3
TX00,20230508 090000 547000,15748.00,1
TX00,20230508 090000 607000,15746.00,1
TX00,20230508 090000 620000,15746.00,1
TX00,20230508 090000 664000,15746.00,1
TX00,20230508 090000 678000,15747.00,1
TX00,20230508 090000 690000,15745.00,1
TX00,20230508 090000 817000,15745.00,1
TX00,20230508 090000 844000,15745.00,1
TX00,20230508 090000 998000,15747.00,1
TX00,20230508 090000 998000,15748.00,5
TX00,20230508 090001 003000,15749.00,6
這邊需要注意的是有時候成交量跟次數會突然爆大量,所以小弟採用RAMDisk與ConcurrentQueue來處理巨量資料產生的現象。另外一個常遇到的問題就是群益API接收即時報價有時候會缺少某些筆的資料,所以往往還需要在收盤之後,重新要求重送當日全部的報價資料做一個回歸校正,至於為什麼即時報價會少資料,小弟還是不清楚原因,可能跟網路頻寬或是設備負載有關吧,還希望網路神人能夠幫忙解惑,只求即時技術分析的精準度不要受到太大影響。
接著我們透過C#的LINQ表示式將即時報價轉為OHLC格式,在這邊我們需要注意到依據不同分析習慣,有退位與進位表示式,也就是說早上九點到九點五分時段裡所有報價匯集成的K棒是應該放在九點這個點還是九點五分這個點。小弟是習慣採用進位表示式,所以以下的內文都以此為準。
private IEnumerable<OHLCBar> GenerateOHLCBars(IEnumerable<Tick> ticks, long barSizeInTicks)
{
TimeSpan naturalTime = new TimeSpan(8, 45, 0);
var bars = from tick in ticks
let barIndexForDay = Math.Floor(tick.Timestamp.TimeOfDay.Subtract(naturalTime).TotalSeconds / barSizeInTicks)
let barBeginDateTime = tick.Timestamp.Date.Add(naturalTime).AddSeconds(barIndexForDay * barSizeInTicks)
let barEndDateTime = tick.Timestamp.Date.Add(naturalTime).AddSeconds((barIndexForDay + 1) * barSizeInTicks)
group tick by new { barBeginDateTime, barEndDateTime, tick.Symbol } into tickGroup
orderby tickGroup.Key.barBeginDateTime
let orderedPrices = tickGroup.OrderBy(t => t.Timestamp).Select(t => t.Price)
select new OHLCBar
{
Symbol = tickGroup.Key.Symbol,
OPEN = orderedPrices.First(),
CLOSE = orderedPrices.Last(),
HIGH = orderedPrices.Max(),
LOW = orderedPrices.Min(),
VOLUME = Convert.ToUInt32(tickGroup.Sum(t => t.Volume)),
BeginTime = tickGroup.Key.barBeginDateTime,
EndTime = tickGroup.Key.barEndDateTime,
Interval = TimeSpan.FromSeconds(barSizeInTicks)
};
return bars;
}
所以匯集不同長度K棒的時候,也需要統一設定為進位格式。
public static IEnumerable<Quote> Aggregate<TQuote>(
this IEnumerable<TQuote> quotes,
PeriodSize newSize)
where TQuote : IQuote
{
// handle no quotes scenario
if (quotes == null || !quotes.Any())
{
return new List<Quote>();
}
// parameter conversion
TimeSpan timeSpan = newSize.ToTimeSpan();
if (timeSpan <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
"Quotes Aggregation must use a usable new size value (see documentation for options).");
}
// return aggregation with RoundUp
return quotes
.OrderBy(x => x.Date)
.GroupBy(x => x.Date.RoundUp(timeSpan))
.Select(x => new Quote
{
Date = x.Key,
Open = x.First().Open,
High = x.Max(t => t.High),
Low = x.Min(t => t.Low),
Close = x.Last().Close,
Volume = x.Sum(t => t.Volume)
});
}
其中的時段單位可以設定為不同長度,端看個人喜好。
internal static TimeSpan ToTimeSpan(this PeriodSize periodSize)
{
return periodSize switch
{
PeriodSize.OneMinute => TimeSpan.FromMinutes(1),
PeriodSize.TwoMinutes => TimeSpan.FromMinutes(2),
PeriodSize.ThreeMinutes => TimeSpan.FromMinutes(3),
PeriodSize.FiveMinutes => TimeSpan.FromMinutes(5),
PeriodSize.FifteenMinutes => TimeSpan.FromMinutes(15),
PeriodSize.ThirtyMinutes => TimeSpan.FromMinutes(30),
PeriodSize.OneHour => TimeSpan.FromHours(1),
PeriodSize.TwoHours => TimeSpan.FromHours(2),
PeriodSize.FourHours => TimeSpan.FromHours(4),
PeriodSize.Day => TimeSpan.FromDays(1),
PeriodSize.Week => TimeSpan.FromDays(7),
_ => TimeSpan.Zero
};
}
另外一個有趣的問題就是如何準確地每五分鐘執行一次K棒資料彙集的動作。不管任何電腦,都存在時間執行誤差的問題,假設第一次執行的時候,是早上九點準,我們要如何保證下一次起來執行的時間是九點五分準,如果我們單純的設定五分鐘,這會導致執行誤差會隨著時間的遞移而越變越長,所以這裡我們透過計算目前電腦滴答(tick)的差距來對齊下一次時間執行時間。
public static double Align(this DateTime dateTime, TimeSpan interval) // 對齊
{
var diff = (interval.Ticks - (dateTime.Ticks % interval.Ticks)) / TimeSpan.TicksPerMillisecond;
return diff == 0 ? 1 : diff;
}
某些市面上的股市軟體會即時地劃出K棒,所以畫面上會看到K棒不斷地變化,直至下一個時間段到來才固定此時間段K棒的長度,一開始小弟也覺得很有趣,不過時間久了,也就乏了。穩定的訊號來源,可靠的技術分析及下單進出策略才是專注的目標。
最後還是祝福大家都能夠賺大錢!