Unit StandardCandles; Interface Uses DataNodes, GenericTosDataNode, SynchronizedTimers, Classes; Type TSingleCandle = Record Open, High, Low, Close : Double; Volume : Integer; End; TCandleArray = Array Of TSingleCandle; TLastTransition = (ltNew, { We have not yet displayed any data. } ltReset, { Major changes. } ltNewCandle, { Added one new candle to the end. } ltUpdateCurrentCandle { The candle being build has changed. Reserved. Not currently reported. } ); TStandardCandles = Class(TDataNodeWithStringKey) Private BarCounter : TBarCounter; TosData : TGenericTosDataNode; FWorkInProgress : TCandleArray; FCachedHistory : TCandleArray; FHistoricalCandleCount : Integer; FCurrentCandle : TSingleCandle; CurrentCandleValid : Boolean; CacheValid : Boolean; FLastTransition : TLastTransition; FEpoch : Cardinal; FBroadcastChannel : String; FDoNotifySoon : Boolean; Constructor Create(Symbol : String; MinutesPerBar : Byte); Procedure OnTosData; Procedure OnBarData; Procedure ReceiveDebugInfo(Msg : TBroadcastMessage; Owner : TDataNode); Protected Class Function CreateNew(Data : String) : TDataNodeWithStringKey; Override; Public Class Procedure Find(Symbol : String; MinutesPerBar : Byte; OnChange : TThreadMethod; Out Node : TStandardCandles; Out Link : TDataNodeLink); // GetHistory a cached copy. Don't modify it. Function GetHistory : TCandleArray; Property LastTransition : TLastTransition Read FLastTransition; Property HistoricalCandleCount : Integer Read FHistoricalCandleCount; Function GetLastCandleStartTime : TDateTime; { Only meaningful if there is at least on candle. } // This changes when the history changes. Ideally we wouldn't need // this because of the notification event. But it's possible that // one data node is listening to two others, each of which are listening // to this data node. The top level consumer will hear about a change // when only one of the two intermediate nodes has had a chance to // update. The solution is for the middle layer to update it's value // when someone asks for the value. But that can be a lot of work, // so we use the epoch to know if we can use the cached value. Property Epoch : Cardinal Read FEpoch; // Looks up a value in the CSV file, and parses it. This is // automatically done in the constructor. But it can be useful at other // times, in particularly when debugging. Class Function GetHistoricalCandles(Symbol : String; MinutesPerBar : Byte) : TCandleArray; Function GetMinutesPerBar : Integer; // These provide a convienent way to quickly manipulate the value stored // in this datanode. Listeners will be notified of any changes in an // appropriate way, as if the data was real. When you manipulate a // datanode this way, and you send real TOS data (even TOS data for a // different data node) there are no gaurentees made about the results. // Clearly these are only for development, for testing the complicated // formulas which use this data. Procedure DebugReset; { Remove all candles. } Procedure DebugSet(Candles : TCandleArray); { Set the current history to match the input. } Procedure DebugAdd(Candle : TSingleCandle); { Add this one candle to the list. } End; Implementation Uses GenericDataNodes, VolumeWeightedDataNodes, CsvFileData, StandardPlaceHolders, DebugOutput, Math, SysUtils; //////////////////////////////////////////////////////////////////////// // Global //////////////////////////////////////////////////////////////////////// Function RestoreHistory : Boolean; Begin { This is stored in the TVolumeWeightedRealTimeDataNode for historical reasons. It should be stored in some neutral place. } Result := TVolumeWeightedRealTimeDataNode.GetHistoryValid End; //////////////////////////////////////////////////////////////////////// // TStandardCandlesDebugInfo //////////////////////////////////////////////////////////////////////// Type TStandardCandlesDebugInfo = Class(TBroadcastMessage) Public SingleCandle : Boolean; Candles : TCandleArray; Class Function BroadcastChannel(Symbol : String; MinutesPerBar : Byte) : String; End; Class Function TStandardCandlesDebugInfo.BroadcastChannel(Symbol : String; MinutesPerBar : Byte) : String; Begin Result := 'TStandardCandlesDebugInfo.' + Chr(MinutesPerBar) + Symbol End; //////////////////////////////////////////////////////////////////////// // TStandardCandles //////////////////////////////////////////////////////////////////////// Procedure TStandardCandles.DebugReset; Var Candles : TCandleArray; Begin SetLength(Candles, 0); DebugSet(Candles) End; Procedure TStandardCandles.DebugSet(Candles : TCandleArray); Var Info : TStandardCandlesDebugInfo; Begin Info := TStandardCandlesDebugInfo.Create; Info.SingleCandle := False; Info.Candles := Candles; Info.Send(FBroadcastChannel) End; Procedure TStandardCandles.DebugAdd(Candle : TSingleCandle); Var Info : TStandardCandlesDebugInfo; Begin Info := TStandardCandlesDebugInfo.Create; Info.SingleCandle := True; SetLength(Info.Candles, 1); Info.Candles[0] := Candle; Info.Send(FBroadcastChannel) End; Procedure TStandardCandles.ReceiveDebugInfo(Msg : TBroadcastMessage; Owner : TDataNode); Var Info : TStandardCandlesDebugInfo; Begin Info := Msg As TStandardCandlesDebugInfo; If Info.SingleCandle Then Begin If Length(FWorkInProgress) = FHistoricalCandleCount Then { This doesn't grow arbitrarily. Typically we'll start with several days worth of data, then add one day's worth. } SetLength(FWorkInProgress, Length(FWorkInProgress) + BarCounter.GetBarsPerDay); FWorkInProgress[FHistoricalCandleCount] := Info.Candles[0]; Inc(FHistoricalCandleCount); FLastTransition := ltNewCandle End Else Begin FWorkInProgress := Info.Candles; FHistoricalCandleCount := Length(FWorkInProgress); FLastTransition := ltReset End; CacheValid := False; Inc(FEpoch); NotifyListeners End; Constructor TStandardCandles.Create(Symbol : String; MinutesPerBar : Byte); Var Link : TDataNodeLink; Begin // Start at one. Listeners will, by default, all start from 0. But we // may contain data when we first start. FEpoch := 1; Inherited Create; TBarCounter.Find(MinutesPerBar, OnBarData, BarCounter, Link); AddAutoLink(Link); TGenericTosDataNode.Find(Symbol, OnTosData, TosData, Link); AddAutoLink(Link); // This is only used by the debugger. FBroadcastChannel := TStandardCandlesDebugInfo.BroadcastChannel(Symbol, MinutesPerBar); RegisterForBroadcast(FBroadcastChannel, ReceiveDebugInfo); { Ideally we start with the history which was automatically prepaired for us by another task, then add realtime data. But if we start late there will be a big hole. In that case it's better to use no history and just the real time data. } If RestoreHistory Then Begin FWorkInProgress := GetHistoricalCandles(Symbol, MinutesPerBar); FHistoricalCandleCount := Length(FWorkInProgress); If FHistoricalCandleCount > 0 Then FLastTransition := ltReset End End; Function TStandardCandles.GetMinutesPerBar : Integer; Begin Result := BarCounter.MinutesPerBar End; Class Function TStandardCandles.GetHistoricalCandles(Symbol : String; MinutesPerBar : Byte) : TCandleArray; Var AllCandles, CurrentCandle : TStringList; I : Integer; Link : TDataNodeLink; HistoricalData : TFileOwnerDataNode; EncodedData : String; Begin TFileOwnerDataNode.Find('SC_OvernightData.csv', Nil, HistoricalData, Link); EncodedData := HistoricalData.ThreadSafeGet(IntToStr(MinutesPerBar), Symbol); Link.Release; AllCandles := Nil; CurrentCandle := Nil; Try AllCandles := TStringList.Create; AllCandles.Delimiter := ';'; AllCandles.DelimitedText := EncodedData; CurrentCandle := TStringList.Create; CurrentCandle.Delimiter := ':'; SetLength(Result, AllCandles.Count); For I := 0 To Pred(AllCandles.Count) Do With Result[I] Do Begin CurrentCandle.DelimitedText := AllCandles[I]; Open := StrToFloat(CurrentCandle[0]); High := StrToFloat(CurrentCandle[1]); Low := StrToFloat(CurrentCandle[2]); Close := StrToFloat(CurrentCandle[3]); Volume := StrToInt(CurrentCandle[4]) End Except On Ex : Exception Do Begin DebugOutputWindow.AddMessage('Error reading standard candle data. Symbol=' + Symbol + ', MinutesPerBar=' + IntToStr(MinutesPerBar) + ', Classname=' + Ex.ClassName + ': ' + Ex.Message); SetLength(Result, 0) End End; AllCandles.Free; CurrentCandle.Free End; Class Function TStandardCandles.CreateNew(Data : String) : TDataNodeWithStringKey; Begin Assert(Length(Data) >= 1); Result := Create(Copy(Data, 2, MaxInt), Byte(Data[1])) End; Procedure TStandardCandles.OnTosData; Var Last : PTosData; LastPrice : Double; Begin { Ignore missing data. Assume it is small, and the result without the data is close enough. We occasionally go down on a broken TCP/IP socket, and come back up again very quickly. If we are down for a long time, OnBarData will detect it and act accordingly. } If (BarCounter.CurrentBar <> UnknownTime) And (TosData.IsValid) Then Begin Last := TosData.GetLast; If Last.Time >= BarCounter.IgnoreIfBefore Then Begin LastPrice := Last^.Price; With FCurrentCandle Do If Not CurrentCandleValid Then Begin Open := LastPrice; High := LastPrice; Low := LastPrice; Close := LastPrice; Volume := Last^.Size; CurrentCandleValid := True End Else Begin High := Max(High, LastPrice); Low := Min(Low, LastPrice); Close := LastPrice; Volume := Volume + Last^.Size End End End End; Procedure TStandardCandles.OnBarData; Procedure Reset; Begin { Reset } SetLength(FWorkInProgress, 0); SetLength(FCachedHistory, 0); FHistoricalCandleCount := 0; CurrentCandleValid := False; CacheValid := False; FLastTransition := ltReset; Inc(FEpoch); { Strictly speaking, we're not sure anything interesting has changed. But it's always safe to send one reset after another. And detecting the case where this is redundant would be hard. } FDoNotifySoon := True; End; { Reset } Procedure AddCurrentCandle; Begin { AddCurrentCandle } If Length(FWorkInProgress) = FHistoricalCandleCount Then { This doesn't grow arbitrarily. Typically we'll start with several days worth of data, then add one day's worth. } SetLength(FWorkInProgress, Length(FWorkInProgress) + BarCounter.GetBarsPerDay); FWorkInProgress[FHistoricalCandleCount] := FCurrentCandle; Inc(FHistoricalCandleCount); CacheValid := False; CurrentCandleValid := False; End; { AddCurrentCandle } Begin { TStandardCandles.OnBarData } If BarCounter.TimePhase = tpUpdate Then Case BarCounter.LastTransition Of bctNone, bctSkip : { Skip suggests a data error; we were down for a long period of time. For none this is redundant. } Reset; bctFirst : { A fresh start for the data. We know we're not in the middle of a candle. } CurrentCandleValid := False; bctNext, bctEnd : Begin If Not CurrentCandleValid Then { If we have a stock with a candle with no data, we erase everything and restart after the missing candle. This case seems to be ignored by most books, so we report nothing. } Reset Else Begin { Add the candle we just finished to the list. } AddCurrentCandle; FLastTransition := ltNewCandle; Inc(FEpoch); FDoNotifySoon := True End End End Else If BarCounter.TimePhase = tpNotify Then If FDoNotifySoon Then Begin FDoNotifySoon := False; NotifyListeners End End; { TStandardCandles.OnBarData } Class Procedure TStandardCandles.Find(Symbol : String; MinutesPerBar : Byte; OnChange : TThreadMethod; Out Node : TStandardCandles; Out Link : TDataNodeLink); Var TempNode : TDataNodeWithStringKey; Begin FindCommon(TStandardCandles, Chr(MinutesPerBar) + Symbol, OnChange, TempNode, Link); Node := TempNode As TStandardCandles End; Function TStandardCandles.GetHistory : TCandleArray; Begin If Not CacheValid Then Begin FCachedHistory := Copy(FWorkInProgress, 0, FHistoricalCandleCount); CacheValid := True End; Result := FCachedHistory End; Function TStandardCandles.GetLastCandleStartTime : TDateTime; Begin Result := BarCounter.IgnoreIfBefore End; End.