Fibonacci levels
Fibonacci levels
A convention-driven way to build Fibonacci tools: expose one level-set property and get user-editable ratios, colours and visibility for free, with ready-made default sets to start from.
ADVANCED What it is
Fibonacci tools follow a convention rather than a special base class. You expose a single
FibLevelSet Levels property (an ObservableCollection<FibLevel>),
tag it with [DrawingToolProperty], and the editor picks it up reflectively. That one property
gives the user per-level control over ratio, colour and visibility with no extra wiring.
The types live in SabrTrader.Pipeline.Drawing.Tools and are deliberately small. A
FibLevel is a ratio, an optional per-level colour (where null means "inherit the
tool's stroke colour"), and a visibility flag. A FibLevelSet is an observable collection of
them, so the properties dialog's list editor binds to it directly.
FibLevels.csnamespace SabrTrader.Pipeline.Drawing.Tools;
public sealed class FibLevel
{
public double Ratio { get; set; } // applied to the tool's anchor range
public ChartColor? Color { get; set; } // null = inherit the tool's Color
public bool IsVisible { get; set; } = true;
public FibLevel() { }
public FibLevel(double ratio, ChartColor? color = null, bool visible = true);
public FibLevel Clone();
}
public sealed class FibLevelSet : ObservableCollection<FibLevel>
{
public FibLevelSet();
public FibLevelSet(IEnumerable<FibLevel> items);
public FibLevelSet Clone(); // deep copy
public void ReplaceWith(IEnumerable<FibLevel> other);
public ReadOnlySpan<FibLevel> Snapshot { get; } // render-thread view, see below
}
Ready-made level sets
You rarely build a level set by hand. The static FibLevels helper ships the standard ratio
ladders and palettes, and factory methods that return a populated FibLevelSet. Some pair each
ratio with a TradingView-style colour; others leave the colour null so every level inherits
the tool's stroke colour.
FibLevels.cs// Ratio ladders (raw arrays):
FibLevels.StandardRetracements; // 0, .236, .382, .5, .618, .786, 1
FibLevels.StandardExtensions; // 1.272, 1.414, 1.618, 2, 2.618, 3.618, 4.236
FibLevels.StandardRetracementsAndExtensions;
FibLevels.StandardSpeedRatios; // .382, .5, .618
FibLevels.StandardTimeZones; // 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
FibLevels.StandardGannBoxRatios; // .25, .382, .5, .618, .75 - the Gann box inner grid
FibLevels.GannAngles; // (Price, Time) slope pairs for a Gann fan
// Factory methods returning a populated FibLevelSet:
FibLevels.DefaultRetracementSet(); // ladder + per-level colours
FibLevels.DefaultExtensionSet();
FibLevels.DefaultRetracementSetInheritColor(); // every level inherits the tool's Color
FibLevels.DefaultSpeedRatioSetInheritColor();
FibLevels.DefaultCircleRatioSetInheritColor();
FibLevels.DefaultGannBoxSetInheritColor(); // Gann box grid - use one set for price, one for time
FibLevelSets, each with its own ratios, per-level visibility and colour, so a trader can show
a price grid without a time grid (or give each a different ladder). Seed both from
DefaultGannBoxSetInheritColor(): the box outline already marks 0 and 1, so
StandardGannBoxRatios carries only the inner grid.
Rendering the levels
Snapshot on the render path. A FibLevelSet is an
ObservableCollection the properties editor mutates on the UI thread, so iterating it while a
trader edits a level can throw. set.Snapshot hands back the levels as of the last structural
change as a ReadOnlySpan<FibLevel>, safe from any thread and allocation-free to
walk. Enumerate the collection itself only in editor code.
In OnRender you derive each level's price from the two anchors (the ratio interpolates
between anchor 0's price and anchor 1's price), convert it to a pixel Y with
PriceToPixelY, and draw a horizontal line out to the plot area's right edge. Each level uses
its own colour, falling back to the tool's Color when none is set.
AcmeFibRetracement.csusing System;
using SabrTrader.Pipeline.Drawing;
using SabrTrader.Pipeline.Drawing.Tools;
using SabrTrader.Pipeline.Plots;
[DrawingToolMetadata("Acme Fib Retracement", Category = "Fibonacci", AnchorCount = 2)]
public sealed class AcmeFibRetracement : DrawingToolBase
{
public AcmeFibRetracement() : base(anchorCount: 2) { }
[DrawingToolProperty(DisplayName = "Levels", Category = "Levels", Order = 10,
Description = "Configurable ratios with per-level visibility and colour.")]
public FibLevelSet Levels { get; set; } = FibLevels.DefaultRetracementSet();
public override void OnRender(IDrawingToolRenderContext ctx)
{
double p0 = Anchors[0].Price, p1 = Anchors[1].Price;
double xL = Math.Min(ctx.AnchorToPixel(Anchors[0]).X, ctx.AnchorToPixel(Anchors[1]).X);
double xR = ctx.PlotArea.Right;
foreach (var lvl in Levels.Snapshot) // never the live collection here
{
if (!lvl.IsVisible) continue;
double price = p0 + lvl.Ratio * (p1 - p0);
double y = ctx.Viewport.PriceToPixelY(price);
ChartColor stroke = lvl.Color ?? Color; // per-level, else tool colour
DrawLine(ctx, xL, y, xR, y, stroke, LineWidthPx); // keeps the tool's LineStyle
ctx.Renderer.DrawText(xL + 4, y - 4, $"{lvl.Ratio:0.000} ({price:0.##})", stroke, 11);
}
if (ctx.IsSelected)
{
ctx.DrawHandle(ctx.AnchorToPixel(Anchors[0]).X, ctx.AnchorToPixel(Anchors[0]).Y);
ctx.DrawHandle(ctx.AnchorToPixel(Anchors[1]).X, ctx.AnchorToPixel(Anchors[1]).Y);
}
}
public override bool HitTest(double px, double py, IDrawingToolRenderContext ctx, double tol = 6)
{
double p0 = Anchors[0].Price, p1 = Anchors[1].Price;
double xL = Math.Min(ctx.AnchorToPixel(Anchors[0]).X, ctx.AnchorToPixel(Anchors[1]).X);
double xR = ctx.PlotArea.Right;
foreach (var lvl in Levels.Snapshot)
{
if (!lvl.IsVisible) continue;
double price = p0 + lvl.Ratio * (p1 - p0);
double y = ctx.Viewport.PriceToPixelY(price);
if (px >= xL && px <= xR && DrawingGeometry.DistanceToHorizontalLine(py, y) <= tol)
return true;
}
return false;
}
}
FibLevel and FibLevelSet
offer a deep Clone(). FibLevelSet.ReplaceWith swaps in clones of the given levels
in one step and raises a single Reset change notification, so a painting thread never sees the
set half-empty and a bound editor rebuilds once. Editing the live set while you iterate it confuses the bound
editor: clone, change the copy, then ReplaceWith.