Unit IntermediateRunning; Interface Uses GenericL1DataNode, GenericDataNodes, AlertBase; Type THistoricalCandle = Record Extreme : Double; Time : Double; MaxPreviousStrength : Double End; TRecentCandles = Record { 6 should normally be sufficient. But we're having trouble around the case where you throw the oldest one away, so we have 7. } Candle : Array [0..6] Of THistoricalCandle; Count : Byte End; TComparison = (cLessReportable, cEqual, cMoreReportable); TIntermediateRunning = Class(TAlert) Private FUp : Boolean; L1Data : TGenericL1DataNode; VolatilityData : TGenericDataNode; RecentHistory : TRecentCandles; CurrentGoal : Record Extreme : Double; // This is the last point we are comparing to // when we determine the quality. ToDisplay : Double; // We display this point, which may be slighly // higher. We discount the spread when // calculating the quality, but not when // displaying the result. Time : Double; // The first time that the Extreme gets higher, // we record that price, the that price plus the // spread, and the time. End; CurrentSource : Record Extreme : Double; // This is the point that we will record for this // candle as a possible starting place. Time : Double; // The is the last time that we saw this pice. End; LastSourceWasExtreme : Boolean; LastPushTime : TDateTime; CurrentValid : Boolean; Volatility : Double; Procedure ClearData; Procedure NewVolatilityData; Procedure NewL1Data; Procedure NewTosData; Procedure CompressRecentHistory; Procedure CheckForAlert; Procedure UpdateCandles(TimeOnly : Boolean); Function Compare(Older, Newer : Double) : TComparison; Function Difference(Older, Newer : Double) : Double; Protected Constructor Create(Params : TParamList); Override; End; Implementation Uses DataNodes, GenericTosDataNode, StandardPlaceHolders, Prices, Math, StrUtils, SysUtils; Const CandleWidth = 1.0 / 24.0 / 60.0 / 2.0; // 30 seconds. Procedure TIntermediateRunning.ClearData; Begin RecentHistory.Count := 0; CurrentValid := False End; Procedure TIntermediateRunning.NewVolatilityData; Var NewValue : Double; Begin If VolatilityData.IsValid Then Begin NewValue := VolatilityData.GetDouble; If NewValue <> Volatility Then Begin Volatility := NewValue; ClearData End End Else Begin Volatility := 0; ClearData End End; Function TIntermediateRunning.Compare(Older, Newer : Double) : TComparison; Begin If Newer = Older Then Result := cEqual Else If (Newer > Older) = FUp Then Result := cMoreReportable Else Result := cLessReportable End; Function TIntermediateRunning.Difference(Older, Newer : Double) : Double; Begin If FUp Then Result := Newer - Older Else Result := Older - Newer End; Constructor TIntermediateRunning.Create(Params : TParamList); Var Symbol : String; Factory : IGenericDataNodeFactory; Link : TDataNodeLink; TosData : TGenericTosDataNode; // We don't use the data. We just use this as a timer. Begin Assert(Length(Params) = 2, 'Expected: (Symbol, Up)'); Symbol := Params[0]; FUp := Params[1]; Inherited Create; TGenericL1DataNode.Find(Symbol, NewL1Data, L1Data, Link); AddAutoLink(Link); TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('TickVolatility').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(NewVolatilityData, VolatilityData, Link); AddAutoLink(Link); DoInCorrectThread(NewVolatilityData); End; Procedure TIntermediateRunning.CheckForAlert; Var Success : Boolean; Strength, PriceChange : Double; StartTime : TDateTime; EndTime : TDateTime; Procedure FindBest; Const NoValue = -1; { The volatility is expressed in the number of $ the price can be expected to move in 15 minutes. We are looking at smaller times, so we scale things to the right time. This was we know what values we should consider unusual. } TimeUnit = 1.0 / 24.0 / 4.0; // 15 minutes. Var CurrentStrength : Double; CurrentIndex : Integer; BestIndex : Integer; LastComparison : Integer; Begin { FindBest } EndTime := CurrentGoal.Time; BestIndex := NoValue; LastComparison := NoValue; For CurrentIndex := Low(RecentHistory.Candle) To Pred(RecentHistory.Count) Do With RecentHistory.Candle[CurrentIndex] Do { If the time passed is too small, this object is just not good at evaluating it. Leave it to the short term running up / down to report the event. It is even possible for this value to be negative, and we certainly don't want to report that! } If Time + CandleWidth < CurrentGoal.Time Then Begin CurrentStrength := Difference(Extreme, CurrentGoal.Extreme) / Sqrt((EndTime - Time) / TimeUnit) / Volatility; If (BestIndex = NoValue) Or (CurrentStrength > Strength) Then Begin Strength := CurrentStrength; BestIndex := CurrentIndex End; If BestIndex <> NoValue Then If Strength <= MaxPreviousStrength Then BestIndex := NoValue; LastComparison := CurrentIndex End; If BestIndex = NoValue Then Success := False Else Begin Success := True; StartTime := RecentHistory.Candle[BestIndex].Time; PriceChange := CurrentGoal.ToDisplay - RecentHistory.Candle[BestIndex].Extreme; RecentHistory.Candle[LastComparison].MaxPreviousStrength := Max(RecentHistory.Candle[LastComparison].MaxPreviousStrength, Strength) End End; { FindBest } Const MinReportableStrength = 2; Begin { TIntermediateRunning.CheckForAlert } If Not CurrentValid Then Exit; If GetSubmitTime - CurrentGoal.Time > 2 * CandleWidth Then { Event is old. Displaying an alert would be confusing at best. This can only happen when a stock is barely active, so it's okay to throw the alert away. Note: This value can easily be one candle width even if we have a lot of prints, if the goal value went up right at the beginning of the candle. That's why we allow 2 candle widths. } Exit; FindBest; If Success And (Strength > MinReportableStrength) Then Report('Running ' + IfThen(FUp, 'up', 'down') + ': ' + FormatPrice(PriceChange, True) + ' in ' + DurationString(StartTime, EndTime) {+ ' ending ' + TimeToStr(EndTime) + ' at ' + FormatPrice(CurrentGoal.Extreme)}, Strength) End; { TIntermediateRunning.CheckForAlert } Procedure TIntermediateRunning.CompressRecentHistory; Procedure RemoveItem(Index : Integer); Var I : Integer; Begin { RemoveItem } // Index cannot be 0! { We could not pass this point (going left) unless the strength was higher than this. The implication is that the limit for everything further left is at least this high. When we delete this entry, we still don't want anyone to get past this point without the required strength. } RecentHistory.Candle[Pred(Index)].MaxPreviousStrength := Max(RecentHistory.Candle[Pred(Index)].MaxPreviousStrength, RecentHistory.Candle[Index].MaxPreviousStrength); For I := Index To Pred(High(RecentHistory.Candle)) Do RecentHistory.Candle[I] := RecentHistory.Candle[Succ(I)]; Dec(RecentHistory.Count) End; { RemoveItem } Var I : Integer; Weight : Integer; CurrentScore, BestScore : Double; BestIndex : Integer; ComparisonPrice : Double; Begin { TIntermediateRunning.CompressRecentHistory } If (RecentHistory.Count <> Length(RecentHistory.Candle)) Or (Not CurrentValid) Then { We're not full. This could be an assertion failed. But if we're not full, we won't do the push. Still, we don't expect it. } Exit; Weight := 1; BestScore := MaxDouble; BestIndex := -1; { Avoid compiler warning. } { Look at each gap. } For I := 1 To High(RecentHistory.Candle) Do Begin CurrentScore := (RecentHistory.Candle[I].Time - RecentHistory.Candle[Pred(I)].Time) * Weight; Weight := Weight * 2; If CurrentScore < BestScore Then Begin BestScore := CurrentScore; BestIndex := I End End; If BestIndex = 1 Then { If we want to remove the first, oldest gap, we always remove the later print. This keeps the age of the oldest print from shrinking. We couldn't use the algorithm below because we'd be reading off the left side of the array. } RemoveItem(1) Else Begin { To remove the gap, we have to remove one of the two items immediately adjacent to the gap. We have to choose which of these two items to keep. We look at the next item on either side of these items. We try keep the item which is most centered relative to the items around it. } If BestIndex = High(RecentHistory.Candle) Then { To deal with the most current historical item, we have to compare it to the item which is building, and not yet in the history yet. } ComparisonPrice := CurrentSource.Extreme Else ComparisonPrice := RecentHistory.Candle[Succ(BestIndex)].Extreme; ComparisonPrice := (RecentHistory.Candle[BestIndex - 2].Extreme + ComparisonPrice) / 2; If Abs(ComparisonPrice - RecentHistory.Candle[BestIndex].Extreme) >= Abs(ComparisonPrice - RecentHistory.Candle[Pred(BestIndex)].Extreme) Then { The later one is further from the average, so delete it. } RemoveItem(BestIndex) Else { The earlier one gets it. } RemoveItem(Pred(BestIndex)) End End; { TIntermediateRunning.CompressRecentHistory } { Check on time. See if we need to move data from the current candle to the historical candles, etc. If we make the move, then we check for an alert, and then we create a new current candle. Otherwise, assuming that TimeOnly is false, we update the current candle from the L1. TimeOnly is an optimization so we don't have to do as much work if we are called from a timer, or a TOS update. } Procedure TIntermediateRunning.UpdateCandles(TimeOnly : Boolean); Const MaxHistory = 1.0 / 24.0 / 60.0 * 25.0; // 25 minutes. Var GoalValue, SourceValue : Double; Current : PL1Data; DataValid, StartNewCandle : Boolean; CurrentTime : TDateTime; I : Integer; Begin If Volatility = 0 Then Exit; CurrentTime := GetSubmitTime; If TimeOnly Then Begin Current := Nil; DataValid := CurrentValid End Else Begin If L1Data.IsValid Then Begin Current := L1Data.GetCurrent; DataValid := (Current.BidPrice > 0) And (Current.AskPrice > 0) End Else Begin Current := Nil; DataValid := False End; If Not DataValid Then ClearData End; If DataValid Then Begin StartNewCandle := Not CurrentValid; If CurrentValid And (CurrentTime - LastPushTime >= CandleWidth) Then { Push the candle into the history list and start a new candle. } Begin { Start by deleting recent candles which will be obscured by the new candle. } I := Pred(RecentHistory.Count); While (I >= Low(RecentHistory.Candle)) And (Compare(RecentHistory.Candle[I].Extreme, CurrentSource.Extreme) <> cMoreReportable) Do Begin Dec(I); If I >= Low(RecentHistory.Candle) Then RecentHistory.Candle[I].MaxPreviousStrength := Max(RecentHistory.Candle[I].MaxPreviousStrength, RecentHistory.Candle[Succ(I)].MaxPreviousStrength) End; RecentHistory.Count := Succ(I); { Delete anything that is too old. Should be one at most one item at a time, so let's keep it simple. } If (RecentHistory.Count > 0) And (RecentHistory.Candle[0].Time < CurrentTime - MaxHistory) Then Begin Dec(RecentHistory.Count); For I := 1 To RecentHistory.Count Do RecentHistory.Candle[Pred(I)] := RecentHistory.Candle[I] End; { If we still don't have room in the list, we need to combine two entries to make room. } If RecentHistory.Count = Length(RecentHistory.Candle) Then CompressRecentHistory; { If the extreme price was also the closing price, update the time accordingly. } If LastSourceWasExtreme Then CurrentSource.Time := CurrentTime; { Store the current candle at the end of the list. } With RecentHistory.Candle[RecentHistory.Count] Do Begin Extreme := CurrentSource.Extreme; Time := CurrentSource.Time; MaxPreviousStrength := 0 End; Inc(RecentHistory.Count); { Now compare the current data to the new history. } CheckForAlert; { Start a new candle. } StartNewCandle := True End; { Update from L1 if required } If StartNewCandle Or Not TimeOnly Then Begin If Not Assigned(Current) Then Current := L1Data.GetCurrent; If Compare(Current^.BidPrice, Current^.AskPrice) = cMoreReportable Then { The spread always works against us. We choose these values so that the direction from the SourceValue to the GoalValue is the opposite of what the alert is looking for. } Begin GoalValue := Current^.BidPrice; SourceValue := Current^.AskPrice End Else Begin GoalValue := Current^.AskPrice; SourceValue := Current^.BidPrice End; If StartNewCandle Then Begin { Create a new pair of current candles. } LastPushTime := CurrentTime; CurrentGoal.Extreme := GoalValue; CurrentGoal.ToDisplay := SourceValue; CurrentGoal.Time := CurrentTime; CurrentSource.Extreme := SourceValue; CurrentSource.Time := CurrentTime; LastSourceWasExtreme := True; CurrentValid := True End Else If Not TimeOnly Then Begin { Update the current candles. } If Compare(CurrentGoal.Extreme, GoalValue) = cMoreReportable Then Begin CurrentGoal.Extreme := GoalValue; CurrentGoal.ToDisplay := SourceValue; CurrentGoal.Time := CurrentTime End; If Compare(CurrentSource.Extreme, SourceValue) <> cMoreReportable Then Begin CurrentSource.Extreme := SourceValue; CurrentGoal.Time := CurrentTime; LastSourceWasExtreme := True End Else LastSourceWasExtreme := False End End End End; Procedure TIntermediateRunning.NewL1Data; Begin UpdateCandles(False) End; Procedure TIntermediateRunning.NewTosData; Begin UpdateCandles(True) End; //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// Initialization TGenericDataNodeFactory.StoreFactory('RunningUpIntermediate', TGenericDataNodeFactory.CreateWithArgs(TIntermediateRunning, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('RunningDownIntermediate', TGenericDataNodeFactory.CreateWithArgs(TIntermediateRunning, StandardSymbolPlaceHolder, False)); End.