Unit AlertsData; { This is just a collection of very simple alert definitions. Each one is a class defining the alerts, with no other support classes. Any alert requiring a class heirarchy or other support classes was moved to its own file. } Interface Implementation Uses DataNodes, GenericDataNodes, SimpleMarketData, AlertBase, NormalVolumeBreakBars, AverageHistoricalVolume, VolumeWeightedData, RecentHighsAndLows, GenericL1DataNode, GenericTosDataNode, Prices, StandardPlaceHolders, NewDayDetector, GenericFundamentalDataNode, DebugOutput, StandardCandles, LinearRegression, IntraDayEma, CCI, VWAP, CsvFileData, TalRegionals, IntradaySma, Math, SysUtils, DateUtils, StrUtils, Types; //////////////////////////////////////////////////////////////////////// // THighVolume //////////////////////////////////////////////////////////////////////// Type THighVolume = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FSymbol : String; BarData : TNormalVolumeBreakBars; VolumeData : TAverageHistoricalData; Procedure NewBarData; End; Constructor THighVolume.Create(Params : TParamList); Var Link : TDataNodeLink; Begin Assert(Length(Params) = 1); FSymbol := Params[0]; Inherited Create; TNormalVolumeBreakBars.Find(FSymbol, NewBarData, BarData, Link); AddAutoLink(Link); TAverageHistoricalData.Find(FSymbol, Nil, VolumeData, Link); AddAutoLink(Link) End; Procedure THighVolume.NewBarData; // Returns the period that contains the time. If the time // is on the boundary of two periods, it chooses the lower one. Function LowerTimeFrame(DateTime : TDateTime) : TAHVPeriods; Var Time : TDateTime; Begin Time := TimeOf(DateTime); If Time < AHVStartTime Then Result := 0 Else Result := Min(High(TAHVPeriods), Ceil((Time-AHVStartTime)/AHVPeriod)) End; // Returns the period that containst the time. If the time // is on the boundary of two periods, it chooses the higher one. Function HigherTimeFrame(DateTime : TDateTime) : TAHVPeriods; Var Time : TDateTime; Begin Time := TimeOf(DateTime); If Time < AHVStartTime Then Result := 0 Else Result := Min(High(TAHVPeriods), Succ(Floor((Time-AHVStartTime)/AHVPeriod))) End; Function PeriodsCovered(FromTime, ToTime : TDateTime) : TAHVPeriodSet; Begin If (ToTime - FromTime) > 1.0 Then // More than a day. Result := [Low(TAHVPeriods)..High(TAHVPeriods)] Else If DateOf(ToTime) = DateOf(FromTime) Then // Start time to finish time is continuous. Result := [LowerTimeFrame(FromTime)..HigherTimeFrame(ToTime)] Else // Start time to midnight is unioned with midnight to finish time. Result := [LowerTimeFrame(FromTime)..High(TAHVPeriods)] + [0..HigherTimeFrame(ToTime)] End; Function ExpectedAverageVolume(ActivePeriods : TAHVPeriodSet) : Integer; Var P : TAHVPeriods; Begin // We don't have any good data for pre or post market. // Use the adjacent data. If AHVPreMarket In ActivePeriods Then Begin Exclude(ActivePeriods, AHVPreMarket); Include(ActivePeriods, Succ(AHVPreMarket)) End; If AHVAfterMarket In ActivePeriods Then Begin Exclude(ActivePeriods, AHVAfterMarket); Include(ActivePeriods, Pred(AHVAfterMarket)) End; // Find the max of all time periods. Result := -1; For P := Low(TAHVPeriods) To High(TAHVPeriods) Do If P In ActivePeriods Then Result := Max(Result, VolumeData.GetVolume(P)) End; Const MaxQuality = 100; Var Bars : TVolumeBlocks; LastBar : TVolumeBlock; Duration : TDateTime; Coverage : TAHVPeriodSet; Expected, Threashold : Integer; Quality : Double; Msg : String; Begin Bars := BarData.GetBlocks; If Length(Bars) < 1 Then Exit; LastBar := Bars[Pred(Length(Bars))]; Duration := LastBar.EndTime - LastBar.StartTime; If Duration = 0 Then // Volume / time = infinity! Report('High instantaneous volume.', MaxQuality) Else Begin Coverage := PeriodsCovered(LastBar.StartTime, LastBar.EndTime); Expected := ExpectedAverageVolume(Coverage); If Expected >= 0 Then Begin Threashold := Round(1.5 * Expected * Duration / AHVPeriod); If BarData.BlockSize > Threashold Then Begin If BarData.BlockSize > Threashold * 2 Then Msg := 'Very high' Else Msg := 'High'; Try Quality := Min((BarData.BlockSize / Duration) / (Expected / AHVPeriod), MaxQuality) Except On Exception Do Quality := MaxQuality End; Report(Msg + ' relative volume during the last ' + DurationString(LastBar.StartTime, GetSubmitTime) + '.', Quality) End End End End; //////////////////////////////////////////////////////////////////////// // TGapDownReversal //////////////////////////////////////////////////////////////////////// Type TGapDownReversal = Class(TAlert) Private L1Data : TGenericL1DataNode; Primed : Boolean; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TGapDownReversal.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Inherited Create; Symbol := Params[0]; TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link) End; Procedure TGapDownReversal.NewData; Var Current : PL1Data; ContinuationStr : String; Begin If L1Data.IsValid Then Begin Current := L1Data.GetCurrent; If (Current^.High <= 0) Or (Current^.Low <= 0) Or (Current^.Open <= 0) Or (Current^.PrevClose <= 0) Then Primed := False Else If Current^.PrevClose <= Current.Open Then Primed := False // Gapped up or no gap at all. Else If (Current^.High > Current.PrevClose) And Primed Then Begin // Currently crossed, previously not crossed Primed := False; If Current.Open > Current.Low Then ContinuationStr := FormatPrice(Current.Open - Current.Low) Else ContinuationStr := 'No'; Report('Gap reversal, Gap=' + FormatPrice(Current.Open - Current.PrevClose, True) + ', ' + ContinuationStr +' gap continuation.', Current.PrevClose - Current.Low ) End Else If (Current^.High < Current.PrevClose) Then // We have just verified that we have been below the // close all day. Primed := True End Else // If the crossing happens while the datafeed is dead, the event // is lost. Without this, we might report large numbers of // crossings if we've been down for a while, and the pullback // would be wrong (in addition to the time). Primed := False End; //////////////////////////////////////////////////////////////////////// // TGapUpReversal //////////////////////////////////////////////////////////////////////// Type TGapUpReversal = Class(TAlert) Private L1Data : TGenericL1DataNode; Primed : Boolean; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TGapUpReversal.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link) End; Procedure TGapUpReversal.NewData; Var Current : PL1Data; ContinuationStr : String; Begin If L1Data.IsValid Then Begin Current := L1Data.GetCurrent; If (Current^.High <= 0) Or (Current^.Low <= 0) Or (Current^.Open <= 0) Or (Current^.PrevClose <= 0) Then Primed := False Else If Current^.PrevClose >= Current.Open Then Primed := False // Gapped down or no gap at all. Else If (Current^.Low < Current.PrevClose) And Primed Then Begin // Currently crossed, previously not crossed Primed := False; If Current.Open < Current.High Then ContinuationStr := FormatPrice(Current.High - Current.Open) Else ContinuationStr := 'No'; Report('Gap reversal, Gap=' + FormatPrice(Current.Open - Current.PrevClose, True) + ', ' + ContinuationStr +' gap continuation.', Current.High - Current.PrevClose ) End Else If (Current^.Low > Current.PrevClose) Then // We have just verified that we have been above the // close all day. Primed := True End Else // If the crossing happens while the datafeed is dead, the event // is lost. Without this, we might report large numbers of // crossings at once if we've been down for a while, and the // pullbacks would be wrong (in addition to the time). Primed := False End; //////////////////////////////////////////////////////////////////////// // TBlockPrint //////////////////////////////////////////////////////////////////////// Type TBlockPrint = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private TosData : TGenericTosDataNode; L1Data : TGenericL1DataNode; HistoricalVolumeData : TGenericDataNode; MinQuality : Integer; Procedure NewTosData; Procedure ReadVolumeData; Published End; Constructor TBlockPrint.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params) = 1); Symbol := Params[0]; Inherited Create; If Not SymbolIsIndex(Symbol) Then Begin TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link); TGenericL1DataNode.Find(Symbol, Nil, L1Data, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('ADVol').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(ReadVolumeData, HistoricalVolumeData, Link); AddAutoLink(Link); MinQuality := 20000; DoInCorrectThread(ReadVolumeData) End End; Procedure TBlockPrint.ReadVolumeData; Begin MinQuality := 20000; If HistoricalVolumeData.IsValid Then If HistoricalVolumeData.GetInteger <= 1000000 Then MinQuality := 5000 End; Procedure TBlockPrint.NewTosData; Var Size : Double; Bid, Ask : Double; Msg : String; Exchange : String; Last : PTosData; Begin If TosData.IsValid Then Begin Last := TosData.GetLast; If Last^.EventType = etNewPrint Then Begin Size := Last^.Size; If Size >= MinQuality Then Begin Msg := 'Block trade.'; If L1Data.IsValid Then Begin Bid := L1Data.GetCurrent^.BidPrice; Ask := L1Data.GetCurrent^.AskPrice; If (Bid > 0) And (Ask > 0) And (Last^.Price > 0) Then Begin If (Last^.Price < Bid) And (Last^.Price < Ask) Then Msg := Msg + ' Trading below.' Else If (Last^.Price > Bid) And (Last^.Price > Ask) Then Msg := Msg + ' Trading above.' Else If Ask > Bid Then Begin If Last^.Price = Bid Then Msg := Msg + ' At the bid.' Else If Last^.Price < Ask Then Msg := Msg + ' Trading between.' Else If Last^.Price = Ask Then Msg := Msg + ' At the ask.' End End End; Exchange := LongExchangeName(Last^.Exchange); If (Exchange <> '') And (Exchange[1] <> '[') And (Exchange[Length(Exchange)] <> ']') Then Msg := Msg + ' (' + Exchange + ')'; Report(Msg, Size) End End End End; //////////////////////////////////////////////////////////////////////// // TGapRetracementArcUp //////////////////////////////////////////////////////////////////////// Type TGapRetracementArcUp = Class(TAlert) Private L1Data : TGenericL1DataNode; TickVolData : TGenericDataNode; Primed : Boolean; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TGapRetracementArcUp.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Inherited Create; Symbol := Params[0]; TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('TickVolatility').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(NewData, TickVolData, Link); AddAutoLink(Link) End; Procedure TGapRetracementArcUp.NewData; Var Current : PL1Data; TickVol : Double; PercentClosure : Double; Begin If L1Data.IsValid And TickVolData.IsValid Then Begin Current := L1Data.GetCurrent; TickVol := TickVolData.GetDouble(); If (Current^.High <= 0) Or (Current^.Low <= 0) Or (Current^.Open <= 0) Or (Current^.PrevClose <= 0) Then Primed := False Else If (Current^.PrevClose + TickVol) >= Current.Open Then Primed := False // Gapped down, no gap at all, or did not gap up high enough. Else If (Current^.High > (Current^.Open + TickVol)) And (Current^.Low > Current^.PrevClose) And (Current^.Low < Current^.Open) And Primed Then Begin // Currently crossed, previously not crossed Primed := False; PercentClosure := (Current^.Open - Current^.Low) / (Current^.Open - Current^.PrevClose) * 100; Report(Format('Crossed above open after %f%% gap closure.', [PercentClosure]), PercentClosure) End Else If (Current^.High <= (Current^.Open + TickVol)) Then Primed := True Else Primed := False End Else // If the crossing happens while the datafeed is dead, the event // is lost. Without this, we might report large numbers of // crossings if we've been down for a while, and the pullback // would be wrong (in addition to the time). Primed := False End; //////////////////////////////////////////////////////////////////////// // TGapRetracementArcDown //////////////////////////////////////////////////////////////////////// Type TGapRetracementArcDown = Class(TAlert) Private L1Data : TGenericL1DataNode; TickVolData : TGenericDataNode; Primed : Boolean; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TGapRetracementArcDown.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('TickVolatility').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(NewData, TickVolData, Link); AddAutoLink(Link) End; Procedure TGapRetracementArcDown.NewData; Var Current : PL1Data; TickVol : Double; PercentClosure : Double; Begin If L1Data.IsValid Then Begin Current := L1Data.GetCurrent; TickVol := TickVolData.GetDouble(); If (Current^.High <= 0) Or (Current^.Low <= 0) Or (Current^.Open <= 0) Or (Current^.PrevClose <= 0) Then Primed := False Else If Current^.PrevClose <= (Current.Open + TickVol) Then Primed := False // Gapped up, no gap at all, or did not gap down enough. Else If (Current^.Low < (Current^.Open - TickVol)) And (Current^.High < Current^.PrevClose) And (Current^.High > Current^.Open) And Primed Then Begin // Currently crossed, previously not crossed Primed := False; PercentClosure := (Current^.High - Current^.Open) / (Current^.PrevClose - Current^.Open) * 100; Report(Format('Crossed below open after %f%% gap closure.', [PercentClosure]), PercentClosure) End Else If (Current^.Low >= (Current^.Open - TickVol)) Then Primed := True Else Primed := False End Else // If the crossing happens while the datafeed is dead, the event // is lost. Without this, we might report large numbers of // crossings at once if we've been down for a while, and the // pullbacks would be wrong (in addition to the time). Primed := False End; //////////////////////////////////////////////////////////////////////// // TIntraDayHighLow //////////////////////////////////////////////////////////////////////// Type IntraDayHighLow = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private Msg : String; TosData : TGenericTosDataNode; Highs : Boolean; Frequency : Double; LastPrintTime : TDateTime; BestPriceThisPeriod : Double; BestPriceLastPeriod : Double; ReportedThisPeriod : Boolean; ThisPeriodValid : Boolean; LastPeriodValid : Boolean; Procedure NewData; Function MoreReportable(A, B : Double) : Boolean; Published End; Function IntraDayHighLow.MoreReportable(A, B : Double) : Boolean; Begin If Highs Then Result := A > B Else Result := A < B End; Constructor IntraDayHighLow.Create(Params : TParamList); Const Direction : Array[False..True] Of String = ('low', 'high'); Var Symbol : String; Link : TDataNodeLink; Minutes : Integer; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, Highs, Minutes)'); Symbol := Params[0]; Highs := Params[1]; Minutes := Params[2]; Frequency := 24.0 * 60.0 / Minutes; Msg := Format('New %d minute %s.', [Minutes, Direction[Highs]]); Inherited Create; TGenericTosDataNode.Find(Symbol, NewData, TosData, Link); AddAutoLink(Link) End; Procedure IntraDayHighLow.NewData; Var Last : PTosData; Begin If Not TosData.IsValid Then ThisPeriodValid := False Else Begin Last := TosData.GetLast; If Last^.EventType = etNewPrint Then Begin If (LastPrintTime <> 0) And (Floor(TimeOf(LastPrintTime) * Frequency) <> Floor(TimeOf(Last^.Time) * Frequency)) Then Begin BestPriceLastPeriod := BestPriceThisPeriod; LastPeriodValid := ThisPeriodValid; BestPriceThisPeriod := Last^.Price; ThisPeriodValid := True; ReportedThisPeriod := False End Else If ThisPeriodValid And MoreReportable(Last^.Price, BestPriceThisPeriod) Then BestPriceThisPeriod := Last^.Price; If LastPeriodValid And (Not ReportedThisPeriod) And MoreReportable(Last^.Price, BestPriceLastPeriod) Then Begin Report(Msg); ReportedThisPeriod := True End; LastPrintTime := Last^.Time End End End; //////////////////////////////////////////////////////////////////////// // TLargeBidAsk // // This is only recommended for stocks trading under 1,000,000 shares // per day. At one time this data node would only work for those // stocks. Now we expect a higher level of code to take care of that. //////////////////////////////////////////////////////////////////////// Type TLargeBidOrAsk = Class(TAlert) Private FBidSide : Boolean; L1Data : TGenericL1DataNode; CurrentLevel : (clTriggered, clProbablyTriggered, clUnknown, clProbablyNotTriggered, clNotTriggered); LastTriggeredTime : TDateTime; LowestTriggeredPrice : Double; HighestTriggeredPrice : Double; HighestTriggeredSize : Integer; AlertsThisTrigger : Integer; ExpectedValue : Integer; Procedure NewL1Data; Procedure GetValues(Out Price : Double; Out Size : Integer); Function BaseDescription : String; Class Function GetExpectedBidAskSize(Symbol : String) : Integer; Protected Constructor Create(Params : TParamList); Override; Published End; Class Function TLargeBidOrAsk.GetExpectedBidAskSize(Symbol : String) : Integer; Var AverageDailyVolume : Integer; Begin If SymbolIsFuture(Symbol) Or SymbolIsIndex(Symbol) Then Result := MaxInt Else Begin AverageDailyVolume := GetAverageDailyVolume(Symbol); If AverageDailyVolume <= 0 Then // Volume not specified, so we have no way to guess Result := MaxInt Else If AverageDailyVolume <= 1000000 Then Result := 6000 Else if AverageDailyVolume <= 3000000 Then Result := 10000 //Else if AverageDailyVolume <= 10000000 Then // Result := 25000 -- this value is way too low! Else Result := MaxInt End End; Procedure TLargeBidOrAsk.GetValues(Out Price : Double; Out Size : Integer); Var Current : PL1Data; Begin Current := L1Data.GetCurrent; If FBidSide Then Begin Price := Current^.BidPrice; Size := Current^.BidSize End Else Begin Price := Current^.AskPrice; Size := Current^.AskSize End End; Function TLargeBidOrAsk.BaseDescription : String; Begin If FBidSide Then Result := 'Large Bid Size' Else Result := 'Large Ask Size'; End; Constructor TLargeBidOrAsk.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=2, 'Expected args: Symbol, BidSide)'); Symbol := Params[0]; FBidSide := Params[1]; Inherited Create; ExpectedValue := GetExpectedBidAskSize(Symbol); CurrentLevel := clUnknown; If ExpectedValue < MaxInt Then Begin TGenericL1DataNode.Find(Symbol, NewL1Data, L1Data, Link); AddAutoLink(Link) End End; Procedure TLargeBidOrAsk.NewL1Data; Const MinSize = 6000; //Previously 10,000 CutOffTime = 1.0 / 24.0 / 60.0 * 3.0; { Three minutes. } Var Price : Double; Size : Integer; NextLevel : (nlTriggered, nlUnknown, nlNotTriggered); AlertMessage : String; Begin NextLevel := nlUnknown; If L1Data.IsValid Then Begin GetValues(Price, Size); If (Price > 0) And (Size > 0) Then Begin If Size < MinSize Then NextLevel := nlNotTriggered Else Begin NextLevel := nlTriggered; If (CurrentLevel In [clTriggered, clProbablyTriggered]) Or (GetSubmitTime - LastTriggeredTime < CutOffTime) Then Begin { We were already triggered. Let's see if the trigger condition has changed enough to report another alert. } If (AlertsThisTrigger < 5) And ((Price < LowestTriggeredPrice) Or (Price > HighestTriggeredPrice) Or (Size > HighestTriggeredSize)) Then Begin Inc(AlertsThisTrigger); AlertMessage := BaseDescription; If (Size > HighestTriggeredSize) Then Begin HighestTriggeredSize := Size; AlertMessage := AlertMessage + ' (Size increasing)' End; If (Price < LowestTriggeredPrice) Then Begin LowestTriggeredPrice := Price; AlertMessage := AlertMessage + ' (Price dropping)' End Else If (Price > HighestTriggeredPrice) Then Begin HighestTriggeredPrice := Price; AlertMessage := AlertMessage + ' (Price rising)' End; Report(AlertMessage, Size) End End Else If CurrentLevel <> clUnknown Then Begin { This is a brand new trigger. } Report(BaseDescription, Size); LowestTriggeredPrice := Price; HighestTriggeredPrice := Price; HighestTriggeredSize := Size; AlertsThisTrigger := 1 End End End End; Case NextLevel Of nlTriggered : CurrentLevel := clTriggered; nlUnknown : If CurrentLevel = clTriggered Then CurrentLevel := clProbablyTriggered Else If CurrentLevel = clNotTriggered Then CurrentLevel := clProbablyNotTriggered; nlNotTriggered : Begin If CurrentLevel In [clTriggered, clProbablyTriggered] Then LastTriggeredTime := GetSubmitTime; CurrentLevel := clNotTriggered End End End; //////////////////////////////////////////////////////////////////////// // TPercentChangeForTheDay //////////////////////////////////////////////////////////////////////// Type TPercentChangeForTheDay = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FLastEventDate : TDateTime; FLastExtremeValue : Double; FLastPercentReported : Integer; FPreviousHighLowValid : Boolean; Data : TGenericL1DataNode; Procedure NewData; End; Constructor TPercentChangeForTheDay.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=2, 'Expected params: (Symbol, Up)'); Symbol := Params[0]; FUp := Params[1]; Inherited Create; TGenericL1DataNode.Find(Symbol, NewData, Data, Link); AddAutoLink(Link) End; Procedure TPercentChangeForTheDay.NewData; Const MinPercentChange = 3; // Originally this was 3% Var CurrentDate : TDateTime; CurrentData : PL1Data; Price : Double; PriceChange : TValueSign; NewPercent : Integer; NowValid : Boolean; Begin NowValid := False; CurrentDate := DateOf(GetSubmitTime); If CurrentDate <> FLastEventDate Then Begin FLastEventDate := CurrentDate; If FUp Then FLastExtremeValue := 0 Else FLastExtremeValue := MaxDouble; FLastPercentReported := 0 End; If Data.IsValid Then Begin CurrentData := Data.GetCurrent; If FUp Then Price := CurrentData^.High Else Price := CurrentData^.Low; If (Price > 0) And (CurrentData^.PrevClose > 0) Then Begin NowValid := True; If FUp Then PriceChange := Sign(Price - FLastExtremeValue) Else PriceChange := Sign(FLastExtremeValue - Price); If PriceChange <> 0 Then Begin FLastExtremeValue := Price; If FUp Then NewPercent := Max(0, Trunc((Price - CurrentData^.PrevClose) / CurrentData^.PrevClose * 100.0)) Else NewPercent := Max(0, Trunc((CurrentData^.PrevClose - Price) / CurrentData^.PrevClose * 100.0)); If PriceChange = 1 Then Begin { A higher high or lower low. } If (NewPercent > FLastPercentReported) And (NewPercent >= MinPercentChange) Then Begin If FPreviousHighLowValid Then Report(Format('%s %d%% for the day.', [IfThen(FUp, 'Up', 'Down'), NewPercent]), NewPercent); FLastPercentReported := NewPercent End End Else {If PriceChange = -1 Then} { A correction. The high dropped, or the low rose. } FLastPercentReported := NewPercent End End End; FPreviousHighLowValid := NowValid End; //////////////////////////////////////////////////////////////////////// // TStrongVolume //////////////////////////////////////////////////////////////////////// Type TStrongVolume = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FLastVolumeGood : Boolean; FHistoricalVolumeGood : Boolean; FHistoricalVolume : Integer; FLastMultipleReported : Integer; CurrentVolumeData : TGenericTosDataNode; HistoricalVolumeData : TGenericDataNode; Procedure NewCurrentVolumeData; Procedure NewHistoricalVolumeVolumeData; Procedure Initialize; End; Constructor TStrongVolume.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params) = 1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; TGenericTosDataNode.Find(Symbol, NewCurrentVolumeData, CurrentVolumeData, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('ADVol').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(NewHistoricalVolumeVolumeData, HistoricalVolumeData, Link); AddAutoLink(Link); DoInCorrectThread(Initialize); End; Procedure TStrongVolume.Initialize; Begin If Not FHistoricalVolumeGood Then NewHistoricalVolumeVolumeData End; Procedure TStrongVolume.NewHistoricalVolumeVolumeData; Begin FHistoricalVolume := 0; If HistoricalVolumeData.IsValid Then FHistoricalVolume := HistoricalVolumeData.GetInteger; FHistoricalVolumeGood := FHistoricalVolume > 0; FLastVolumeGood := False End; Procedure TStrongVolume.NewCurrentVolumeData; Const MinimumMultiple = 1; // Used to be 3. Var CurrentVolume : Integer; CurrentMultiple : Integer; Begin If FHistoricalVolumeGood Then Begin If CurrentVolumeData.IsValid Then CurrentVolume := CurrentVolumeData.GetLast^.Volume Else CurrentVolume := 0; If CurrentVolume <= 0 Then FLastVolumeGood := False Else Begin CurrentMultiple := CurrentVolume Div FHistoricalVolume; If (CurrentMultiple > FLastMultipleReported) And (CurrentMultiple >= MinimumMultiple) And FLastVolumeGood Then Report(Format('Currently at %d x average daily volume.', [CurrentMultiple]), CurrentMultiple); FLastMultipleReported := CurrentMultiple; FLastVolumeGood := True End End End; //////////////////////////////////////////////////////////////////////// // TOpeningRangeBreak //////////////////////////////////////////////////////////////////////// Type TOpeningRangeBreak = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FInitialMinutes : Integer; FInitialReportTime : TDateTime; FState : (orbsNoData, // Waiting for first print of comparison candle. orbsReady, // Waiting to expand the comparison candle, or report an alert. orbsReported); // Reported at least once today. Waiting for a data correction or a new day. FLastReadTime : TDateTime; FExtremeValue : Double; TosData : TGenericTosDataNode; FundamentalData : TGenericFundamentalDataNode; ListedOn : (loUnknown, loNASDAQ, loNYSE, loIndex, loOther); Function MoreReportable(A, B : Double) : Boolean; Function MostReportable(A, B : Double) : Double; Procedure NewTosData; Procedure NewFundamentalData; Procedure NewPrice(CurrentValue : Double; EventTime : TDateTime; ValidForFirst : Boolean); Procedure BeginningOfDay(Current : TDateTime); End; Constructor TOpeningRangeBreak.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, Up, Minutes)'); Symbol := Params[0]; FUp := Params[1]; FInitialMinutes := Params[2]; Inherited Create; If Not SymbolIsFuture(Symbol) Then Begin TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link); If SymbolIsIndex(Symbol) Then { Many of these show up as AMEX for some reason, if we believe the data feed. } ListedOn := loIndex Else Begin TGenericFundamentalDataNode.Find(Symbol, NewFundamentalData, FundamentalData, Link); AddAutoLink(Link) End; BeginningOfDay(Now) End End; Function TOpeningRangeBreak.MostReportable(A, B : Double) : Double; Begin If FUp Then Result := Max(A, B) Else Result := Min(A, B) End; Function TOpeningRangeBreak.MoreReportable(A, B : Double) : Boolean; Begin If FUp Then Result := A > B Else Result := A < B End; Procedure TOpeningRangeBreak.NewFundamentalData; Var ExchangeString : String; Begin If FundamentalData.IsValid Then Begin ExchangeString := FundamentalData.GetCurrent^.ListedExchange; If ExchangeString = '' Then ListedOn := loUnknown Else If ExchangeString = 'NASD' Then ListedOn := loNASDAQ Else If ExchangeString = 'NYSE' Then ListedOn := loNYSE Else If ExchangeString = '$NDX' Then ListedOn := loIndex Else ListedOn := loOther End Else ListedOn := loUnknown End; Procedure TOpeningRangeBreak.NewTosData; Const OpenTime = 1.0 / 24.0 * 6.5; Var Last : PTosData; EventTime : TDateTime; ValidForFirst : Boolean; Begin If TosData.IsValid Then Begin Last := TosData.GetLast; EventTime := Last^.Time; If (EventTime > Today) { This should probably be handled somewhere else. When we see this condition, a print before today, its always bad. at 5:05am when we double check the status of the prints, we seem to be reporting the last print of yesterday as a new print, not as an update to the value like we should be. When we get a print with no time, I don't know what it is, but it's bad. This logic will fail with securities that trade 24 hours and happen to have a print at midnight. } And (Not Last.FormT) And (Last.EventType = etNewPrint) Then Begin Case ListedOn Of loNYSE : ValidForFirst := Last^.Exchange = 'NYSE'; loIndex : ValidForFirst := TimeOf(EventTime) > OpenTime; Else Begin //If TimeOf(EventTime) < OpenTime Then // DebugOutputWindow.AddMessage('TimeOf(EventTime) < OpenTime, ' + DateTimeToStr(EventTime) + ', ' + DebugString); ValidForFirst := True End End; NewPrice(Last^.Price, EventTime, ValidForFirst) End End End; Procedure TOpeningRangeBreak.NewPrice(CurrentValue : Double; EventTime : TDateTime; ValidForFirst : Boolean); Const BlackOutPeriod = 1.0 / 24.0 / 60.0; // Must wait at least one minute after // support is established before // reporting anything. Begin If DateOf(EventTime) > FLastReadTime Then BeginningOfDay(EventTime); Case FState Of orbsNoData : { On the NYSE we ignore everything before the first print from the specialist. For the other exchanges, the clock starts with the first print of the day. } If ValidForFirst Then Begin FInitialReportTime := EventTime + FInitialMinutes / 24.0 / 60.0; FExtremeValue := CurrentValue; //Report('orbsReady, FInitialReportTime = ' + DateTimeToStr(FInitialReportTime)); FState := orbsReady End; //Else Report('Deferring, not ValidForFirst'); orbsReady : Begin If EventTime <= FInitialReportTime Then FExtremeValue := MostReportable(CurrentValue, FExtremeValue) Else If MoreReportable(CurrentValue, FExtremeValue) Then Begin FState := orbsReported; If EventTime > FInitialReportTime + BlackoutPeriod Then Report(Format('Opening range break%s. (%d minutes)', [IfThen(FUp, 'out', 'down'), FInitialMinutes])) End End; orbsReported : Begin { Only report once per day. } End End End; Procedure TOpeningRangeBreak.BeginningOfDay(Current : TDateTime); Begin FLastReadTime := Current; //FInitialReportTime := 0; FState := orbsNoData; End; //////////////////////////////////////////////////////////////////////// // TOpeningRangeBreak // This version started watching all stocks at exactly the opening bell. // This was a problem, especially for the one minute alerts, because the // NYSE specialist often didn't do anything for about 90 seconds after // the open. //////////////////////////////////////////////////////////////////////// (* Type TOpeningRangeBreak = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FInitialMinutes : Integer; FInitialReportTime : TDateTime; FState : (orbsNoData, // Waiting for first print of comparison candle. orbsReady, // Waiting to expand the comparison candle, or report an alert. orbsReported); // Reported at least once today. Waiting for a data correction or a new day. FLastReadTime : TDateTime; FExtremeValue : Double; TosData : TGenericTosDataNode; Function MoreReportable(A, B : Double) : Boolean; Function MostReportable(A, B : Double) : Double; Procedure NewNewDayData; Procedure NewTosData; Procedure NewPrice(CurrentValue : Double; EventTime : TDateTime); Procedure BeginningOfDay(Current : TDateTime); End; Constructor TOpeningRangeBreak.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, Up, Minutes)'); Symbol := Params[0]; FUp := Params[1]; FInitialMinutes := Params[2]; Inherited Create; If Not SymbolIsFuture(Symbol) Then Begin TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link); TNewDay.Find(Symbol, NewNewDayData, Link); AddAutoLink(Link); BeginningOfDay(Now); End End; Function TOpeningRangeBreak.MostReportable(A, B : Double) : Double; Begin If FUp Then Result := Max(A, B) Else Result := Min(A, B) End; Function TOpeningRangeBreak.MoreReportable(A, B : Double) : Boolean; Begin If FUp Then Result := A > B Else Result := A < B End; Procedure TOpeningRangeBreak.NewNewDayData; Begin BeginningOfDay(Now) End; Procedure TOpeningRangeBreak.NewTosData; Var Last : PTosData; Begin If TosData.IsValid Then Begin Last := TosData.GetLast; If (Not Last.FormT) And (Last.EventType = etNewPrint) Then NewPrice(Last^.Price, Last^.Time) End End; Procedure TOpeningRangeBreak.NewPrice(CurrentValue : Double; EventTime : TDateTime); Const BlackOutPeriod = 1.0 / 24.0 / 60.0; // Must wait at least one minute after // support is established before // reporting anything. Begin If DateOf(EventTime) > FLastReadTime Then BeginningOfDay(EventTime); Case FState Of orbsNoData : { If time passes before we get any data, we stay in this state until tomorrow. That cuts off the alert for today. This is not perfect because we typically will see yesterday's high and low, rather than nothing. We don't distinguish between those two cases. We probably should. } If (EventTime <= FInitialReportTime) Then Begin FExtremeValue := CurrentValue; FState := orbsReady End; orbsReady : Begin If EventTime <= FInitialReportTime Then FExtremeValue := MostReportable(CurrentValue, FExtremeValue) Else If MoreReportable(CurrentValue, FExtremeValue) Then Begin FState := orbsReported; If EventTime > FInitialReportTime + BlackoutPeriod Then Report(Format('Opening range break%s. (%d minutes)', [IfThen(FUp, 'out', 'down'), FInitialMinutes])) End End; orbsReported : Begin { It might be nice to do a correction, but only if the price came from a new high/low. But it might also cause some problems, even with no data loss, if the high/low data and the prints come at different times. } End End End; Procedure TOpeningRangeBreak.BeginningOfDay(Current : TDateTime); Const OpenTime = 1.0 / 24.0 * 6.5; { 6:30 am } Begin FLastReadTime := Current; FInitialReportTime := DateOf(FLastReadTime) + OpenTime + FInitialMinutes / 24.0 / 60.0; FState := orbsNoData; End; *) //////////////////////////////////////////////////////////////////////// // TOpeningRangeBreak // This version uses the high and the low, rather than the prints. // This was better for recovering from bad data, but it made a mess when // we were losing a lot of data! //////////////////////////////////////////////////////////////////////// (* Type TOpeningRangeBreak = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FInitialMinutes : Integer; FInitialReportTime : TDateTime; FState : (orbsNoData, // Waiting for first print of comparison candle. orbsReady, // Waiting to expand the comparison candle, or report an alert. orbsReported); // Reported at least once today. Waiting for a data correction or a new day. FLastReadTime : TDateTime; FExtremeValue : Double; RecentHighLowData : TRecentHighLow; { Use this just because it doesn't report as often. } Procedure BeginningOfDay(Current : TDateTime); Procedure NewHighLowData; Function MoreReportable(A, B : Double) : Boolean; End; Procedure TOpeningRangeBreak.BeginningOfDay(Current : TDateTime); Const OpenTime = 1.0 / 24.0 * 6.5; { 6:30 am } Begin FLastReadTime := Current; FInitialReportTime := DateOf(FLastReadTime) + OpenTime + FInitialMinutes / 24.0 / 60.0; FState := orbsNoData; End; Constructor TOpeningRangeBreak.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, Up, Minutes)'); Symbol := Params[0]; FUp := Params[1]; FInitialMinutes := Params[2]; Inherited Create; If Not SymbolIsFuture(Symbol) Then Begin TRecentHighLow.Find(Symbol, FUp, NewHighLowData, RecentHighLowData, Link); AddAutoLink(Link); BeginningOfDay(Now); DoInCorrectThread(NewHighLowData) End End; Procedure TOpeningRangeBreak.NewHighLowData; Var EventTime : TDateTime; CurrentValue : Double; Begin EventTime := GetSubmitTime; If DateOf(EventTime) > FLastReadTime Then BeginningOfDay(EventTime); Case FState Of orbsNoData : { If time passes before we get any data, we stay in this state until tomorrow. That cuts off the alert for today. This is not perfect because we typically will see yesterday's high and low, rather than nothing. We don't distinguish between those two cases. We probably should. } If (EventTime <= FInitialReportTime) And (RecentHighLowData.IsValid) Then Begin FExtremeValue := CurrentValue; FState := orbsReady End; orbsReady : If RecentHighLowData.IsValid Then Begin CurrentValue := RecentHighLowData.CurrentValue; If EventTime <= FInitialReportTime Then FExtremeValue := CurrentValue Else If MoreReportable(CurrentValue, FExtremeValue) Then Begin FState := orbsReported; If RecentHighLowData.PreviouslyValid Then Report(Format('Opening range break%s. (%d minutes)', [IfThen(FUp, 'out', 'down'), FInitialMinutes])) End End; orbsReported : If RecentHighLowData.IsValid Then Begin CurrentValue := RecentHighLowData.CurrentValue; If Not MoreReportable(CurrentValue, FExtremeValue) Then Begin { Data Correction. The high or low moved backwards. } FExtremeValue := CurrentValue; FState := orbsReady End End End End; Function TOpeningRangeBreak.MoreReportable(A, B : Double) : Boolean; Begin If FUp Then Result := A > B Else Result := A < B End; *) //////////////////////////////////////////////////////////////////////// // TBrightBands // This was based on TPercentChangeForTheDay. Instead of being up n%, // we want to be up n standard deviations. //////////////////////////////////////////////////////////////////////// Type TBrightBands = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FLastEventDate : TDateTime; FLastExtremeValue : Double; FLastCountReported : Integer; FPreviousHighLowValid : Boolean; FOneStandardDeviation : Double; L1Data : TGenericL1DataNode; BackgroundData : TGenericDataNode; Procedure ReadL1Data; Procedure ReadBackgroundData; Procedure Reset; End; Constructor TBrightBands.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=2, 'Expected params: (Symbol, Up)'); Symbol := Params[0]; FUp := Params[1]; Inherited Create; TGenericL1DataNode.Find(Symbol, ReadL1Data, L1Data, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('BrightVolatility').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(ReadBackgroundData, BackgroundData, Link); AddAutoLink(Link); DoInCorrectThread(ReadBackgroundData) End; Procedure TBrightBands.ReadBackgroundData; Var NewValue : Double; Begin If BackgroundData.IsValid Then NewValue := BackgroundData.GetDouble Else NewValue := 0; If NewValue <> FOneStandardDeviation Then Begin FOneStandardDeviation := NewValue; Reset; ReadL1Data End End; Procedure TBrightBands.Reset; Begin If FUp Then FLastExtremeValue := 0 Else FLastExtremeValue := MaxDouble; FLastCountReported := 0; FPreviousHighLowValid := False End; Procedure TBrightBands.ReadL1Data; Const MinChange = 1; Var CurrentDate : TDateTime; CurrentData : PL1Data; Price : Double; PriceChange : TValueSign; NewCount : Integer; NowValid : Boolean; Begin NowValid := False; CurrentDate := DateOf(GetSubmitTime); If CurrentDate <> FLastEventDate Then Begin FLastEventDate := CurrentDate; Reset End; If (FOneStandardDeviation > 0) And L1Data.IsValid Then Try CurrentData := L1Data.GetCurrent; If FUp Then Price := CurrentData^.High Else Price := CurrentData^.Low; If (Price > 0) And (CurrentData^.PrevClose > 0) Then Begin NowValid := True; If FUp Then PriceChange := Sign(Price - FLastExtremeValue) Else PriceChange := Sign(FLastExtremeValue - Price); If PriceChange <> 0 Then Begin FLastExtremeValue := Price; If FUp Then NewCount := Max(0, Trunc((Price - CurrentData^.PrevClose) / FOneStandardDeviation / CurrentData^.PrevClose)) Else NewCount := Max(0, Trunc((CurrentData^.PrevClose - Price) / FOneStandardDeviation / CurrentData^.PrevClose)); If PriceChange = 1 Then Begin { A higher high or lower low. } If (NewCount > FLastCountReported) And (NewCount >= MinChange) Then Begin If FPreviousHighLowValid Then Report(Format('%s %d standard deviation%s for the day.', [IfThen(FUp, 'Up', 'Down'), NewCount, IfThen(NewCount > 1, 's')]), NewCount); FLastCountReported := NewCount End End Else {If PriceChange = -1 Then} { A correction. The high dropped, or the low rose. } FLastCountReported := NewCount End End Except { Divided by 0 or overflow. } End; FPreviousHighLowValid := NowValid End; //////////////////////////////////////////////////////////////////////// // TPtsEntrySignals // Precision Trading System / Mel // Draw a channel +/- 1 (23 period) standard deviation around a (23 // period) linear regression line. // 3 period EMA, 3 period MLR, and price are all going the same way. All // are moving toward the mean of the 23 period channel, and all are within // the one standard deviation channel. 3 period MLR is further in that // direction than the 3 period EMA. Quality is the distance between // the price and one standard deviation on the far side of the mean. //////////////////////////////////////////////////////////////////////// Type TPtsEntrySignals = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FMinutesPerBar : Byte; FLastReportTime : Double; { The print time of the last time we reported this alert. 0 if not since this object was created. } FPreviouslyTriggered : Boolean; FPrevoiusValuesValid : Boolean; FPreviousEma, FPreviousPrice, FPreviousMean3 : Double; EmaData : TGenericDataNode; PriceData : TStandardCandles; LinearRegressionData, MLR3Data : TLinearRegressionBase; Procedure SaveCurrentValues; Procedure OnNewBarData; Function CorrectDirection(Direction : Double) : Boolean; Function DirectionScaler : Integer; End; Constructor TPtsEntrySignals.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=3, 'Expected: (Symbol, Minutes Per Bar, Up)'); Symbol := Params[0]; FMinutesPerBar := Params[1]; FUp := Params[2]; Inherited Create; TStandardCandles.Find(Symbol, FMinutesPerBar, OnNewBarData, PriceData, Link); AddAutoLink(Link); TLinearRegressionBase.Find(Symbol, FMinutesPerBar, 23, Nil, LinearRegressionData, Link); AddAutoLink(Link); TLinearRegressionBase.Find(Symbol, FMinutesPerBar, 3, Nil, MLR3Data, Link); AddAutoLink(Link); Factory := CreateIntradayEmaFactory(FMinutesPerBar, 3); Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(Nil, EmaData, Link); AddAutoLink(Link); DoInCorrectThread(SaveCurrentValues) End; { We use the same procedure for initialization, and after we have received data. Either way, we store the current values for comparison when we get new values. This would only be a problem if someone created a new alert inside of a callback from OnNewBarData. Dealing with that case seems unnessary, and would add some complication to the code. } Procedure TPtsEntrySignals.SaveCurrentValues; Var UnusedM, UnusedB : Double; Begin If EmaData.IsValid And MLR3Data.GetValid { And (PriceData.HistoricalCandleCount >= 1) } Then Begin FPreviousEma := EmaData.GetDouble; FPreviousPrice := PriceData.GetHistory[Pred(PriceData.HistoricalCandleCount)].Close; MLR3Data.GetMAndBAndMLR(UnusedM, UnusedB, FPreviousMean3); FPrevoiusValuesValid := True End End; Procedure TPtsEntrySignals.OnNewBarData; Var TriggeredThisTime : Boolean; CurrentPrice, StdDev, M23, B23, Mean23, ToBound, EMA3, M3, B3, Mean3 : Double; ToReport : String; Begin TriggeredThisTime := False; If PriceData.LastTransition In [ltNew, ltReset] Then FPrevoiusValuesValid := False; { Check to see that we have valid data to look at } If FPrevoiusValuesValid And EmaData.IsValid And LinearRegressionData.GetValid And MLR3Data.GetValid {And // This is implied by the previous items. (PriceData.HistoricalCandleCount >= 1)} Then Begin EMA3 := EmaData.GetDouble; { and the EMA is going in the right direction } If CorrectDirection(EMA3 - FPreviousEma) Then Begin CurrentPrice := PriceData.GetHistory[Pred(PriceData.HistoricalCandleCount)].Close; { and the price is going in the right direction } If CorrectDirection(CurrentPrice - FPreviousPrice) Then Begin MLR3Data.GetMAndBAndMLR(M3, B3, Mean3); { and the MLR3 is going in the right direction } If CorrectDirection(Mean3 - FPreviousMean3) { and the MLR3 has already crossed the EMA3 } And CorrectDirection(Mean3 - EMA3) Then Begin LinearRegressionData.GetMAndBAndMLR(M23, B23, Mean23); { and the price has not crossed the mean yet } If CorrectDirection(Mean23 - CurrentPrice) { and the MLR3 has not crossed the mean yet } { and therefore the EMA3 has not crossed the mean yet } And CorrectDirection(Mean23 - Mean3) Then Begin { Now we have all the positive conditons. But this alert is edge triggered. So make sure this wasn't true last time. } If Not FPreviouslyTriggered Then Begin StdDev := LinearRegressionData.GetStdDev; ToBound := Mean23 + StdDev * DirectionScaler; //Report('PTS ' + IfThen(FUp, 'long', 'short') + ' entry signal', (ToBound - CurrentPrice) * DirectionScaler) //Report(Format('PTS %s entry signal, %d minute chart.channel=(%.2f, %.2f, %.2f) $%.4f/bar', [IfThen(FUp, 'long', 'short'), Mean-StdDev, Mean, Mean+StdDev, M]), (ToBound - CurrentPrice) * DirectionScaler) ToReport := 'PTS ' + IfThen(FUp, 'long', 'short') + ' entry signal, ' + IntToStr(FMinutesPerBar) + ' minute chart.'; If FLastReportTime > 0 Then ToReport := ToReport + ' Previously reported ' + DurationString(FLastReportTime, PriceData.GetLastCandleStartTime) + ' ago.'; Report(ToReport, (ToBound - CurrentPrice) * DirectionScaler); FLastReportTime := PriceData.GetLastCandleStartTime End; { Make sure we don't report an alert next time. } TriggeredThisTime := True End End End End End; FPreviouslyTriggered := TriggeredThisTime; { Start from scratch when we save the current values. This will typically repeat some of the work from above. But there are so many cases above that it is easier to start from scratch. } SaveCurrentValues End; Function TPtsEntrySignals.CorrectDirection(Direction : Double) : Boolean; Begin If FUp Then Result := Direction > 0.0 Else Result := Direction < 0.0 End; Function TPtsEntrySignals.DirectionScaler : Integer; Begin If FUp Then Result := 1 Else Result := -1 End; //////////////////////////////////////////////////////////////////////// // TWoodieCciBuySell //////////////////////////////////////////////////////////////////////// Type TWoodieCciState = (wcsUnknown, wcsTriggered, wcsReady); TWoodieCciBuySell = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private FUp : Boolean; FMinutesPerBar : Byte; FPreviousState : TWoodieCciState; CciData : TGenericDataNode; Procedure OnNewCciData; Procedure Initialize; Function CurrentState : TWoodieCciState; End; Constructor TWoodieCciBuySell.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=3, 'Expected: (Symbol, Minutes Per Bar, Up)'); Symbol := Params[0]; FMinutesPerBar := Params[1]; FUp := Params[2]; Inherited Create; Factory := CreateCciFactory(FMinutesPerBar); Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(OnNewCciData, CciData, Link); AddAutoLink(Link); DoInCorrectThread(Initialize) End; Procedure TWoodieCciBuySell.OnNewCciData; Var NewState : TWoodieCciState; Begin NewState := CurrentState; If (NewState = wcsTriggered) And (FPreviousState = wcsReady) Then Report('CCI ' + IfThen(FUp, 'rising above -100', 'falling below +100') + ' on a ' + IntToStr(FMinutesPerBar) + ' minute chart'); FPreviousState := NewState End; Procedure TWoodieCciBuySell.Initialize; Begin If FPreviousState = wcsUnknown Then FPreviousState := CurrentState End; Function TWoodieCciBuySell.CurrentState : TWoodieCciState; Begin If Not CciData.IsValid Then Result := wcsUnknown Else If FUp Then If CciData.GetDouble > -100.0 Then Result := wcsTriggered Else Result := wcsReady Else If CciData.GetDouble < 100.0 Then Result := wcsTriggered Else Result := wcsReady End; //////////////////////////////////////////////////////////////////////// // TVwapDivergenceAlert //////////////////////////////////////////////////////////////////////// Type TVwapDivergenceAlert = Class(TAlert) Private TosData : TGenericTosDataNode; VwapData : TGenericDataNode; FLastReported : Double; FUp : Boolean; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TVwapDivergenceAlert.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=2, 'Expected params: (Symbol, Up)'); Symbol := Params[0]; FUp := Params[1]; Inherited Create; TGenericTosDataNode.Find(Symbol, Nil, TosData, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.CreateWithArgs(TVWAP, Symbol); Factory.Find(NewData, VwapData, Link); AddAutoLink(Link) End; Procedure TVwapDivergenceAlert.NewData; Var CurrentVwap, CurrentPrice, Divergence : Double; Direction, Msg : String; Begin If VwapData.IsValid And TosData.IsValid Then Begin CurrentVwap := VwapData.GetDouble; CurrentPrice := TosData.GetLast^.Price; If (CurrentVwap > 0) And (CurrentPrice > 0) Then Begin Divergence := (CurrentPrice - CurrentVwap) / CurrentVwap * 100; If Not FUp Then Divergence := - Divergence; Divergence := Floor(Divergence); If Divergence > FLastReported Then Begin { If the integer percent divergence is higher than before, we print the value, and we raise the minimum. The next report will have to be at least 1.0 higher. } If FUp Then Direction := 'above' Else Direction := 'below'; If Divergence < 0.5 Then Msg := 'Crossed ' + Direction + ' VWAP' Else Msg := Format('Trading %.0f%% %s VWAP', [Divergence, Direction]); Report(Msg, Divergence); FLastReported := Divergence End Else If Divergence < FLastReported - 1.25 Then { If the divergence goes back at least 5% then we lower the minimum. Maybe it was a bad print, so we don't want that to ecplipse a good print, or maybe the market is really moving that much, so the move is still interesting. Either way, we can't go below -0.5, so we always report 0% as a minimum. } FLastReported := Max(-0.5, Divergence) End End End; //////////////////////////////////////////////////////////////////////// // TSteppingDown //////////////////////////////////////////////////////////////////////// Type TSteppingDown = Class(TAlert) Private TosData : TGenericTosDataNode; L1Data : TGenericL1DataNode; Golden : Boolean; // We are currently in a golden state. We enter a // golden state when the offer is one penny above // the last price, and the offer has a sufficient // size. LastGoldenTime : TDateTime; // This is the end of the last time we // were in a golden state. This might // be set to the current time when we are // in the middle of a golden state. This // might be 0 at the beginning of a golden // state since we aren't required to mark // anything until the end. StepCount : Integer; // 0 means nothing interesting is going on. // 1 means that we've had a golden state, possibly // still in progress. // 2 means that we've had a golden state, the // price moved down, then we had a second golden // state. // No limit on the top end. LastGoldenOfferPrice : Double; // This was the price of the offer the // the last time we were golden, possibly // including now. This is meaningless if // the step count is 0. Otherwise, we // expect this number to go down in order // for the step count to go up. MinGoldenSize : Integer; Procedure InitMinGoldenSize(Symbol : String); Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Procedure TSteppingDown.InitMinGoldenSize(Symbol : String); Var AverageDailyVolume : Integer; Begin If SymbolIsFuture(Symbol) Or SymbolIsIndex(Symbol) Then MinGoldenSize := MaxInt Else Begin AverageDailyVolume := GetAverageDailyVolume(Symbol); If AverageDailyVolume <= 0 Then // Volume not specified, so we have no way to guess MinGoldenSize := MaxInt Else If AverageDailyVolume <= 1000000 Then MinGoldenSize := 10000 Else if AverageDailyVolume <= 3000000 Then MinGoldenSize := 15000 Else if AverageDailyVolume <= 10000000 Then MinGoldenSize := 30000 Else MinGoldenSize := MaxInt End End; Procedure TSteppingDown.NewData; Var PreviouslyGolden : Boolean; Procedure Reset; Begin { Reset } PreviouslyGolden := False; LastGoldenTime := 0; StepCount := 0; LastGoldenOfferPrice := 0 End; { Reset } Const MaxPause = 1.0 / 24.0 / 60.0; // We can go one minute before giving up. Var Current : PL1Data; Last : PTosData; Begin { TSteppingDown.NewData } PreviouslyGolden := Golden; // Check for timeout. The events all show the beginning of a state. // before we check for the new state, we look at the old state, and see // how long it lasted. If Golden Then // The golden state has lasted at least thing long. (Maybe longer, we'll // find out shortly.) So we record this time as a possible timout value. LastGoldenTime := GetSubmitTime Else If (LastGoldenTime > 0) And (GetSubmitTime - LastGoldenTime > MaxPause) Then // We timed out. We were in the process of looking at something // interesting, but not any more. Reset; Golden := False; If TosData.IsValid And L1Data.IsValid Then Begin Current := L1Data.GetCurrent; Last := TosData.GetLast; If Current^.AskPrice > LastGoldenOfferPrice Then // Moving the wrong direction. This could be the start of a // pattern, if we are golden, but this cannot be the continuation // of a pattern. In any case, the offer we were watching went away. Reset; If (Current^.AskSize >= MinGoldenSize) And (Abs(Current^.AskPrice - (Last.Price + 0.01)) < 0.00005) Then Golden := True; // Seems like there are more cases that could cause a reset. I'm not // sure about some of these, and the default is to wait and see if we // get back to a good state. For example, if the offer drops BEFORE // the price, and it's a big enough offer, that looks suspicious. // However, the timing of our data isn't that precise. If Golden Then Begin If (StepCount = 0) Or (Current^.AskPrice < LastGoldenOfferPrice) Then Begin Inc(StepCount); LastGoldenOfferPrice := Current^.AskPrice; If StepCount = 3 Then Report('Offer stepping down.') Else If StepCount > 3 Then Report('Offer continuing to step down.') End End End; End; { TSteppingDown.NewData } Constructor TSteppingDown.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; InitMinGoldenSize(Symbol); If MinGoldenSize < MaxInt Then Begin TGenericTosDataNode.Find(Symbol, NewData, TosData, Link); AddAutoLink(Link); TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link) End End; //////////////////////////////////////////////////////////////////////// // TTradingAboveBelow //////////////////////////////////////////////////////////////////////// Type TTradingAboveBelow = Class(TAlert) Private FSavedCount, FRequiredCount : Integer; FFirstSavedEvent : TDateTime; FLastPrintedEvent : TDateTime; FSymbol : String; FAbove : Boolean; TosData : TGenericTosDataNode; L1Data : TGenericL1DataNode; RegionalData : TTalRegionalBidAsk; FundamentalData : TGenericFundamentalDataNode; Procedure NewEvent(Last : PTosData); Procedure NewData; Procedure CheckFundamentals; Protected Constructor Create(Params : TParamList); Override; End; Constructor TTradingAboveBelow.Create(Params : TParamList); Var Link : TDataNodeLink; SpecialistOnly : Boolean; Begin Assert(Length(Params)=3, 'Expected params: (Symbol, Above, SpecialistOnly)'); FSymbol := Params[0]; FAbove := Params[1]; SpecialistOnly := Params[2]; Inherited Create; If Not SymbolIsIndex(FSymbol) Then If SpecialistOnly Then Begin TGenericFundamentalDataNode.Find(FSymbol, CheckFundamentals, FundamentalData, Link); AddAutoLink(Link); DoInCorrectThread(CheckFundamentals) End Else Begin TGenericTosDataNode.Find(FSymbol, NewData, TosData, Link); AddAutoLink(Link); TGenericL1DataNode.Find(FSymbol, Nil, L1Data, Link); AddAutoLink(Link) End End; Procedure TTradingAboveBelow.CheckFundamentals; Var Exchange : String; Link : TDataNodeLink; Begin If Assigned(FundamentalData) Then If FundamentalData.IsValid Then Begin Exchange := FundamentalData.GetCurrent^.ListedExchange; FundamentalData := Nil; If (Exchange = 'NYSE') Or (Exchange = 'AMEX') Then Begin TGenericTosDataNode.Find(FSymbol, NewData, TosData, Link); AddAutoLink(Link); Link.SetReceiveInput(True); TTalRegionalBidAsk.Find(FSymbol, Nil, RegionalData, Link); AddAutoLink(Link) End End End; Procedure TTradingAboveBelow.NewData; { New TOS Data } Var Current : PL1Data; Last : PTosData; Price : Double; DoNewEvent : Boolean; Begin If TosData.IsValid Then Begin Last := TosData.GetLast; If (Last.EventType = etNewPrint) Then Begin DoNewEvent := False; Price := Last^.Price; If Assigned(L1Data) Then Begin If L1Data.IsValid Then Begin Current := L1Data.GetCurrent; If FAbove Then DoNewEvent := (Price > Current^.BidPrice) And (Price > Current^.AskPrice) Else DoNewEvent := (Price < Current^.BidPrice) And (Price < Current^.AskPrice) End End Else If Assigned(RegionalData) Then Begin If (Not Last^.FormT) And RegionalData.Valid Then Begin If FAbove Then DoNewEvent := (Price > RegionalData.Bid) And (Price > RegionalData.Ask) Else DoNewEvent := (Price < RegionalData.Bid) And (Price < RegionalData.Ask) End End; If DoNewEvent Then NewEvent(Last) End End End; { Something was trading above or below. Apply filters to say if we're going to display it immediately or save it for later. } Procedure TTradingAboveBelow.NewEvent(Last : PTosData); Function Direction : String; Begin { Direction } If FAbove Then Result := 'above' Else Result := 'below' End; { Direction } Function ComparedTo : String; Begin { ComparedTo } If Assigned(L1Data) Then Result := '' Else Result := ' specialist' End; { ComparedTo } Function ExchangeName : String; Begin { ExchangeName } If (Last.Exchange <> '') And (Last.Exchange[1] <> '[') And (Last.Exchange[Length(Last.Exchange)] <> ']') Then Result := ' (' + Last.Exchange + ')' Else Result := '' End; { ExchangeName } Function StandardMessage : String; Begin { StandardMessage } Result := 'Trading ' + Direction + ComparedTo + ExchangeName; End; { StandardMessage } Const { If we haven't seen an event in the past 3 minutes, we display a new alert. } MaxWaitTime = 1.0 / 24.0 / 60.0 * 3.0; Var CurrentTime : TDateTime; Begin { TTradingAboveBelow.NewEvent } CurrentTime := GetSubmitTime; If CurrentTime - FLastPrintedEvent >= MaxWaitTime Then Begin FRequiredCount := 0; FFirstSavedEvent := 0; FLastPrintedEvent := CurrentTime; FSavedCount := 0; Report(StandardMessage, {Last^.Size} 1) End Else Begin If FSavedCount >= FRequiredCount Then Begin If FSavedCount >= 1 Then Report(StandardMessage + ' ' + IntToStr(Succ(FSavedCount)) + ' times in ' + DurationString(FFirstSavedEvent, CurrentTime), {Last^.Size} Succ(FSavedCount)*2) Else Report(StandardMessage, {Last^.Size} 2); FLastPrintedEvent := CurrentTime; FRequiredCount := FRequiredCount * 2 + 1; FFirstSavedEvent := 0; FSavedCount := 0 End Else Begin Inc(FSavedCount); If FFirstSavedEvent = 0 Then FFirstSavedEvent := CurrentTime End End End; { TTradingAboveBelow.NewEvent } //////////////////////////////////////////////////////////////////////// // TNR7 //////////////////////////////////////////////////////////////////////// Type TNR7 = Class(TAlert) Private CandleData : TStandardCandles; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TNR7.Create(Params : TParamList); Var Symbol : String; MinutesPerBar : Integer; Link : TDataNodeLink; Begin Assert(Length(Params)=2, 'Expected params: (Symbol, Minutes / Bar)'); Symbol := Params[0]; MinutesPerBar := Params[1]; Inherited Create; TStandardCandles.Find(Symbol, MinutesPerBar, NewData, CandleData, Link); AddAutoLink(Link) End; Procedure TNR7.NewData; Const LookBack = 6; // NR 7 Means to compare the 7th bar to the previous 6. Var Candles : TCandleArray; Function CheckForNR7Once(BarsToSkip : Integer) : Boolean; Var Compare : Double; I : Integer; Begin { CheckForNR7Once } Result := False; If (BarsToSkip + LookBack) >= Length(Candles) Then Exit; I := Pred(Length(Candles) - BarsToSkip); With Candles[I] Do Compare := High - Low; For I := I - LookBack To Pred(I) Do With Candles[I] Do If Compare >= (High - Low) Then Exit; Result := True End; { CheckForNR7Once } Var Quality : Integer; Begin { TNR7.NewData } Candles := CandleData.GetHistory; Quality := 0; While CheckForNR7Once(Quality) Do Inc(Quality); If Quality = 1 Then Report('NR7', 1) Else If Quality > 1 Then Report('NR7-' + IntToStr(Quality), Quality) End; { TNR7.NewData } //////////////////////////////////////////////////////////////////////// // TUnusualNumberOfPrints //////////////////////////////////////////////////////////////////////// { A lot of people have asked for something like this, including Cash from Bright Trading. This looks at the number of prints, instead of volume. For the realtime data we use the actual rate at which we receive new prints. However, for the historical data, we cheat. For simplicity we start from the volume by time statics that we already have for the relative volume alerts and filters. We add one new peice of historical data, the average size of a print, to estimate the typical number of prints that we expect at a particular time. My biggest concern is that the historical data is only precise to a 15 minute boundary. Some detailed analysis off line shows that the first minute can easily have 2-3 times as many prints at the 5th or 10th minute after the open. However, our algorithm really requires that we use wide bands. If we had more precise data, we'd have no way to deal with it. The design of this class is based on the design of the high relative volume alert. The good part of HRV was that we inspect each print, and when we had enough prints we check the time. This can allow us to report things more quickly. However, this lead to two problems. First, we had some alerts with a quality that was off the scale, 1/0. Second, we would often report several alerts in a row, and never combined alerts, so you couldn't look at just the quality to find big stuff. I start by looking for as many prints as we usually see in a 15 minute period. I also break time up into 3 minute periods. If we get 15 minutes worth of data in 3 minutes or less, then we've met the minimum criteria for an alert, and we report an alert. To avoid the 1/0 problem, and to smooth things out in general, we always divide by 3 minutes, regardless of how long it really took. (If it took longer than 3 minutes, then we would have aborted this attempt and tried again.) Then, if we get another block of 15 minutes worth in the 3 minute period, we report another alert with a higher quality. So this smoothing allows us to give a more useful, if less precise, quality measurement, compared to HRV. The thought is that we're unlikely to see 15 minutes worth of prints in an instant, but a very low number of prints after that, so we don't care as much as we did in HRV. For simplicity I set the expectations based based on which 15 minute period is is process when we start looking at the data. At the worst, we could be off by just under 3 minutes. The more interesting the alert, and the more quickly we are rattling off alerts, the more precise this estimate is. If we go 3 minutes without getting enough prints to make a report, we start fresh, with one exception. We don't immediately report an alert as soon as we get to the 5x level. We wait until we pass the quality of the last alert that we reported, so we don't have a lot of repeats. If we don't report any alerts, over time we will gradually lower that requirement. The 5x minimum was one of the first reqiurements because it prevents us from sending too many alerts. When reviewing the data we saw that the number of alerts per minute was very spikey, as expected. However, this also helped us a lot once we picked the algorithm, because it gives us more room to smooth out the data and not worry about some of the approximations that we are making, such as arbitrarily breaking every 15 minutes. Psudocode / Basic Idea: Initialization: Expect := Number of prints that we expect in a 15 minute period. StartTime := 0 PrintsRequired := Expect LevelToBeat := 0 Count := 0 RecentlyAlerted := False On New Print: If (PrintTime > StartTime + 3 minutes) Then StartTime := PrintTime PrintsRequired := Expect If RecentlyAlerted Then RecentlyAlerted := False LevelToBeat := Count Else LevelToBeat := Max(0, LevelToBeat – 1) Count := 0 Dec(PrintsRequired) If PrintsRequired <= 0 Then Inc(Count) If Count > LevelToBeat Then Report Alert PrintsRequired := Expect RecentlyAlerted := True } Type TUnusualNumberOfPrints = Class(TAlert) Private Expect : Integer; { Number of prints required to be interesting } PreviousPeriod : TDateTime; { Review PrintsRequired when the period changes. } ResetTime : TDateTime; { Give up on the previous alert and start counting again. } PrintsRequired : Integer; { Number of prints before our next check } LevelToBeat : Integer; { Quality required for an alert. } Count : Integer; { Number of groups of prints in this time slice. } RecentlyAlerted : Boolean; { The last time we reset was from enough prints, not time. } VolumeData : TAverageHistoricalData; TosData : TGenericTosDataNode; SharesPerPrintData : TGenericDataNode; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TUnusualNumberOfPrints.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Factory : IGenericDataNodeFactory; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; PreviousPeriod := AHVNone; TGenericTosDataNode.Find(Symbol, NewData, TosData, Link); AddAutoLink(Link); TAverageHistoricalData.Find(Symbol, Nil, VolumeData, Link); AddAutoLink(Link); Factory := TGenericDataNodeFactory.FindFactory('SharesPerPrint').Duplicate; Factory.SetValue(SymbolNameKey, Symbol); Factory.Find(NewData, SharesPerPrintData, Link); AddAutoLink(Link) End; Procedure TUnusualNumberOfPrints.NewData; Const ThreeMinutes = 1.0 / 24.0 / 60.0 * 3.0; Var SubmitTime : TDateTime; Period : TAHVPeriods; Begin If Not TosData.IsValid Then Exit; If TosData.GetLast^.EventType <> etNewPrint Then Exit; SubmitTime := GetSubmitTime; If CompareDateTime(SubmitTime, ResetTime) <> LessThanValue Then Begin ResetTime := SubmitTime + ThreeMinutes; // Get our new expecatations. Expect := 0; { In case of any problems, we set this to 0 so we don't do any further processing. } Period := TAverageHistoricalData.HigherTimeFrame(SubmitTime); If Period <> PreviousPeriod Then Begin PreviousPeriod := Period; If VolumeData.IsValid And SharesPerPrintData.IsValid Then Begin If Period = AHVPreMarket Then Inc(Period) Else If Period = AHVAfterMarket Then Dec(Period); Try Expect := Round(VolumeData.GetVolume(Period) / SharesPerPrintData.GetDouble); Except End; If Expect < 5 Then Expect := 0 End End; // Reset the search PrintsRequired := Expect; If RecentlyAlerted Then LevelToBeat := Count Else Dec(LevelToBeat); LevelToBeat := Max(0, LevelToBeat); RecentlyAlerted := False; Count := 0 End; If Expect > 0 Then Begin Dec(PrintsRequired); If PrintsRequired < 1 Then Begin Inc(Count); If Count > LevelToBeat Then Report('Unusual number of prints', Count * 5.0); PrintsRequired := Expect; RecentlyAlerted := True End End End; //////////////////////////////////////////////////////////////////////// // TLargeSpread //////////////////////////////////////////////////////////////////////// { This alert is only defined for NYSE stocks. A "large spread" is defined as at least 50 cents. We only report on the transition from the ready state to the triggered state. Furthermore, we do not report an alert unless we have been in the ready state for at least 30 seconds. Finally, if the data feed is down, including when we first start, that's a third state. This state does not cause an alert, but is also supresses future alerts, just like the trigged state does. } Type TLargeSpread = Class(TAlert) Private FTriggered : Boolean; FLastTriggerTime : TDateTime; FSymbol : String; RegionalData : TTalRegionalBidAsk; FundamentalData : TGenericFundamentalDataNode; Procedure NewData; Procedure CheckFundamentals; Protected Constructor Create(Params : TParamList); Override; End; Constructor TLargeSpread.Create(Params : TParamList); Var Link : TDataNodeLink; Begin Assert(Length(Params)=1, 'Expected params: (Symbol)'); FSymbol := Params[0]; Inherited Create; If Not SymbolIsIndex(FSymbol) Then Begin TGenericFundamentalDataNode.Find(FSymbol, CheckFundamentals, FundamentalData, Link); AddAutoLink(Link); DoInCorrectThread(CheckFundamentals) End; FTriggered := True; End; Procedure TLargeSpread.CheckFundamentals; Var Exchange : String; Link : TDataNodeLink; Begin If Assigned(FundamentalData) Then If FundamentalData.IsValid Then Begin Exchange := FundamentalData.GetCurrent^.ListedExchange; FundamentalData := Nil; If Exchange = 'NYSE' Then Begin TTalRegionalBidAsk.Find(FSymbol, NewData, RegionalData, Link); AddAutoLink(Link); Link.SetReceiveInput(True) End End End; Procedure TLargeSpread.NewData; Const MinBreakTime = 1.0 / 24.0 / 60.0 / 2.0; { 30 seconds } Var NewState : (nsReady, nsTriggered, nsNeither); Begin If Not RegionalData.Valid Then NewState := nsNeither Else If RegionalData.Ask - RegionalData.Bid > 0.499999 Then NewState := nsTriggered Else NewState := nsReady; If (NewState = nsTriggered) And Not FTriggered And (GetSubmitTime - FLastTriggerTime > MinBreakTime) Then Report('Large Spread'); If FTriggered Then FLastTriggerTime := GetSubmitTime; FTriggered := NewState <> nsReady End; //////////////////////////////////////////////////////////////////////// // TGenericCross // // This has been coming for a long time. You can insert any two factories // that return doubles. When one crosses above the other, we report a // predefined messages. Similar things have existed -- see // ConfirmedCrossing.pas -- but this is the first completely generic one. // // Message -- This is a fixed message to be displayed any time we fire // the alert. // ReportFactory -- This is the data which needs to cross above the // other data. It must move from below the other data // to above the other data. It may be equal in between. // CompareFactory -- We compare the data in reportFactory to this data. // IgnoreEvents -- 0 means that we check any time either data node // changes. 1 means that we ignore events from // ReportFactory (the first factory) and only watch // CompareFactory (the second factory). 2 means that we // watch the first one and ignore the second one. // This is an optimization for the SMAs because we know // they update at the same time. And we know that the // shorter one will always be valid if the longer one // is. In some of the previous comparisons, we // specifically defined it so that the price had to move, // not the reference number, to generate an alert. // // Note that symbol is not explicitly included as an input. Presumably // it is an imput in both ReportFactory and CompareFactory. However, it // doesn't have to be. // // Note: A few alerts like this will have one common data node watching // for both cases, then simple adapters around it to generate the up or down // alerts. Because there is so little work in this data node, there // wasn't any real nead for that. In fact, it probably would have been // more complicated that way. Also, the initial case for this alert is // a pair of intraday sma objects. And those already have a lot of // caching to ensure that they don't do a lot of work if you call them // multiple times. //////////////////////////////////////////////////////////////////////// Type TGenericCross = Class(TAlert) Private FPrimed : Boolean; FMessage : String; ReportData, CompareData : TGenericDataNode; Procedure NewData; Protected Constructor Create(Params : TParamList); Override; End; Constructor TGenericCross.Create(Params : TParamList); Var Link : TDataNodeLink; Factory : IGenericDataNodeFactory; IgnoreEvents : Integer; Begin Assert(Length(Params) = 4, 'Expected params: (Message, ReportFactory, CompareFactory, IgnoreEvents)'); FMessage := Params[0]; IgnoreEvents := Params[3]; Inherited Create; Factory := IUnknown(Params[1]) As IGenericDataNodeFactory; If IgnoreEvents = 1 Then Factory.Find(Nil, ReportData, Link) Else Factory.Find(NewData, ReportData, Link); AddAutoLink(Link); Factory := IUnknown(Params[2]) As IGenericDataNodeFactory; If IgnoreEvents = 2 Then Factory.Find(Nil, CompareData, Link) Else Factory.Find(NewData, CompareData, Link); AddAutoLink(Link) End; Procedure TGenericCross.NewData; Var Difference : Double; Begin If Not (ReportData.IsValid And CompareData.IsValid) Then FPrimed := False Else Begin Difference := ReportData.GetDouble - CompareData.GetDouble; If Difference > 0 Then Begin If FPrimed Then Begin Report(FMessage); FPrimed := False End End Else If Difference < 0 Then FPrimed := True End End; Function SmaCrossFactory(Faster : Integer; Above : Boolean; Slower : Integer; Period : Integer) : IGenericDataNodeFactory; Var Msg : String; FastFactory, SlowFactory, AboveFactory, BelowFactory : IGenericDataNodeFactory; Ignore : Integer; Begin Msg := IntToStr(Faster) + ' crossed '; If Above Then Msg := Msg + 'above ' Else Msg := Msg + 'below '; Msg := Msg + IntToStr(Slower) + ' (' + IntToStr(Period) + ' minute)'; FastFactory := CreateIntradaySmaFactory(Period, Faster); SlowFactory := CreateIntradaySmaFactory(Period, Slower); If Above Then Begin AboveFactory := FastFactory; BelowFactory := SlowFactory; Ignore := 1 End Else Begin AboveFactory := SlowFactory; BelowFactory := FastFactory; Ignore := 2 End; Result := TGenericDataNodeFactory.CreateWithArgs(TGenericCross, Msg, AboveFactory, BelowFactory, Ignore) End; //////////////////////////////////////////////////////////////////////// // TNyseImbalance //////////////////////////////////////////////////////////////////////// Type TNyseImbalance = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private TosData : TGenericTosDataNode; L1Data : TGenericL1DataNode; FLastReport : Integer; FMoreBuyers : Boolean; Procedure NewData; Published End; Constructor TNyseImbalance.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params) = 2, 'Expected params: (Symbol, More Buyers)'); Symbol := Params[0]; FMoreBuyers := Params[1]; Inherited Create; TGenericL1DataNode.Find(Symbol, NewData, L1Data, Link); AddAutoLink(Link); TGenericTosDataNode.Find(Symbol, Nil, TosData, Link); AddAutoLink(Link) End; Procedure TNyseImbalance.NewData; Var Volume, Imbalance : Integer; Begin If L1Data.IsValid And TosData.IsValid Then Begin Volume := TosData.GetLast^.Volume; If (Volume > 0) Then Begin Imbalance := L1Data.GetCurrent^.NyseImbalance; If Not FMoreBuyers Then Imbalance := -Imbalance; If Imbalance > FLastReport Then Begin FLastReport := Imbalance; Report(IfThen(FMoreBuyers, 'Buy Imbalance. ', 'Sell Imbalance. ') + IntToStr(Imbalance) + ' shares.', Imbalance * 100.0 / Volume); End End End End; //////////////////////////////////////////////////////////////////////// // TOneMinuteVolumeSpike // // We're making this as simple as possible. We look at the volume for // the last minute, breaking on exact minutes. So we're looking at the // last 0 to 59.999 seconds worth of data. // // This will look a lot like running up now, but with volume instead of // prices. When the volume gets to 200% of expected, we report an alert. // And then again at 250%. And then again at 300%. Etc. Those numbers // are constants in this code. The user can use the quality filter to // limit these further. // // To keep this thing from going crazy, we have a second constraint. // The quailty should be at least 1/2 as big as we reported last time, // or we won't report it. Last time is usually the last minute. If // trading drops significantly, then we use 1/4 of the value from 2 times // ago, or 1/8 the value from 3 times ago, etc. Whichever is the largest // we have to beat. // // We use the standard historical volume expecations, which change during // the day. These are a little simpler here than in some places because // we are always resetting at exact one minute boundaries. Note that // we require the quality, not the volume, to be twice as much as the // previous candle. //////////////////////////////////////////////////////////////////////// Type TOneMinuteVolumeSpike = Class(TAlert) Protected Constructor Create(Params : TParamList); Override; Private VolumeData : TAverageHistoricalData; TosData : TGenericTosDataNode; FMinQualityToReport : Double; FExpectedVolume : Double; FNewCandleTime : TDateTime; FStartingVolume : Integer; Procedure NewData; Published End; Constructor TOneMinuteVolumeSpike.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params) = 1, 'Expected params: (Symbol)'); Symbol := Params[0]; Inherited Create; TGenericTosDataNode.Find(Symbol, NewData, TosData, Link); AddAutoLink(Link); TAverageHistoricalData.Find(Symbol, Nil, VolumeData, Link); AddAutoLink(Link) End; Procedure TOneMinuteVolumeSpike.NewData; Const OneMinute = 1.0 / 24.0 / 60.0; Var TimeFrame : TAHVPeriods; Volume : Integer; Time : TDateTime; Quality : Double; ReportInterval : Double; Begin If TosData.IsValid Then Begin Volume := TosData.GetLast^.Volume; Time := GetSubmitTime; If CompareTime(Time, FNewCandleTime) = LessThanValue Then Begin // Same candle. Keep comparing to the baseline. If Volume <= FStartingVolume Then // If there was a correction, the volume could go backwards. // This would be a reasonable response. FStartingVolume := Volume Else If FExpectedVolume > 0 Then Begin Quality := (Volume - FStartingVolume) / FExpectedVolume; If Quality >= FMinQualityToReport Then Begin Report('Trading at ' + IntToStr(Round(Quality * 100)) + '% of historical volume.', Quality); If Quality < 10 Then // This needs to be 0.5 higher than before. But we // always push it back down to a round number. So if we // were expecing a min of 3.0 this time, and we got 3.0 // or 3.499, next time we need a min of 3.5. ReportInterval := 0.5 Else If Quality < 25 Then // Report when we cross an integer. ReportInterval := 1.0 Else If Quality < 100 Then ReportInterval := 5.0 Else If Quality < 1000 Then // This scale is pretty aribtrary. Ideally we'd have // some cap where we stop reporting, or a formula that // would keep trimming these forever. ReportInterval := 10.0 Else If Quality < 10000 Then ReportInterval := 100.0 Else ReportInterval := 1000.0; FMinQualityToReport := (Floor(Quality / ReportInterval) + 1) * ReportInterval End End End Else Begin // Start a new candle. FNewCandleTime := IncMilliSecond(Time, 60000 - MilliSecondOfTheMinute(Time)); FStartingVolume := Volume; // Note, if we skip a period due to lack of activity, we don't // make a special case. So perhaps the volume number decreases // more slowly than expected. That's just not an interesting // case. FMinQualityToReport := Max(2.0, FMinQualityToReport/2.0); TimeFrame := VolumeData.HigherTimeFrame(Time); If TimeFrame = AHVPreMarket Then Inc(TimeFrame) Else If TimeFrame = AHVAfterMarket Then Dec(TimeFrame); FExpectedVolume := VolumeData.GetVolume(TimeFrame) / 15.0 End End End; //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// Initialization TGenericDataNodeFactory.StoreStandardFactory('HighRelativeVolume', THighVolume); TGenericDataNodeFactory.StoreStandardFactory('GapDownReversal', TGapDownReversal); TGenericDataNodeFactory.StoreStandardFactory('GapUpReversal', TGapUpReversal); TGenericDataNodeFactory.StoreStandardFactory('BlockPrint', TBlockPrint); TGenericDataNodeFactory.StoreStandardFactory('GapRetracementArcUp', TGapRetracementArcUp); TGenericDataNodeFactory.StoreStandardFactory('GapRetracementArcDown', TGapRetracementArcDown); TGenericDataNodeFactory.StoreFactory('1MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 1)); TGenericDataNodeFactory.StoreFactory('1MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 1)); TGenericDataNodeFactory.StoreFactory('5MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 5)); TGenericDataNodeFactory.StoreFactory('5MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 5)); TGenericDataNodeFactory.StoreFactory('10MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 10)); TGenericDataNodeFactory.StoreFactory('10MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 10)); TGenericDataNodeFactory.StoreFactory('15MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 15)); TGenericDataNodeFactory.StoreFactory('15MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 15)); TGenericDataNodeFactory.StoreFactory('30MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 30)); TGenericDataNodeFactory.StoreFactory('30MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 30)); TGenericDataNodeFactory.StoreFactory('60MinHigh', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, True, 60)); TGenericDataNodeFactory.StoreFactory('60MinLow', TGenericDataNodeFactory.CreateWithArgs(IntraDayHighLow, StandardSymbolPlaceHolder, False, 60)); TGenericDataNodeFactory.StoreFactory('LargeBidSize', TGenericDataNodeFactory.CreateWithArgs(TLargeBidOrAsk, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('LargeAskSize', TGenericDataNodeFactory.CreateWithArgs(TLargeBidOrAsk, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreFactory('PercentUpForDay', TGenericDataNodeFactory.CreateWithArgs(TPercentChangeForTheDay, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('PercentDownForDay', TGenericDataNodeFactory.CreateWithArgs(TPercentChangeForTheDay, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreStandardFactory('StrongVolume', TStrongVolume); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout1', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 1)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown1', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 1)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout5', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 5)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown5', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 5)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout10', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 10)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown10', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 10)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout15', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 15)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown15', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 15)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout30', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 30)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown30', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 30)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakout60', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, True, 60)); TGenericDataNodeFactory.StoreFactory('OpeningRangeBreakdown60', TGenericDataNodeFactory.CreateWithArgs(TOpeningRangeBreak, StandardSymbolPlaceHolder, False, 60)); TGenericDataNodeFactory.StoreFactory('BrightBandsUp', TGenericDataNodeFactory.CreateWithArgs(TBrightBands, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('BrightBandsDown', TGenericDataNodeFactory.CreateWithArgs(TBrightBands, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreFactory('PtsEntryUp5', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 5, True)); TGenericDataNodeFactory.StoreFactory('PtsEntryDown5', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 5, False)); TGenericDataNodeFactory.StoreFactory('PtsEntryUp15', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 15, True)); TGenericDataNodeFactory.StoreFactory('PtsEntryDown15', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 15, False)); TGenericDataNodeFactory.StoreFactory('PtsEntryUp30', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 30, True)); TGenericDataNodeFactory.StoreFactory('PtsEntryDown30', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 30, False)); TGenericDataNodeFactory.StoreFactory('PtsEntryUp90', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 90, True)); TGenericDataNodeFactory.StoreFactory('PtsEntryDown90', TGenericDataNodeFactory.CreateWithArgs(TPtsEntrySignals, StandardSymbolPlaceHolder, 90, False)); TGenericDataNodeFactory.StoreFactory('WoodiesCciEntryUp3', TGenericDataNodeFactory.CreateWithArgs(TWoodieCciBuySell, StandardSymbolPlaceHolder, 3, True)); TGenericDataNodeFactory.StoreFactory('WoodiesCciEntryDown3', TGenericDataNodeFactory.CreateWithArgs(TWoodieCciBuySell, StandardSymbolPlaceHolder, 3, False)); TGenericDataNodeFactory.StoreFactory('VwapDivergenceUp', TGenericDataNodeFactory.CreateWithArgs(TVwapDivergenceAlert, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('VwapDivergenceDown', TGenericDataNodeFactory.CreateWithArgs(TVwapDivergenceAlert, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreStandardFactory('SteppingDown', TSteppingDown); TGenericDataNodeFactory.StoreFactory('TradingAbove', TGenericDataNodeFactory.CreateWithArgs(TTradingAboveBelow, StandardSymbolPlaceHolder, True, False)); TGenericDataNodeFactory.StoreFactory('TradingBelow', TGenericDataNodeFactory.CreateWithArgs(TTradingAboveBelow, StandardSymbolPlaceHolder, False, False)); TGenericDataNodeFactory.StoreFactory('TradingAboveSpecialist', TGenericDataNodeFactory.CreateWithArgs(TTradingAboveBelow, StandardSymbolPlaceHolder, True, True)); TGenericDataNodeFactory.StoreFactory('TradingBelowSpecialist', TGenericDataNodeFactory.CreateWithArgs(TTradingAboveBelow, StandardSymbolPlaceHolder, False, True)); TGenericDataNodeFactory.StoreFactory('NR7-15', TGenericDataNodeFactory.CreateWithArgs(TNR7, StandardSymbolPlaceHolder, 15)); TGenericDataNodeFactory.StoreStandardFactory('UnusualNumberOfPrints', TUnusualNumberOfPrints); TGenericDataNodeFactory.StoreStandardFactory('LargeSpread', TLargeSpread); TGenericDataNodeFactory.StoreFactory('Sma8Above20_2', SmaCrossFactory(8, True, 20, 2)); TGenericDataNodeFactory.StoreFactory('Sma8Above20_5', SmaCrossFactory(8, True, 20, 5)); TGenericDataNodeFactory.StoreFactory('Sma8Above20_15', SmaCrossFactory(8, True, 20, 15)); TGenericDataNodeFactory.StoreFactory('Sma8Below20_2', SmaCrossFactory(8, False, 20, 2)); TGenericDataNodeFactory.StoreFactory('Sma8Below20_5', SmaCrossFactory(8, False, 20, 5)); TGenericDataNodeFactory.StoreFactory('Sma8Below20_15', SmaCrossFactory(8, False, 20, 15)); TGenericDataNodeFactory.StoreFactory('Sma20Above200_2', SmaCrossFactory(20, True, 200, 2)); TGenericDataNodeFactory.StoreFactory('Sma20Above200_5', SmaCrossFactory(20, True, 200, 5)); TGenericDataNodeFactory.StoreFactory('Sma20Above200_15', SmaCrossFactory(20, True, 200, 15)); TGenericDataNodeFactory.StoreFactory('Sma20Below200_2', SmaCrossFactory(20, False, 200, 2)); TGenericDataNodeFactory.StoreFactory('Sma20Below200_5', SmaCrossFactory(20, False, 200, 5)); TGenericDataNodeFactory.StoreFactory('Sma20Below200_15', SmaCrossFactory(20, False, 200, 15)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_1', SmaCrossFactory(5, True, 8, 1)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_2', SmaCrossFactory(5, True, 8, 2)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_4', SmaCrossFactory(5, True, 8, 4)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_5', SmaCrossFactory(5, True, 8, 5)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_10', SmaCrossFactory(5, True, 8, 10)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_20', SmaCrossFactory(5, True, 8, 20)); TGenericDataNodeFactory.StoreFactory('Sma5Above8_30', SmaCrossFactory(5, True, 8, 30)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_1', SmaCrossFactory(5, False, 8, 1)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_2', SmaCrossFactory(5, False, 8, 2)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_4', SmaCrossFactory(5, False, 8, 4)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_5', SmaCrossFactory(5, False, 8, 5)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_10', SmaCrossFactory(5, False, 8, 10)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_20', SmaCrossFactory(5, False, 8, 20)); TGenericDataNodeFactory.StoreFactory('Sma5Below8_30', SmaCrossFactory(5, False, 8, 30)); TGenericDataNodeFactory.StoreFactory('NyseBuyImbalance', TGenericDataNodeFactory.CreateWithArgs(TNyseImbalance, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('NyseSellImbalance', TGenericDataNodeFactory.CreateWithArgs(TNyseImbalance, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreStandardFactory('VolumeSpike1', TOneMinuteVolumeSpike); End.