Unit TrailingStop; Interface Implementation Uses DataNodes, GenericDataNodes, AlertBase, GenericTosDataNode, StandardPlaceHolders, DebugOutput, Prices, Math, SysUtils, StrUtils; //////////////////////////////////////////////////////////////////////// // TTrailingStop //////////////////////////////////////////////////////////////////////// // Return this if no step size is appropriate; Const BadStepSize = 0.0; Type // More profitable means to update our internal data, so we know about the // next pullback. More reportable means we're going in the other direction. TCompare = (tsMoreProfitable, tsEqual, tsMoreReportable); { Each row of history represents a range of prices. The most profitable price is the first price. The most reportable price is the next place where we want to create an alert. Creating an alert causes the next price field to move to the more reportable side, expanding the range. New ranges are always added to the bottom. Index 0 is always the oldest range. The age of the range depends on the first print that created the range. The range for each row must completely fit inside the range for the row above it. After handling a new print, the print should be within all of the ranges. The print must be more reportable than the most profitable print, and more profitable than the most reportable price. If a new range has the same most profitable price as the range above it, the new range is deleted. If a new range has a most profitable price more profitable than the row above it, the row above is deleted. Many rows may be deleted at once. If a range has a most reportable point which is more reportable than ranges above (older than) it, the new range is deleted. If a new print could cause multiple alerts, we print the alert from the oldest range. When we extend the range for this row, that will cause us to delete the other rows which could have reported. Following the logic above, you never have to delete a row from the middle. If a print causes us to delete multiple rows, it will always delete all rows from a certain point down, all the way to the bottom, possibly excepting one new row which was created by the same print. We choose to create rows only after all of the required deletions, to make the deletions simple. Potentially, any print could create a new row. However, most of the time the new row would immediately be deleted. For effeciency the code never creates a row if it knows that it will have to immediately delete it. The idea is to create a new record when we have gone as far to the profitable side as we are going, and we are about to switch to the reportable side. As we move to the more profitable stide, we keep updating the last record, pushing it in the right direction. Then, when we start moving in the other direction, the last record stays in place. Small jitters do not always cause new rows, because they are too close to the reportable side of the rows above. } TTrailingStop = Class(TAlert) Private // Down means that we were long, so we report when we go down. FDown : Boolean; TosData : TGenericTosDataNode; HistoryCount : Integer; History : Array Of Record InitialPrice : Double; InitialTime : TDateTime; NextPrice : Double End; PreviouslyValid : Boolean; Procedure NewTosData; Procedure MakeRoomForHistory; Function Compare(A, B : Double) : TCompare; Procedure FindLevels(StartingPoint, CurrentPoint : Double; Out BrokenLevelText : String; Out BrokenLevelQuality, Next : Double); Procedure FindFirstLevel(StartingPoint : Double; Out NextLevel : Double; Out Success : Boolean); Protected Constructor Create(Params : TParamList); Override; Procedure Reset; Function MoreReportableDirection : Integer; Function GetStepSize(StartingPoint : Double) : Double; Virtual; Abstract; Function DescriptionOfMove(PriceIncrease: Double; Steps : Integer) : String; Virtual; Abstract; Function QualityFromSteps(Steps : Integer) : Double; Virtual; Abstract; End; Procedure TTrailingStop.Reset; Begin HistoryCount := 0; SetLength(History, 0); PreviouslyValid := False; NewTosData End; Constructor TTrailingStop.Create(Params : TParamList); Var Symbol : String; Link : TDataNodeLink; Begin Assert(Length(Params) = 2, 'Expected: (Symbol, Down)'); Symbol := Params[0]; FDown := Params[1]; Inherited Create; TGenericTosDataNode.Find(Symbol, NewTosData, TosData, Link); AddAutoLink(Link) End; Function TTrailingStop.MoreReportableDirection : Integer; Begin If FDown Then Result := -1 Else Result := 1 End; Function TTrailingStop.Compare(A, B : Double) : TCompare; Begin If A = B Then Result := tsEqual Else If (A > B) XOr FDown Then // A is more reportable than B // A = 10.00, B = 9.99, Direction is Up (We are short, so we report when it goes up, against us.) // A = 9.99, B = 10.00, Direction is Down (We are long, so we report when it goes down, against us.) Result := tsMoreReportable Else Result := tsMoreProfitable End; Procedure TTrailingStop.MakeRoomForHistory; Begin Inc(HistoryCount); If HistoryCount > Length(History) Then SetLength(History, Max(4, 2*Length(History))) End; Procedure TTrailingStop.NewTosData; Var Last : PTosData; I : Integer; WasReportable, CanCreateNew : Boolean; BrokenLevelText : String; BrokenLevelQuality, NextPriceLevel : Double; Begin Try If Not TosData.IsValid Then PreviouslyValid := False Else Begin Last := TosData.GetLast; I := 0; While I < HistoryCount Do If Compare(Last^.Price, History[I].InitialPrice) <> tsMoreReportable Then Begin HistoryCount := I; Break End Else Inc(I); WasReportable := False; I := 0; While I < HistoryCount Do If Compare(Last^.Price, History[I].NextPrice) <> tsMoreProfitable Then Begin FindLevels(History[I].InitialPrice, Last^.Price, BrokenLevelText, BrokenLevelQuality, History[I].NextPrice); If PreviouslyValid Then Report('Moved: ' + BrokenLevelText + ' in ' + DurationString(History[I].InitialTime, GetSubmitTime) + '.', BrokenLevelQuality); HistoryCount := Succ(I); WasReportable := True; Break End Else Inc(I); If Not WasReportable Then Begin FindFirstLevel(Last^.Price, NextPriceLevel, CanCreateNew); If CanCreateNew And ((HistoryCount = 0) Or ((Compare(NextPriceLevel, History[Pred(HistoryCount)].NextPrice) = tsMoreProfitable) And (Last.Price <> History[Pred(HistoryCount)].InitialPrice))) Then Begin MakeRoomForHistory; With History[Pred(HistoryCount)] Do Begin InitialPrice := Last^.Price; InitialTime := GetSubmitTime; NextPrice := NextPriceLevel End End End; PreviouslyValid := True End Except On E : Exception Do Begin // This is probably just paranoia, but I don't like some of the // real division then conversion back to integers. This scares // me. Some odd values could cause a numeric overflow. DebugOutputWindow.AddMessage(E.ClassName + ': "' + E.Message + '" in TTrailingStop.NewTosData'); End End End; Procedure TTrailingStop.FindFirstLevel(StartingPoint : Double; Out NextLevel : Double; Out Success : Boolean); Var Step : Double; Begin Step := GetStepSize(StartingPoint); If Step <= BadStepSize Then Success := False Else Begin NextLevel := StartingPoint + MoreReportableDirection * Step * 2; Success := (NextLevel <> StartingPoint) And (NextLevel > 0) End End; Procedure TTrailingStop.FindLevels(StartingPoint, CurrentPoint : Double; Out BrokenLevelText : String; Out BrokenLevelQuality, Next : Double); Function BasicQuality(StartingQuality : Integer) : Integer; Var ToShift : Cardinal; //Use an unsigned number to avoid an infinite loop! Begin { BasicQuality } If StartingQuality <= 0 Then Result := 0 Else Begin ToShift := StartingQuality; Result := 1; While (ToShift And 1) = 0 Do Begin ToShift := ToShift ShR 1; Result := Result ShL 1 End End End; { BasicQuality } Var Step : Double; Distance : Double; Count : Integer; Begin { TTrailingStop.FindLevels } Step := GetStepSize(StartingPoint); Distance := (CurrentPoint - StartingPoint) * MoreReportableDirection; // Is it possible that, due to round off error, the caller thinks that // we just barely crossed a line, but this code thinks we just barely // missed it? Count := Round(Floor(Distance / Step)); BrokenLevelText := DescriptionOfMove(CurrentPoint - StartingPoint, Count); BrokenLevelQuality := QualityFromSteps(BasicQuality(Count)) * (2 / (2 - Random)); Next := StartingPoint + MoreReportableDirection * Succ(Count) * Step End; { TTrailingStop.FindLevels } //////////////////////////////////////////////////////////////////////// // TTrailingStopPercent //////////////////////////////////////////////////////////////////////// Type TTrailingStopPercent = Class(TTrailingStop) Protected Function GetStepSize(StartingPoint : Double) : Double; Override; Function DescriptionOfMove(PriceIncrease: Double; Steps : Integer) : String; Override; Function QualityFromSteps(Steps : Integer) : Double; Override; End; Function TTrailingStopPercent.GetStepSize(StartingPoint : Double) : Double; Begin Result := StartingPoint * 0.0025 End; Function TTrailingStopPercent.DescriptionOfMove(PriceIncrease: Double; Steps : Integer) : String; Begin Result := IntToStr(Steps ShR 2); Case (Steps Mod 4) Of 1 : Result := Result + '.25'; 2 : Result := Result + '.5'; 3 : Result := Result + '.75'; End; Result := IfThen(MoreReportableDirection > 0, '+', '-') + Result + '%' End; Function TTrailingStopPercent.QualityFromSteps(Steps : Integer) : Double; Begin Result := Steps / 4.0 End; //////////////////////////////////////////////////////////////////////// // TTrailingStopSigma //////////////////////////////////////////////////////////////////////// Type TTrailingStopSigma = Class(TTrailingStop) Private CurrentVolatility : Double; VolatilityData : TGenericDataNode; Procedure ReadVolatilityData; Protected Constructor Create(Params : TParamList); Override; Function GetStepSize(StartingPoint : Double) : Double; Override; Function DescriptionOfMove(PriceIncrease: Double; Steps : Integer) : String; Override; Function QualityFromSteps(Steps : Integer) : Double; Override; End; Procedure TTrailingStopSigma.ReadVolatilityData; Var NewValue : Double; Begin If VolatilityData.IsValid Then Begin NewValue := VolatilityData.GetDouble / 2; // Yuck! A penny is right maybe for most NASDAQ/NYSE/AMEX stocks, but not some of the other posssibilities. If NewValue < 0.01 Then //If NewValue < 0.0001 Then NewValue := 0 End Else NewValue := 0; If NewValue <> CurrentVolatility Then Begin CurrentVolatility := NewValue; Reset End End; Function TTrailingStopSigma.GetStepSize(StartingPoint : Double) : Double; Begin Result := CurrentVolatility / 2 End; Function TTrailingStopSigma.DescriptionOfMove(PriceIncrease: Double; Steps : Integer) : String; Begin Result := FormatPrice(PriceIncrease, True) End; Function TTrailingStopSigma.QualityFromSteps(Steps : Integer) : Double; Begin Result := Steps / 2.0 End; Constructor TTrailingStopSigma.Create(Params : TParamList); Var Symbol : String; Factory : IGenericDataNodeFactory; Link : TDataNodeLink; Begin Inherited; Symbol := Params[0]; Factory := TGenericDataNodeFactory.FindFactory('TickVolatility').Duplicate; Factory.SetValue(SymbolNameKey, Params[0]); Factory.Find(ReadVolatilityData, VolatilityData, Link); AddAutoLink(Link); DoInCorrectThread(ReadVolatilityData) End; //////////////////////////////////////////////////////////////////////// // Initialization //////////////////////////////////////////////////////////////////////// Initialization TGenericDataNodeFactory.StoreFactory('TTrailingStopDownPercent', TGenericDataNodeFactory.CreateWithArgs(TTrailingStopPercent, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('TTrailingStopUpPercent', TGenericDataNodeFactory.CreateWithArgs(TTrailingStopPercent, StandardSymbolPlaceHolder, False)); TGenericDataNodeFactory.StoreFactory('TTrailingStopDownSigma', TGenericDataNodeFactory.CreateWithArgs(TTrailingStopSigma, StandardSymbolPlaceHolder, True)); TGenericDataNodeFactory.StoreFactory('TTrailingStopUpSigma', TGenericDataNodeFactory.CreateWithArgs(TTrailingStopSigma, StandardSymbolPlaceHolder, False)); End.