Unit SynchronizedTimers; { This is a clock which tells you the number of minutes that have passed since the open. This is the floor of the actual number, making it an integer. This is not driven by the local time. This is driven by some outside force. The obvious way to drive this is to update the time before reporting each TOS event. This way you can build candles in the normal way. Even if the datafeed is behind, or your clock is not perfectly matched to the datafeed, the bars will be broken at the right time. } Interface Uses DataNodes, Classes, Types; Const { Initially we store this for the time, before we know the real time. } UnknownTime = MaxInt; Type { For the candle/bar data, we pay particular attention to making sure that multiple data nodes that fire at the same time all see the same data. For example, If I'm looking at the number of up-candles on a 5, 10, and 15 minute chart for the same alert, they will all report consistantly. This is always a problem with the way that our data node work, but here it's most obvious, because so many things are all keyed to go off togather. The data nodes in this unit include a property called TimePhase. Each outgoing notification will go out twice. First we update our own internals. Then we set TimePhase to tpUpdate and notify our listeners, then we set TimePhase to tpNotify, and we notify our listeners again. This is only part of the soltuion. The assumption is that the next level of listners (each presumably holding candles for one specific stock for one specific time frame) will update their data on the tpUpdate phase, then notify their listeners on the tpNotify phase. Of course, this trick only works once. The next level up doesn't know about the two phases. However, we expect these data nodes to have their own tricks. In paricular, some type of epoch counter, so they know if someone is reading from them when an update to them is pending. These two seperate methods come from the fact that we designed this unit to be completely seperate from the next level up, thinking we could make it more generically useful. The original idea worked well, when people only looked at one chart at a time. But when we had to make different charts work togather, we did a quick patchwork, and added TimePhase. } TTimePhase = (tpNotActive, tpUpdate, tpNotify); { This is the basic timer that drives all of the others. We report once each time the integer number of minutes changes. This can cause a lot of work because so many people are all listening to the same clock. This is different from the way that we process most data. Most data nodes are keyed to a particular stock, and are only effected by a message related to that stock. This data node potentially makes a lot of work out of one event, where we usually spread the work out more. This does not explicitly deal with a datafeed that goes down. There is no valid bit. The clock can never go backwards. Assume that a late print is something to be ignored. (Possibly it could be inserted into the right place, but that's way beyond the capabilities supported by this class.) "early" prints are rare. These are filtered out. These could cause serious danamge since we are incapable of going back. } TSynchronizedTimer = Class(TDataNode) Private FStartTime : TDateTime; { When this candle started. This is the ideal start time, which might be before the actual signal came in. } FNextToReport : TDateTime; { This is the ideal start time of the next candle. } FMinutes : Integer; { Minutes from midnight of the start of the candle. } FTimePhase : TTimePhase; Procedure SetTimeReal(RemoteTime : TDateTime); Constructor Create; Public { Listeners use this like a standard data node. Find an instance. When the number of minutes on the clock changes, the user will be notified, and the Minutes property will contain the right number. } Class Procedure Find(OnChange : TThreadMethod; Out Node : TSynchronizedTimer; Out Link : TDataNodeLink); Property Minutes : Integer Read FMinutes; Property StartTime : TDateTime Read FStartTime; Property TimePhase : TTimePhase Read FTimePhase; { Producers call SetTime with the time. RemoteTime implies that this probably comes from a datafeed, not from our local clock. } Class Procedure SetTime(RemoteTime : TDateTime); { This compares RemoteTime to the real time according to our local clock. This will give us a warning if something is fishy, and sometimes ignore the request. The debugger/simulator should always call SetTime(), as that time will probably be off from the real time. This is a tough one to implement. The assumption is that RemoteTime should never be that far ahead of our clock. If we receive a print with tomorrows date, that would kill us! I think this actually happened, before we added this test, but I can't be sure. } Class Procedure SafeSetTime(RemoteTime : TDateTime); End; TBarCounterTransition = (bctNone, { This is the intial state. } bctFirst, { We have started a valid bar, but previously we were not on a valid bar. This is the first bar we've seen today. } bctNext, { We have moved directly from one valid bar to the next. } bctEnd, { We gracefully transitioned from a valid bar to the end of the day. } bctSkip); { Somehow the clock skipped forward in an unexpected manner. } { Note the abscence of a backward item. Some old prints are unavoidable. The consumer should look for these and ignore them. Note the abscence of a skip/end hybrid. We can't really detect an ungraceful shift to the end very well, so we often give it the benefit of the doubt. } { This is only active during market hours. It resets itself to say no data after the close, and doesn't change back until the open. So the bar number before the first market print and after the first post-market print is UnknownTime. It always starts counting from when the market opens. So an hour bar would go from 6:30 - 7:30. The first bar is always 0. } TBarCounter = Class(TDataNodeWithStringKey) Private FCurrentBar, FPreviousBar : Integer; FLastTransition : TBarCounterTransition; FMinutesPerBar : Integer; FIgnoreIfBefore : TDateTime; FTimePhase : TTimePhase; NotifyRequired : Boolean; BasicTimer : TSynchronizedTimer; Constructor Create(MinutesPerBar : Byte); Procedure OnBasicTimer; Protected Class Function CreateNew(Data : String) : TDataNodeWithStringKey; Override; Public Class Procedure Find(MinutesPerBar : Byte; OnChange : TThreadMethod; Out Node : TBarCounter; Out Link : TDataNodeLink); Property CurrentBar : Integer Read FCurrentBar; Property PreviousBar : Integer Read FPreviousBar; Property LastTransition : TBarCounterTransition Read FLastTransition; Property IgnoreIfBefore : TDateTime Read FIgnoreIfBefore; Property MinutesPerBar : Integer Read FMinutesPerBar; Function GetBarsPerDay : Integer; Property TimePhase : TTimePhase Read FTimePhase; End; Implementation Uses DebugOutput, Math, DateUtils, SysUtils; Const TimeOfOpen = 6 * 60 + 30; // 6:30am, Pacific time. MinutesOfTrading = 6 * 60 + 30; // 6 1/2 hours. //////////////////////////////////////////////////////////////////////// // TSynchronizedTimer //////////////////////////////////////////////////////////////////////// Var Instance : TSynchronizedTimer; { There is only one of these. An individual stock might not trade right around the expected clock signal, but other stocks will trade. Durning normal market hours, we expect at least on stock to trade right around each minute boundary. All one minute bars should update at the same time. } Class Procedure TSynchronizedTimer.Find(OnChange : TThreadMethod; Out Node : TSynchronizedTimer; Out Link : TDataNodeLink); Begin GetCreateDestroyLock.Enter; Try If Not Assigned(Instance) Then Instance := TSynchronizedTimer.Create; Node := Instance; Link := Node.CreateLink(OnChange) Finally GetCreateDestroyLock.Leave End End; Class Procedure TSynchronizedTimer.SetTime(RemoteTime : TDateTime); Begin If Not Assigned(Instance) Then Begin GetCreateDestroyLock.Enter; Try If Not Assigned(Instance) Then Instance := TSynchronizedTimer.Create Finally GetCreateDestroyLock.Leave End End; Instance.SetTimeReal(RemoteTime) End; Var FishyPrintCount : Integer = 0; DisplayFishyPrintCount : Integer = 1; WorstFishyPrint : Double = 0.0; Class Procedure TSynchronizedTimer.SafeSetTime(RemoteTime : TDateTime); Const OneSecond = 1.0 / 24.0 / 60.0 / 60.0; MaxError = OneSecond * 5.0; Var Error : Double; Begin Error := RemoteTime - Now; // We only worry about prints that are in the future. Prints that are in // the past are actually valid, and are filtered out later. If Error > MaxError Then Begin Inc(FishyPrintCount); If (FishyPrintCount >= DisplayFishyPrintCount) Or (Error > WorstFishyPrint) Then Begin DebugOutput.DebugOutputWindow.AddMessage(Format('%d fishy prints. Last: %.7g seconds ahead. Worst: %.7g seconds ahead', [FishyPrintCount, Error / OneSecond, WorstFishyPrint / OneSecond])); If Error > WorstFishyPrint Then WorstFishyPrint := Error; DisplayFishyPrintCount := FishyPrintCount * 2 End End Else SetTime(RemoteTime) End; Constructor TSynchronizedTimer.Create; Begin Inherited Create(True); FMinutes := UnknownTime End; Procedure TSynchronizedTimer.SetTimeReal(RemoteTime : TDateTime); Var MinutesSinceMidnight : Integer; Begin //If RemoteTime >= FNextToReport Then If CompareDateTime(RemoteTime, FNextToReport) <> LessThanValue Then Begin MinutesSinceMidnight := MinuteOfTheDay(RemoteTime); FStartTime := IncMinute(DateOf(RemoteTime), MinutesSinceMidnight); FNextToReport := IncMinute(FStartTime); FMinutes := MinutesSinceMidnight - TimeOfOpen; FTimePhase := tpUpdate; NotifyListeners; FTimePhase := tpNotify; NotifyListeners; FTimePhase := tpNotActive End End; //////////////////////////////////////////////////////////////////////// // TBarCounter //////////////////////////////////////////////////////////////////////// Constructor TBarCounter.Create(MinutesPerBar : Byte); Var Link : TDataNodeLink; Begin Assert(MinutesPerBar > 0); FMinutesPerBar := MinutesPerBar; Inherited Create; TSynchronizedTimer.Find(OnBasicTimer, BasicTimer, Link); AddAutoLink(Link); FPreviousBar := UnknownTime; FCurrentBar := UnknownTime End; Function TBarCounter.GetBarsPerDay : Integer; Begin Result := Succ(Pred(MinutesOfTrading) Div FMinutesPerBar) End; Class Function TBarCounter.CreateNew(Data : String) : TDataNodeWithStringKey; Begin Assert(Length(Data) = 1); Result := Create(Byte(Data[1])) End; Class Procedure TBarCounter.Find(MinutesPerBar : Byte; OnChange : TThreadMethod; Out Node : TBarCounter; Out Link : TDataNodeLink); Var TempNode : TDataNodeWithStringKey; Begin FindCommon(TBarCounter, Chr(MinutesPerBar), OnChange, TempNode, Link); Node := TempNode As TBarCounter End; Procedure TBarCounter.OnBasicTimer; Var NewBar : Integer; Procedure NotifyListeners1; Begin { NotifyListeners1 } FTimePhase := tpUpdate; NotifyRequired := True; NotifyListeners End; { NotifyListeners1 } Procedure NotifyListeners2; Begin { NotifyListeners2 } If NotifyRequired Then Begin FTimePhase := tpNotify; NotifyListeners End; FTimePhase := tpNotActive End; { NotifyListeners2 } Begin { TBarCounter.OnBasicTimer } If BasicTimer.TimePhase = tpUpdate Then Begin FTimePhase := tpUpdate; If (BasicTimer.Minutes < 0) Or (BasicTimer.Minutes >= MinutesOfTrading) Then Begin If Not (FLastTransition In [bctNone, bctEnd]) Then Begin FCurrentBar := UnknownTime; FLastTransition := bctEnd; NotifyListeners1 End End Else Begin FIgnoreIfBefore := BasicTimer.FStartTime; NewBar := BasicTimer.Minutes Div FMinutesPerBar; If NewBar <> FCurrentBar Then Begin If FLastTransition = bctNone Then FLastTransition := bctFirst Else If NewBar = Succ(FCurrentBar) Then FLastTransition := bctNext Else FLastTransition := bctSkip; FPreviousBar := FCurrentBar; FCurrentBar := NewBar; NotifyListeners1 End End End Else If BasicTimer.TimePhase = tpNotify Then NotifyListeners2 End; { TBarCounter.OnBasicTimer } End.