Unit ShortTermCandles; { This unit is aimed at handling requests like "show me stocks that have moved up $0.50 in the last 5 minutes" and "show me stocks which have had unusual volume in the last 10 minutes". These are grouped togather because they use the same underlying picture of the historical data: TShortTermCandles. This view is simpler and more traditional than a lot of what we've done in the past. Each of these is precise to the minute. If we are exactly at a minute boundary then everything is precise. Otherwise we could be up to one minute off. We might also use this to redo the short term running up/down alerts. These could use the same model. The down side is that we'd only use prints to trigger these alerts and we'd loose the confirmation that comes from using the bid and the ask. The up side is that they would be simpler and easier to understand. They would be designed to work like the 5 minute consolidation breakout and similar alerts. The running up alert would compare the current price to the low of the current candle and the lows of the previous two candles. } Interface Implementation Uses DataNodes, GenericTosDataNode, GenericDataNodes, StandardPlaceHolders, Prices, Classes, DateUtils, Types, SysUtils, Math; //////////////////////////////////////////////////////////////////////// // TShortTermCandles provides a slightly different view of a candlestick // chart than we use in other places. This only has today's data. It // starts out empty. It always keeps 1 minute candles. It keeps a // fixed (maximum) number of candles. It does not distinguish between // pre, post, and normal market hours. // // If there are periods of no activity we set the volume of the candle // to 0 and the high, low, open and close of the candle to the closing // value of the previous candle. //////////////////////////////////////////////////////////////////////// Const MaxHistory = 120; // We keep 120 candles, no more. Type TSingleCandle = Record LastPrice, HighPrice, LowPrice : Double; EndingVolume : Integer; // This is the total volume for the day, not just the candle. End; TShortTermCandles = Class(TDataNodeWithStringKey) Private TosData : TGenericTosDataNode; FHistory : Array [0 .. Pred(MaxHistory)] Of TSingleCandle; FHistoryCount : ShortInt; FHistoryNextIndex : ShortInt; // If we add a candle, it goes here. We we want the last candle, it is one before here, possibly wrapping around. FCurrentInitialized : Boolean; FCurrent : TSingleCandle; FCurrentEndTime : TDateTime; FLatestPrintTime : TDateTime; FEpoch : Integer; Procedure AddCandle(Const NewCandle : TSingleCandle); Procedure ExtendCandles(Count : Integer); Procedure NewTosData; Constructor Create(Symbol : String); Protected Class Function CreateNew(Data : String) : TDataNodeWithStringKey; Override; Public Class Procedure Find(Symbol : String; OnChange : TThreadMethod; Out Node : TShortTermCandles; Out Link : TDataNodeLink); Procedure CheckHistoryState; Function GetCandle(Back : Integer) : TSingleCandle; Property HistoryCount : ShortInt Read FHistoryCount; Function GetCurrentCandle : TSingleCandle; Property CurrentCandleValid : Boolean Read FCurrentInitialized; Function TosValid : Boolean; Function TosValue : PTosData; Property Epoch : Integer Read FEpoch; End; Procedure TShortTermCandles.AddCandle(Const NewCandle : TSingleCandle); Begin FHistory[FHistoryNextIndex] := NewCandle; If FHistoryCount < MaxHistory Then Inc(FHistoryCount); FHistoryNextIndex := Succ(FHistoryNextIndex) Mod MaxHistory End; // We keep a fixed maximum number of candles. We add one candle at a time to // the end. We delete candles from the beginning, if required, to maintian // the maximum length. To avoid unnecessary copies, we always write the new // value over the old value that is disappearing, and we update a pointer to // the end of the list. Function TShortTermCandles.GetCandle(Back : Integer) : TSingleCandle; Begin Assert((Back <= FHistoryCount) And (Back > 0)); Result := FHistory[(FHistoryNextIndex - Back + MaxHistory) Mod MaxHistory] End; Function TShortTermCandles.GetCurrentCandle : TSingleCandle; Begin Result := FCurrent End; // There was a period of inactivity. Fill in the blanks. 3 candles back // should always be 3 minutes ago, even if nothing happened since then. Procedure TShortTermCandles.ExtendCandles(Count : Integer); Var I : Integer; ToCopy : TSingleCandle; Begin If FHistoryCount > 0 Then Begin If Count > MaxHistory Then Count := MaxHistory; ToCopy := GetCandle(1); ToCopy.HighPrice := ToCopy.LastPrice; ToCopy.LowPrice := ToCopy.LastPrice; For I := 1 To Count Do AddCandle(ToCopy) End End; // The data can update for two reasons. // 1) If there is a new print then we might have to update the current candle. // 2) As time passes we update the older candles. // a) We might demote the current candle to the list of historical candles. // b) We might get rid of very old historical candles. // This procedure takes care of #2, the time part of the algorithm. Procedure TShortTermCandles.CheckHistoryState; Const MinutesPerDay = 24 * 60; Var CurrentTime : TDateTime; NewEndTime : TDateTime; PastCurrentMinute : Integer; MinutesPassed : Integer; Begin CurrentTime := GetSubmitTime; If CompareDateTime(CurrentTime, FCurrentEndTime) <> LessThanValue Then Begin Inc(FEpoch); NewEndTime := CurrentTime; PastCurrentMinute := MilliSecondOfTheMinute(NewEndTime); // Implicit conversion to a signed type! Otherwise things don't work like we expect when we negate this value. // Go to the next minute. Add anywhere between 1 millisecond and 1 // minute to the current time to do that. (Delphi is only precise to // the millisecond. If we were within 1/2 of a millisecond of the // last next minute, delphi would have rounded that off in the // CompareDateTime and MilliSecondOfTheMinute functions. NewEndTime := IncMinute(IncMilliSecond(NewEndTime, -PastCurrentMinute), 1); If FCurrentEndTime <= 0 Then MinutesPassed := 0 Else MinutesPassed := Round((NewEndTime - FCurrentEndTime) * MinutesPerDay); If FCurrentInitialized Then Begin // What was the current candle is now a historical candle. AddCandle(FCurrent); FCurrentInitialized := False; MinutesPassed := Max(0, Pred(MinutesPassed)) End; ExtendCandles(MinutesPassed); FCurrentEndTime := NewEndTime End End; Procedure TShortTermCandles.NewTosData; Var Last : PTosData; CurrentTime : TDateTime; Begin If TosData.IsValid Then Begin // If the time goes backwards, ignore the current print. Assume it // was reported late. CurrentTime := GetSubmitTime; If CurrentTime >= FLatestPrintTime Then Begin FLatestPrintTime := CurrentTime; CheckHistoryState; Last := TosData.GetLast; FCurrent.LastPrice := Last^.Price; FCurrent.EndingVolume := Last^.Volume; If FCurrentInitialized Then Begin FCurrent.HighPrice := Max(FCurrent.HighPrice, FCurrent.LastPrice); FCurrent.LowPrice := Min(FCurrent.LowPrice, FCurrent.LastPrice) End Else Begin FCurrent.HighPrice := FCurrent.LastPrice; FCurrent.LowPrice := FCurrent.LastPrice End; FCurrentInitialized := True; // We only notify listeners when a valid print appears. Ideally we // should also notify them when the time updates. The "standard // candles" do that. We ignore that case for simplicity. We can // update at other times, on request, but I don't think there's any // need for us to signal an event at any other time. I feel safe // saying that because I know which other classes are using this. NotifyListeners End End End; Constructor TShortTermCandles.Create(Symbol : String); Var Link : TDataNodeLink; Begin Inherited Create; TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link); End; Class Function TShortTermCandles.CreateNew(Data : String) : TDataNodeWithStringKey; Begin Result := Create(Data) End; Class Procedure TShortTermCandles.Find(Symbol : String; OnChange : TThreadMethod; Out Node : TShortTermCandles; Out Link : TDataNodeLink); Var Temp : TDataNodeWithStringKey; Begin FindCommon(Self, Symbol, OnChange, Temp, Link); Node := Temp As TShortTermCandles End; Function TShortTermCandles.TosValid : Boolean; Begin Result := TosData.IsValid End; Function TShortTermCandles.TosValue : PTosData; Begin Result := TosData.GetLast End; //////////////////////////////////////////////////////////////////////// // TTimedMovement // // This data node tells you how much the stock has moved in the last N // minutes. // // This is based on TShortTermCandles and has the same limitations. // It will notify you on each print. It will give you the right value // at other times, but it will not notify you of clock events. It is // only precise to the minute. //////////////////////////////////////////////////////////////////////// Type TTimedMovement = Class(TGenericDataNode) Private Candles : TShortTermCandles; CandleCount : ShortInt; Protected Constructor Create(Params : TParamList); Override; Public Function IsValid : Boolean; Override; Published Function GetDouble : Double; Override; End; Constructor TTimedMovement.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Inherited Create; Assert(Length(Params) = 2, 'Expected params: (Symbol, Count)'); Symbol := Params[0]; CandleCount := Params[1]; TShortTermCandles.Find(Symbol, NotifyListeners, Candles, Link); AddAutoLink(Link) End; Function TTimedMovement.IsValid : Boolean; Begin Candles.CheckHistoryState; Result := (CandleCount <= Candles.HistoryCount) And Candles.TosValid End; Function TTimedMovement.GetDouble : Double; Begin If IsValid Then Result := Candles.TosValue^.Price - Candles.GetCandle(CandleCount).LastPrice Else Result := 0 End; //////////////////////////////////////////////////////////////////////// // TVolumeChange // // This data node tells you how much volume the stock has traded in the // last N minutes. // // This is based on TShortTermCandles and has the same limitations. // It will notify you on each print. It will give you the right value // at other times, but it will not notify you of clock events. It is // only precise to the minute. //////////////////////////////////////////////////////////////////////// Type TVolumeChange = Class(TGenericDataNode) Private Candles : TShortTermCandles; CandleCount : ShortInt; Protected Constructor Create(Params : TParamList); Override; Public Function IsValid : Boolean; Override; Published Function GetInteger : Integer; Override; End; Constructor TVolumeChange.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Inherited Create; Assert(Length(Params) = 2, 'Expected params: (Symbol, Count)'); Symbol := Params[0]; CandleCount := Params[1]; TShortTermCandles.Find(Symbol, NotifyListeners, Candles, Link); AddAutoLink(Link) End; Function TVolumeChange.IsValid : Boolean; Begin // Our algorithm is less than perfect because sometimes, when we first // start, we are looking at yesterday's volume and we don't know it. So // at some time the volume will drop very sharply and give us a negative // number. At that point the right value for tis filter can only be "I // don't know". Result := GetInteger >= 0 End; Function TVolumeChange.GetInteger : Integer; Begin Candles.CheckHistoryState; If (CandleCount <= Candles.HistoryCount) And Candles.TosValid Then Result := Candles.TosValue^.Volume - Candles.GetCandle(CandleCount).EndingVolume Else Result := -1 End; //////////////////////////////////////////////////////////////////////// // TTimedMovementPercent // // This data node tells you how much the stock has moved in the last N // minutes. // // This is based on TTimedMovement and has the same limitations. // It will notify you on each print. It will give you the right value // at other times, but it will not notify you of clock events. It is // only precise to the minute. // // TTimedMovement sends the the actual movement, the difference between // the old value and the current value, to the database. We are sending // the current value to the database in a different piece of code. When // the user asks for the percent change, the database converts the value // to a percent. In this case the user only has the percent option. // This is aimed at certain indexes like the S&P 500. We use percent, // in part, so that the user don't know exactly which indicator we are // looking at. An ETF and various forms of the futures will all tell a // similar story to the actual index, but they all have a different // scale. By only showing the user a percentage, the scale should not // matter. //////////////////////////////////////////////////////////////////////// Type TTimedMovementPercent = Class(TGenericDataNode) Private Candles : TShortTermCandles; CandleCount : ShortInt; Protected Constructor Create(Params : TParamList); Override; Public Function IsValid : Boolean; Override; Published Function GetDouble : Double; Override; End; Constructor TTimedMovementPercent.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Inherited Create; Assert(Length(Params) = 2, 'Expected params: (Symbol, Count)'); Symbol := Params[0]; CandleCount := Params[1]; TShortTermCandles.Find(Symbol, NotifyListeners, Candles, Link); AddAutoLink(Link) End; Function TTimedMovementPercent.IsValid : Boolean; Begin Candles.CheckHistoryState; Result := (CandleCount <= Candles.HistoryCount) And Candles.TosValid End; Function TTimedMovementPercent.GetDouble : Double; Var OriginalPrice : Double; Begin Result := 0; If IsValid Then Try OriginalPrice := Candles.GetCandle(CandleCount).LastPrice; If OriginalPrice <> 0.0 Then Result := Candles.TosValue^.Price * 100.0 / OriginalPrice - 100.0 Except End End; //////////////////////////////////////////////////////////////////////// // TIntraDayRange // // Find the range of the stock in the last N minutes. N is the furthest // back that we will go. If we have some data, but less than N // historical candles, we will use what we have. // // The range is the difference between the highest and the lowest prices // in the given time period. //////////////////////////////////////////////////////////////////////// Type TIntraDayRange = Class(TGenericDataNode) Private Candles : TShortTermCandles; CandleCount : ShortInt; // Cache the highest and the lowest of the historical candles. // Recompute the current candle each time because it is easier that // way, and we wouldn't be saving much work, anyway. FHighest, FLowest : Double; FEpoch : Integer; Procedure UpdateHistoricalHighLow; Protected Constructor Create(Params : TParamList); Override; Public Function IsValid : Boolean; Override; Published Function GetDouble : Double; Override; End; Constructor TIntraDayRange.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Inherited Create; Assert(Length(Params) = 2, 'Expected params: (Symbol, Count)'); Symbol := Params[0]; CandleCount := Params[1]; TShortTermCandles.Find(Symbol, NotifyListeners, Candles, Link); AddAutoLink(Link) End; Function TIntraDayRange.IsValid : Boolean; Begin Candles.CheckHistoryState; Result := (Candles.HistoryCount > 0) Or Candles.CurrentCandleValid End; Procedure TIntraDayRange.UpdateHistoricalHighLow; Var I : Integer; Begin // Assume we've already updated the candles: Candles.CheckHistoryState; If Candles.Epoch <> FEpoch Then Begin FEpoch := Candles.Epoch; FHighest := -MaxDouble; FLowest := MaxDouble; For I := 1 To Min(CandleCount, Candles.HistoryCount) Do With Candles.GetCandle(I) Do Begin If HighPrice > FHighest Then FHighest := HighPrice; If LowPrice < FLowest Then FLowest := LowPrice End End End; Function TIntraDayRange.GetDouble : Double; Begin Result := 0; If IsValid Then Begin UpdateHistoricalHighLow; If Candles.CurrentCandleValid Then With Candles.GetCurrentCandle Do Result := Max(FHighest, HighPrice) - Min(FLowest, LowPrice) Else Result := FHighest - FLowest End End; //////////////////////////////////////////////////////////////////////// // TShortTermCandlesDebug // // //////////////////////////////////////////////////////////////////////// Type TShortTermCandlesDebug = Class(TGenericDataNode) Private Candles : TShortTermCandles; Epoch : Integer; Value : String; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; Public Function IsValid : Boolean; Override; Published Function GetString : String; Override; End; Procedure TShortTermCandlesDebug.NewData; Var NewEpoch : Integer; Begin NewEpoch := Candles.Epoch; If NewEpoch <> Epoch Then Begin Epoch := NewEpoch; Value := ''; If Candles.HistoryCount >= 1 Then Value := Value + FormatPrice(Candles.GetCandle(1).LastPrice); If Candles.HistoryCount >= 2 Then Value := Value + ', ' + FormatPrice(Candles.GetCandle(2).LastPrice); If Candles.HistoryCount >= 3 Then Value := Value + ', ' + FormatPrice(Candles.GetCandle(3).LastPrice); If Candles.HistoryCount >= 4 Then Value := Value + ', ' + FormatPrice(Candles.GetCandle(4).LastPrice); If Candles.HistoryCount >= 5 Then Value := Value + ', ' + FormatPrice(Candles.GetCandle(5).LastPrice); If Candles.HistoryCount >= 10 Then Value := Value + '; ' + FormatPrice(Candles.GetCandle(10).LastPrice); If Candles.HistoryCount >= 15 Then Value := Value + '; ' + FormatPrice(Candles.GetCandle(15).LastPrice); If Candles.HistoryCount >= 30 Then Value := Value + '; ' + FormatPrice(Candles.GetCandle(30).LastPrice) Else Value := Value + ' (' + IntToStr(Candles.HistoryCount) + ')'; Value := Value + ' ' + TimeToStr(Candles.FLatestPrintTime) + ' ' + TimeToStr(Candles.FCurrentEndTime); NotifyListeners End End; Constructor TShortTermCandlesDebug.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Inherited Create; Assert(Length(Params) = 1, 'Expected params: (Symbol)'); Symbol := Params[0]; TShortTermCandles.Find(Symbol, NewData, Candles, Link); AddAutoLink(Link); Epoch := -1; DoInCorrectThread(NewData) End; Function TShortTermCandlesDebug.IsValid : Boolean; Begin Result := True End; Function TShortTermCandlesDebug.GetString : String; Begin Result := Value End; //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// Initialization TGenericDataNodeFactory.StoreFactory('TimedMovement5', TGenericDataNodeFactory.CreateWithArgs(TTimedMovement, StandardSymbolPlaceHolder, 5)); TGenericDataNodeFactory.StoreFactory('TimedMovement10', TGenericDataNodeFactory.CreateWithArgs(TTimedMovement, StandardSymbolPlaceHolder, 10)); TGenericDataNodeFactory.StoreFactory('TimedMovement15', TGenericDataNodeFactory.CreateWithArgs(TTimedMovement, StandardSymbolPlaceHolder, 15)); TGenericDataNodeFactory.StoreFactory('TimedMovement30', TGenericDataNodeFactory.CreateWithArgs(TTimedMovement, StandardSymbolPlaceHolder, 30)); TGenericDataNodeFactory.StoreFactory('VolumeChange5', TGenericDataNodeFactory.CreateWithArgs(TVolumeChange, StandardSymbolPlaceHolder, 5)); TGenericDataNodeFactory.StoreFactory('VolumeChange10', TGenericDataNodeFactory.CreateWithArgs(TVolumeChange, StandardSymbolPlaceHolder, 10)); TGenericDataNodeFactory.StoreFactory('VolumeChange15', TGenericDataNodeFactory.CreateWithArgs(TVolumeChange, StandardSymbolPlaceHolder, 15)); TGenericDataNodeFactory.StoreFactory('VolumeChange30', TGenericDataNodeFactory.CreateWithArgs(TVolumeChange, StandardSymbolPlaceHolder, 30)); TGenericDataNodeFactory.StoreFactory('QQQQ5', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'QQQQ', 5)); TGenericDataNodeFactory.StoreFactory('QQQQ10', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'QQQQ', 10)); TGenericDataNodeFactory.StoreFactory('QQQQ15', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'QQQQ', 15)); TGenericDataNodeFactory.StoreFactory('QQQQ30', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'QQQQ', 30)); TGenericDataNodeFactory.StoreFactory('SPY5', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'SPY', 5)); TGenericDataNodeFactory.StoreFactory('SPY10', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'SPY', 10)); TGenericDataNodeFactory.StoreFactory('SPY15', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'SPY', 15)); TGenericDataNodeFactory.StoreFactory('SPY30', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'SPY', 30)); TGenericDataNodeFactory.StoreFactory('DIA5', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'DIA', 5)); TGenericDataNodeFactory.StoreFactory('DIA10', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'DIA', 10)); TGenericDataNodeFactory.StoreFactory('DIA15', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'DIA', 15)); TGenericDataNodeFactory.StoreFactory('DIA30', TGenericDataNodeFactory.CreateWithArgs(TTimedMovementPercent, 'DIA', 30)); TGenericDataNodeFactory.StoreFactory('IntraDayRange15', TGenericDataNodeFactory.CreateWithArgs(TIntraDayRange, StandardSymbolPlaceHolder, 15)); TGenericDataNodeFactory.StoreFactory('IntraDayRange30', TGenericDataNodeFactory.CreateWithArgs(TIntraDayRange, StandardSymbolPlaceHolder, 30)); TGenericDataNodeFactory.StoreFactory('IntraDayRange60', TGenericDataNodeFactory.CreateWithArgs(TIntraDayRange, StandardSymbolPlaceHolder, 60)); TGenericDataNodeFactory.StoreFactory('IntraDayRange120', TGenericDataNodeFactory.CreateWithArgs(TIntraDayRange, StandardSymbolPlaceHolder, 120)); TGenericDataNodeFactory.StoreStandardFactory('ShortTermCandlesDebug', TShortTermCandlesDebug); End.