Unit Pullbacks; Interface Implementation Uses GenericDataNodes, AlertBase, GenericL1DataNode, GenericTosDataNode, DataNodes, NewDayDetector, StandardPlaceHolders, Math; Const { This should probably depend on the stock. The distance can't be negative, or we are reporting something really wrong, like the open is less than low. The distance can't be 0 or we'll have a divide by 0 problem. Maybe a similar problem if the distance is very, very small. If this is too small we will report a lot of noise in the morning. } MinDistance = 0.01; //////////////////////////////////////////////////////////////////////// // TPullbackCommon //////////////////////////////////////////////////////////////////////// Type TPullbackCommon = Class(TAlert) Protected FMinPullback : Double; FMinPullbackDisplay : String; L1Data : TGenericL1DataNode; TosData : TGenericTosDataNode; State : (psApproachingExtreme, // Not primed. When we make a new extreme, we will prime. psPullingBack); // Primed. As soon as the pullback is big enough, we signal an alert. LastExtreme : Double; LastExtremeValid : Boolean; FilterUntil : TDateTime; Constructor Create(Params : TParamList); Override; Procedure CheckForNewExtreme; Virtual; Abstract; Procedure CheckForPullback; Virtual; Abstract; Procedure ResetTimeFilter; Function TimeFilterOkay : Boolean; Private Procedure OnNewDay; Procedure OnNewL1Data; Procedure OnNewTosData; End; Constructor TPullbackCommon.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, min pullback as decimal fraction, min pullback as string)'); Symbol := Params[0]; FMinPullback := Params[1]; FMinPullbackDisplay := Params[2]; Inherited Create; TNewDay.Find(Symbol, OnNewDay, Link); AddAutoLink(Link); TGenericL1DataNode.Find(Symbol, OnNewL1Data, L1Data, Link); AddAutoLink(Link); TGenericTosDataNode.Find(Symbol, OnNewTosData, TosData, Link); AddAutoLink(Link) End; Procedure TPullbackCommon.ResetTimeFilter; Const { After a new day starts, wait 90 seconds before reporting any alerts. } PauseTime = 90.0 / 24.0 / 60.0 / 60.0; Begin FilterUntil := GetSubmitTime + PauseTime End; Function TPullbackCommon.TimeFilterOkay : Boolean; Begin Result := GetSubmitTime > FilterUntil End; Procedure TPullbackCommon.OnNewDay; Begin ResetTimeFilter; LastExtremeValid := False; CheckForNewExtreme; // We start in the not primed state. // If we start in the middle of the day, we may get a lot of new day alerts. // In this case we do not want to print a bunch of alerts. // On a real new day notification, high and low are typically the same. // If they are not, we still are in a someone unknown state as we don't // know which happened first. State := psApproachingExtreme End; Procedure TPullbackCommon.OnNewL1Data; Begin CheckForNewExtreme End; Procedure TPullbackCommon.OnNewTosData; Begin CheckForNewExtreme; CheckForPullback End; //////////////////////////////////////////////////////////////////////// // TPullbackFromLows //////////////////////////////////////////////////////////////////////// Type TPullbackFromLows = Class(TPullbackCommon) Protected Procedure CheckForNewExtreme; Override; Procedure CheckForPullback; Override; End; Procedure TPullbackFromLows.CheckForNewExtreme; Var NewValueKnown : Boolean; NewValue : Double; Begin If L1Data.IsValid Then Begin NewValue := L1Data.GetCurrent.Low; NewValueKnown := NewValue > 0.0 End Else Begin { Avoid compiler warning. } NewValue := 0.0; NewValueKnown := False End; If TosData.IsValid And NewValueKnown Then // Merge the after hours TOS into the offical low. See more notes in // TPullbackFromHighs.CheckForNewExtreme NewValue := Min(TosData.GetLast^.Price, NewValue); If NewValueKnown Then If LastExtremeValid Then Begin If NewValue < LastExtreme Then Begin LastExtreme := NewValue; State := psPullingBack End End Else Begin LastExtreme := NewValue; LastExtremeValid := True End End; Procedure TPullbackFromLows.CheckForPullback; Var Current : PL1Data; Start : Double; DistanceToExtreme : Double; Begin If State = psPullingBack Then If TosData.IsValid And L1Data.IsValid Then Begin Current := L1Data.GetCurrent; Start := Max(Current.Open, Current.PrevClose); DistanceToExtreme := Start - LastExtreme; If DistanceToExtreme > MinDistance Then If (TosData.GetLast^.Price - LastExtreme) / DistanceToExtreme >= FMinPullback Then Begin If Current.Low = Current.High Then { Just in case the print data node was awakened before the new day data node. This just protects this one instance. We don't have to worry about all the other else clauses. The new day will be called soon, if not already. } ResetTimeFilter Else If TimeFilterOkay Then Report(FMinPullbackDisplay + ' pullback from lows.', DistanceToExtreme); State := psApproachingExtreme End End End; //////////////////////////////////////////////////////////////////////// // TPullbackFromHighs //////////////////////////////////////////////////////////////////////// Type TPullbackFromHighs = Class(TPullbackCommon) Protected Procedure CheckForNewExtreme; Override; Procedure CheckForPullback; Override; End; Procedure TPullbackFromHighs.CheckForNewExtreme; Var NewValueKnown : Boolean; NewValue : Double; Begin If L1Data.IsValid Then Begin NewValue := L1Data.GetCurrent.High; NewValueKnown := NewValue > 0.0 End Else Begin { Avoid compiler warning. } NewValue := 0.0; NewValueKnown := False End; If TosData.IsValid And NewValueKnown Then // After hours we create our own high by following the prints and watching // the official high. If there is no official high, we don't try to // proceed. We just don't have enough information. Unfortunately // this prevents us from working in the premarket. But it's required // to avoid the case of starting too soo, where we assume the first // print is a new high, then we get another new high as soon as we find // out the real high from the market. This was happening consistantly // if we start the alerrts in the middle of the day. NewValue := Max(TosData.GetLast^.Price, NewValue); If NewValueKnown Then If LastExtremeValid Then Begin If NewValue > LastExtreme Then Begin LastExtreme := NewValue; State := psPullingBack End End Else Begin LastExtreme := NewValue; LastExtremeValid := True End End; Procedure TPullbackFromHighs.CheckForPullback; Var Current : PL1Data; Start : Double; DistanceToExtreme : Double; Begin If State = psPullingBack Then If TosData.IsValid And L1Data.IsValid Then Begin Current := L1Data.GetCurrent; Start := Min(Current.Open, Current.PrevClose); DistanceToExtreme := LastExtreme - Start; If DistanceToExtreme > MinDistance Then If (LastExtreme - TosData.GetLast^.Price) / DistanceToExtreme >= FMinPullback Then Begin If Current.Low = Current.High Then { Just in case the print data node was awakened before the new day data node. This just protects this one instance. We don't have to worry about all the other else clauses. The new day will be called soon, if not already. } ResetTimeFilter Else If TimeFilterOkay Then Report(FMinPullbackDisplay + ' pullback from highs.', DistanceToExtreme); State := psApproachingExtreme End End End; //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// Initialization TGenericDataNodeFactory.StoreFactory('PullbackFromLows25', TGenericDataNodeFactory.CreateWithArgs(TPullbackFromLows, StandardSymbolPlaceHolder, 0.25, '25%')); TGenericDataNodeFactory.StoreFactory('PullbackFromLows75', TGenericDataNodeFactory.CreateWithArgs(TPullbackFromLows, StandardSymbolPlaceHolder, 0.75, '75%')); TGenericDataNodeFactory.StoreFactory('PullbackFromHighs25', TGenericDataNodeFactory.CreateWithArgs(TPullbackFromHighs, StandardSymbolPlaceHolder, 0.25, '25%')); TGenericDataNodeFactory.StoreFactory('PullbackFromHighs75', TGenericDataNodeFactory.CreateWithArgs(TPullbackFromHighs, StandardSymbolPlaceHolder, 0.75, '75%')); End.