Chart X grid, standard timeframes

This commit is contained in:
Michael Schimmel
2025-07-13 21:49:42 +02:00
parent 840904e42d
commit 4d67d9acbe
8 changed files with 619 additions and 165 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<ProjectVersion>20.3</ProjectVersion> <ProjectVersion>20.3</ProjectVersion>
<FrameworkType>FMX</FrameworkType> <FrameworkType>FMX</FrameworkType>
<Base>True</Base> <Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config> <Config Condition="'$(Config)'==''">Release</Config>
<Platform Condition="'$(Platform)'==''">Win64</Platform> <Platform Condition="'$(Platform)'==''">Win64</Platform>
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName> <ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
<TargetedPlatforms>3</TargetedPlatforms> <TargetedPlatforms>3</TargetedPlatforms>
+48 -9
View File
@@ -13,8 +13,6 @@ uses
Myc.Trade.DataArray; Myc.Trade.DataArray;
type type
TTimeframe = (M1, M5, H1, D);
TMycGenericConverter<S, T> = class(TMycConverter<S, T>) TMycGenericConverter<S, T> = class(TMycConverter<S, T>)
type type
TConvertFunc = reference to function(const Value: S): T; TConvertFunc = reference to function(const Value: S): T;
@@ -54,11 +52,11 @@ type
TGenericIndicator<S, T> = class(TIndicator<S, T>) TGenericIndicator<S, T> = class(TIndicator<S, T>)
private private
FFunc: TFunc<S, T>; FFunc: TIndicatorFunc<S, T>;
protected protected
function Calculate(const Value: S): T; override; final; function Calculate(const Value: S): T; override; final;
public public
constructor Create(const AFunc: TFunc<S, T>); constructor Create(const AFunc: TIndicatorFunc<S, T>);
end; end;
implementation implementation
@@ -76,14 +74,49 @@ begin
end; end;
function TTicksToTimeframe.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function TTicksToTimeframe.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
var
baseTime: TDateTime;
begin begin
// Align the time grid to UTC 0:00 using functions from System.DateUtils // Align the time grid to UTC 0:00 using functions from System.DateUtils
baseTime := RecodeMilliSecond(TimeStamp, 0);
case Timeframe of case Timeframe of
M1: Result := RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0); S: Result := baseTime;
M5: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), MinuteOf(TimeStamp) - MinuteOf(TimeStamp) mod 5); S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5));
H1: Result := RecodeMinute(RecodeSecond(RecodeMilliSecond(TimeStamp, 0), 0), 0); S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15));
S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30));
M: Result := RecodeSecond(baseTime, 0);
M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2));
M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3));
M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5));
M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10));
M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15));
M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30));
H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0);
H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2));
H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3));
H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4));
H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8));
H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12));
D: Result := StartOfTheDay(TimeStamp); D: Result := StartOfTheDay(TimeStamp);
// D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch.
D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2);
D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3);
W: Result := TimeStamp.StartOfTheWeek;
MN: Result := TimeStamp.StartOfTheMonth;
// Quarter alignment
MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1);
// Half-year alignment
MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1);
Y: Result := TimeStamp.StartOfTheYear;
else else
// Fallback for any undefined timeframe
Result := 0; Result := 0;
end; end;
end; end;
@@ -122,7 +155,9 @@ begin
begin begin
// A new bar starts, so the previous one is now complete. // A new bar starts, so the previous one is now complete.
if (lastBarTime > 0) then if (lastBarTime > 0) then
begin
states.Add(Broadcast(FCurrentBar)); states.Add(Broadcast(FCurrentBar));
end;
// Start a new bar, Volume is 1 because this is the first tick. // Start a new bar, Volume is 1 because this is the first tick.
currentBar := TOhlcItem.Create(midPrice, midPrice, midPrice, midPrice, 1); currentBar := TOhlcItem.Create(midPrice, midPrice, midPrice, midPrice, 1);
@@ -161,12 +196,16 @@ begin
Result := Broadcast(FFunc(Value)); Result := Broadcast(FFunc(Value));
end; end;
{ TIndicator<S,T> }
function TIndicator<S, T>.ProcessData(const Value: S): TState; function TIndicator<S, T>.ProcessData(const Value: S): TState;
begin begin
Result := FQueue.Enqueue(function: TState begin Result := Broadcast(Calculate(Value)); end); Result := Broadcast(Calculate(Value));
end; end;
constructor TGenericIndicator<S, T>.Create(const AFunc: TFunc<S, T>); { TGenericIndicator<S,T> }
constructor TGenericIndicator<S, T>.Create(const AFunc: TIndicatorFunc<S, T>);
begin begin
inherited Create; inherited Create;
FFunc := AFunc; FFunc := AFunc;
+39 -26
View File
@@ -165,17 +165,25 @@ begin
///// /////
var OhlcPoint: IMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>> := TTicksToTimeframe.Create(M1); var timeframe := TTimeframe.S15;
var OhlcPoint: IMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>> := TTicksToTimeframe.Create(timeframe);
var Timestamps: IMycConverter<TDataPoint<TOhlcItem>, TDateTime> := var Timestamps: IMycConverter<TDataPoint<TOhlcItem>, TDateTime> :=
TMycGenericConverter<TDataPoint<TOhlcItem>, TDateTime> TMycGenericConverter<TDataPoint<TOhlcItem>, TDateTime>
.Create(function(const Ohlc: TDataPoint<TOhlcItem>): TDateTime begin Result := Ohlc.Time; end); .Create(function(const Ohlc: TDataPoint<TOhlcItem>): TDateTime begin Result := Ohlc.Time; end);
OhlcPoint.Sender.Link(TimeStamps);
chart.SetXAxisSeries(timeframe, Timestamps.Sender);
var Panel := chart.Panels[0];
var Ohlc: IMycConverter<TDataPoint<TOhlcItem>, TOhlcItem> := var Ohlc: IMycConverter<TDataPoint<TOhlcItem>, TOhlcItem> :=
TMycGenericConverter<TDataPoint<TOhlcItem>, TOhlcItem> TMycGenericConverter<TDataPoint<TOhlcItem>, TOhlcItem>
.Create(function(const Ohlc: TDataPoint<TOhlcItem>): TOhlcItem begin Result := Ohlc.Data; end); .Create(function(const Ohlc: TDataPoint<TOhlcItem>): TOhlcItem begin Result := Ohlc.Data; end);
OhlcPoint.Sender.Link(Ohlc); OhlcPoint.Sender.Link(Ohlc);
Panel.AddOhlcSeries(Ohlc.Sender);
var Closes: IMycConverter<TOhlcItem, Double> := var Closes: IMycConverter<TOhlcItem, Double> :=
TMycGenericConverter<TOhlcItem, Double>.Create(function(const Ohlc: TOhlcItem): Double begin Result := Ohlc.Close; end); TMycGenericConverter<TOhlcItem, Double>.Create(function(const Ohlc: TOhlcItem): Double begin Result := Ohlc.Close; end);
@@ -184,17 +192,17 @@ begin
var Hull: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateHMA(150)); var Hull: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateHMA(150));
Closes.Sender.Link(Hull); Closes.Sender.Link(Hull);
chart.Panels[0].AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); Panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue);
// Add SMA (Simple Moving Average) // Add SMA (Simple Moving Average)
var Sma: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateSMA(50)); var Sma: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateSMA(50));
Closes.Sender.Link(Sma); Closes.Sender.Link(Sma);
chart.Panels[0].AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); Panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow);
// Add EMA (Exponential Moving Average) // Add EMA (Exponential Moving Average)
var Ema: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateEMA(21)); var Ema: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateEMA(21));
Closes.Sender.Link(Ema); Closes.Sender.Link(Ema);
chart.Panels[0].AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); Panel.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua);
// Add Bollinger Bands (20, 2.0) // Add Bollinger Bands (20, 2.0)
var Boli: IMycConverter<Double, TBollingerBandsResult> := var Boli: IMycConverter<Double, TBollingerBandsResult> :=
@@ -205,59 +213,64 @@ begin
TMycGenericConverter<TBollingerBandsResult, Double> TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end); .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end);
Boli.Sender.Link(BoliUpper); Boli.Sender.Link(BoliUpper);
chart.Panels[0].AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray); Panel.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray);
var BoliMiddle: IMycConverter<TBollingerBandsResult, Double> := var BoliMiddle: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double> TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end); .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end);
Boli.Sender.Link(BoliMiddle); Boli.Sender.Link(BoliMiddle);
chart.Panels[0].AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0); Panel.AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0);
var BoliLower: IMycConverter<TBollingerBandsResult, Double> := var BoliLower: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double> TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end); .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end);
Boli.Sender.Link(BoliLower); Boli.Sender.Link(BoliLower);
chart.Panels[0].AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray); Panel.AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray);
chart.AddPanel; Panel := chart.AddPanel;
// Add RSI (Relative Strength Index) // Add RSI (Relative Strength Index)
var Rsi: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateRSI(14)); var Rsi: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateRSI(14));
Closes.Sender.Link(Rsi); Closes.Sender.Link(Rsi);
chart.Panels[1].AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); Panel.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia);
{
// Add MACD (12, 26, 9) // Add MACD (12, 26, 9)
var Macd: IMycConverter<Double, TMacdResult> := TGenericIndicator<Double, TMacdResult>.Create(TIndicators.CreateMACD(12, 26, 9)); var Macd: IMycConverter<Double, TMacdResult> := TGenericIndicator<Double, TMacdResult>.Create(TIndicators.CreateMACD(12, 26, 9));
Closes.Sender.Link(Macd); Closes.Sender.Link(Macd);
var MacdLine: IMycConverter<TMacdResult, Double> := TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.MacdLine; end); Panel := chart.AddPanel;
var MacdLine: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.MacdLine; end);
Macd.Sender.Link(MacdLine); Macd.Sender.Link(MacdLine);
chart.AddDoubleSeries(MacdLine.Sender, TAlphaColors.Orange); Panel.AddDoubleSeries(MacdLine.Sender, TAlphaColors.Orange);
var MacdSignal: IMycConverter<TMacdResult, Double> := TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.SignalLine; end); var MacdSignal: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.SignalLine; end);
Macd.Sender.Link(MacdSignal); Macd.Sender.Link(MacdSignal);
chart.AddDoubleSeries(MacdSignal.Sender, TAlphaColors.Dodgerblue); Panel.AddDoubleSeries(MacdSignal.Sender, TAlphaColors.Dodgerblue);
var MacdHist: IMycConverter<TMacdResult, Double> := TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.Histogram; end); var MacdHist: IMycConverter<TMacdResult, Double> :=
TMycGenericConverter<TMacdResult, Double>.Create(function(const Item: TMacdResult): Double begin Result := Item.Histogram; end);
Macd.Sender.Link(MacdHist); Macd.Sender.Link(MacdHist);
chart.AddDoubleSeries(MacdHist.Sender, TAlphaColors.Lightgreen, 1.0); Panel.AddDoubleSeries(MacdHist.Sender, TAlphaColors.Lightgreen, 1.0);
Panel := chart.AddPanel;
// Add Stochastic Oscillator (14, 3) - This needs OHLC data, not just Close prices. // Add Stochastic Oscillator (14, 3) - This needs OHLC data, not just Close prices.
var Stoch: IMycConverter<TOhlcItem, TStochasticResult> := TGenericIndicator<TOhlcItem, TStochasticResult>.Create(TIndicators.CreateStochastic(14, 3)); var Stoch: IMycConverter<TOhlcItem, TStochasticResult> :=
TGenericIndicator<TOhlcItem, TStochasticResult>.Create(TIndicators.CreateStochastic(14, 3));
Ohlc.Sender.Link(Stoch); Ohlc.Sender.Link(Stoch);
var StochK: IMycConverter<TStochasticResult, Double> := TMycGenericConverter<TStochasticResult, Double>.Create(function(const Item: TStochasticResult): Double begin Result := Item.K; end); var StochK: IMycConverter<TStochasticResult, Double> :=
TMycGenericConverter<TStochasticResult, Double>.Create(function(const Item: TStochasticResult): Double begin Result := Item.K; end);
Stoch.Sender.Link(StochK); Stoch.Sender.Link(StochK);
chart.AddDoubleSeries(StochK.Sender, TAlphaColors.Green); Panel.AddDoubleSeries(StochK.Sender, TAlphaColors.Green);
var StochD: IMycConverter<TStochasticResult, Double> := TMycGenericConverter<TStochasticResult, Double>.Create(function(const Item: TStochasticResult): Double begin Result := Item.D; end); var StochD: IMycConverter<TStochasticResult, Double> :=
TMycGenericConverter<TStochasticResult, Double>.Create(function(const Item: TStochasticResult): Double begin Result := Item.D; end);
Stoch.Sender.Link(StochD); Stoch.Sender.Link(StochD);
chart.AddDoubleSeries(StochD.Sender, TAlphaColors.Red); Panel.AddDoubleSeries(StochD.Sender, TAlphaColors.Red);
}
OhlcPoint.Sender.Link(TimeStamps);
chart.SetXAxisSeries<TDateTime>(Timestamps.Sender);
chart.Panels[0].AddOhlcSeries(Ohlc.Sender);
var done := ExecuteStrategy(Symbol, OhlcPoint); var done := ExecuteStrategy(Symbol, OhlcPoint);
+48 -2
View File
@@ -130,11 +130,56 @@ end;
procedure TMycNotifyList<T>.Notify(const Func: TNotifyProc); procedure TMycNotifyList<T>.Notify(const Func: TNotifyProc);
var var
Item: PItem; Item: PItem;
Last: PItem;
tmp: PItem;
begin begin
Assert(IsLocked); Assert(IsLocked);
var rev: PItem := nil;
var tmp: PItem := nil;
while (FList <> nil) and Assigned(FList.Receiver) do
begin
Item := FList;
FList := Item.Next;
if Func(Item.Receiver) then
begin
Item.Next := rev;
rev := Item;
end
else
begin
// release the receiver
Item.Receiver := nil;
Item.Next := tmp;
tmp := Item;
end;
end;
while tmp <> nil do
begin
Item := tmp;
tmp := Item.Next;
Item.Prev := nil;
Item.Next := FList;
FList := Item;
if Item.Next <> nil then
Item.Next.Prev := Item;
end;
while rev <> nil do
begin
Item := rev;
rev := Item.Next;
Item.Prev := nil;
Item.Next := FList;
FList := Item;
if Item.Next <> nil then
Item.Next.Prev := Item;
end;
{
tmp := nil; tmp := nil;
Last := nil; Last := nil;
Item := FList; Item := FList;
@@ -189,6 +234,7 @@ begin
if Item.Next <> nil then if Item.Next <> nil then
Item.Next.Prev := Item; Item.Next.Prev := Item;
end; end;
}
end; end;
procedure TMycNotifyList<T>.Release; procedure TMycNotifyList<T>.Release;
+80 -14
View File
@@ -1,4 +1,4 @@
unit Myc.FMX.Chart.Series; unit Myc.Fmx.Chart.Series;
interface interface
@@ -58,14 +58,14 @@ type
end; end;
{ TChartCustomLayer } { TChartCustomLayer }
TChartCustomLayer<T> = class(TMycChart.TLayer) TChartCustomLayer<T> = class abstract(TMycChart.TDataLayer)
private private
FSeries: TChartSeriesProcessor<T>; FSeries: TChartSeriesProcessor<T>;
protected protected
function GetSeries: TMycChart.TSeries; override; function GetSeries: TMycChart.TSeries; override;
function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract;
procedure Update; override; procedure Update; override;
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); override; procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); override; abstract;
public public
constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider<T>); constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider<T>);
destructor Destroy; override; destructor Destroy; override;
@@ -104,6 +104,30 @@ type
); );
end; end;
{ TChartXAxisLayer }
TChartXAxisLayer<T> = class(TMycChart.TXAxisLayer)
private
FSeries: TChartSeriesProcessor<T>;
protected
function GetSeries: TMycChart.TSeries; override;
procedure Update; override;
function GetCaption(Idx: Int64): String; override; abstract;
public
constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>);
destructor Destroy; override;
end;
{ TChartXAxisTimestampLayer }
TChartXAxisTimestampLayer = class(TChartXAxisLayer<TDateTime>)
private
FTimeframe: TTimeframe;
protected
// Provide a formatted timestamp string for a given data index.
function GetCaption(Idx: Int64): String; override; final;
public
constructor Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: IMycDataProvider<TDateTime>);
end;
implementation implementation
uses uses
@@ -320,19 +344,61 @@ begin
Result := FSeries; Result := FSeries;
end; end;
function TChartCustomLayer<T>.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean;
begin
Result := false;
end;
procedure TChartCustomLayer<T>.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>);
begin
// nope
end;
procedure TChartCustomLayer<T>.Update; procedure TChartCustomLayer<T>.Update;
begin begin
FSeries.Update; FSeries.Update;
end; end;
{ TChartXAxisLayer }
constructor TChartXAxisLayer<T>.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>);
begin
inherited Create(AOwner);
FSeries := TChartSeriesProcessor<T>.Create(ADataProvider, AOwner.Lookback.AsMutable);
end;
destructor TChartXAxisLayer<T>.Destroy;
begin
FSeries.Free;
inherited;
end;
function TChartXAxisLayer<T>.GetSeries: TMycChart.TSeries;
begin
Result := FSeries;
end;
procedure TChartXAxisLayer<T>.Update;
begin
FSeries.Update;
end;
constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: IMycDataProvider<TDateTime>);
begin
inherited Create(AOwner, ADataProvider);
FTimeframe := ATimeframe;
end;
function TChartXAxisTimestampLayer.GetCaption(Idx: Int64): String;
var
dt: TDateTime;
formatStr: string;
begin
Result := '';
if (Idx >= 0) and (Idx < FSeries.Count) then
begin
dt := FSeries.Data[Idx];
// Choose format based on the series timeframe.
if FTimeframe >= D then // Daily, Weekly, Monthly, Yearly
formatStr := 'dd.mm.yyyy'
else if FTimeframe >= M then // Hourly + Minutes
formatStr := 'dd.mm.yyyy hh:nn'
else // Seconds
formatStr := 'dd.mm.yyyy hh:nn:ss';
Result := FormatDateTime(formatStr, dt);
end;
end;
end. end.
+378 -92
View File
@@ -38,29 +38,66 @@ type
// Abstract base for a drawable layer in a panel. // Abstract base for a drawable layer in a panel.
TLayer = class abstract(TObject) TLayer = class abstract(TObject)
private
FParent: TPanel;
function GetOwner: TMycChart;
protected protected
function GetOwner: TMycChart; virtual; abstract;
function GetSeries: TSeries; virtual; abstract; function GetSeries: TSeries; virtual; abstract;
procedure Update; virtual; abstract; procedure Update; virtual; abstract;
function GetValueRange(First, Last: Int64; out Min, Max: Double): Boolean; virtual; abstract;
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); virtual; abstract;
public public
constructor Create(AOwner: TPanel);
procedure Repaint; procedure Repaint;
property Owner: TMycChart read GetOwner; property Owner: TMycChart read GetOwner;
property Series: TSeries read GetSeries; property Series: TSeries read GetSeries;
end; end;
TDataLayer = class abstract(TLayer)
private
FParent: TPanel;
protected
function GetOwner: TMycChart; override; final;
function GetValueRange(First, Last: Int64; out Min, Max: Double): Boolean; virtual; abstract;
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); virtual; abstract;
public
constructor Create(AParent: TPanel);
end;
TAxisLayer = class(TLayer)
private
FOwner: TMycChart;
protected
function GetOwner: TMycChart; override; final;
// Paint crosshair and other axis related overlays
procedure Paint(
const Canvas: TCanvas;
const Viewport: TRectF;
ViewStartIndex, ViewCount: Int64;
const MousePos: TPointF
); virtual;
public
constructor Create(AOwner: TMycChart);
end;
TXAxisLayer = class(TAxisLayer)
protected
// Paint crosshair and time caption
procedure Paint(
const Canvas: TCanvas;
const Viewport: TRectF;
ViewStartIndex, ViewCount: Int64;
const MousePos: TPointF
); override;
// This function delivers the text for the caption.
function GetCaption(Idx: Int64): String; virtual; abstract;
end;
// A panel is a rectangular area in the chart with its own Y-axis, which can contain multiple layers. // A panel is a rectangular area in the chart with its own Y-axis, which can contain multiple layers.
TPanel = class(TObject) TPanel = class(TObject)
private private
FOwner: TMycChart; FOwner: TMycChart;
FSeriesList: TObjectList<TMycChart.TLayer>; FSeriesList: TObjectList<TMycChart.TDataLayer>;
FWeight: Single;
procedure SetWeight(const Value: Single);
protected protected
procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64); procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64);
procedure UpdateAndCheckChanges(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); procedure Update(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean);
public public
constructor Create(AOwner: TMycChart); constructor Create(AOwner: TMycChart);
@@ -70,35 +107,42 @@ type
const DataProvider: IMycDataProvider<TOhlcItem>; const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor: TAlphaColor = TAlphaColors.Green; const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red const ADownColor: TAlphaColor = TAlphaColors.Red
): TMycChart.TLayer; ): TMycChart.TDataLayer;
function AddDoubleSeries( function AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>; const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5 const ALineWidth: Single = 1.5
): TMycChart.TLayer; ): TMycChart.TDataLayer;
property Owner: TMycChart read FOwner; property Owner: TMycChart read FOwner;
property Weight: Single read FWeight write SetWeight;
end; end;
private private
FPanelList: TObjectList<TPanel>; FPanelList: TObjectList<TPanel>;
FXAxisSeries: TMycChart.TLayer; FXAxisSeries: TMycChart.TXAxisLayer;
FLookback: TWriteable<Int64>; FLookback: TWriteable<Int64>;
FIdleSubscrId: TMessageSubscriptionId; FIdleSubscrId: TMessageSubscriptionId;
FViewStartIndex: Int64; FViewStartIndex: Int64;
FViewCount: Int64; FViewCount: Int64;
FIsDragging: Boolean; FIsDragging: Boolean;
FDragStartPoint: TPointF; FDragStartPoint: TPointF;
FMousePos: TPointF;
FIsMouseInControl: Boolean;
FJumpButtonRect: TRectF; FJumpButtonRect: TRectF;
FJumpButtonHot: Boolean; FJumpButtonHot: Boolean;
FJumpButtonPressed: Boolean; FJumpButtonPressed: Boolean;
FNeedRepaint: TFlag; FNeedRepaint: TFlag;
FIsResizing: Boolean;
FResizePanelIndex: Integer;
FHotResizePanelIndex: Integer;
function GetPanel(Index: Integer): TPanel; function GetPanel(Index: Integer): TPanel;
function GetPanelCount: Integer; function GetPanelCount: Integer;
protected protected
procedure Paint; override; procedure Paint; override;
procedure DoIdle; procedure DoIdle;
procedure DoMouseLeave; override;
procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override; procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
procedure MouseMove(Shift: TShiftState; X, Y: Single); override; procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
@@ -110,13 +154,15 @@ type
// Adds a new, empty panel to the bottom of the chart. // Adds a new, empty panel to the bottom of the chart.
function AddPanel: TPanel; function AddPanel: TPanel;
// Sets the master series that defines the time scale (X-axis). This series is not drawn. // Sets the master series that defines the time scale (X-axis).
function SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TMycChart.TLayer; function SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: IMycDataProvider<TDateTime>): TMycChart.TXAxisLayer;
property Lookback: TWriteable<Int64> read FLookback write FLookback; property Lookback: TWriteable<Int64> read FLookback write FLookback;
property NeedRepaint: TFlag read FNeedRepaint; property NeedRepaint: TFlag read FNeedRepaint;
property PanelCount: Integer read GetPanelCount; property PanelCount: Integer read GetPanelCount;
property Panels[Index: Integer]: TPanel read GetPanel; default; property Panels[Index: Integer]: TPanel read GetPanel; default;
// The rectangle occupied by the jump-to-latest button, used for hit testing.
property JumpButtonRect: TRectF read FJumpButtonRect;
end; end;
implementation implementation
@@ -125,6 +171,11 @@ uses
System.Math, System.Math,
Myc.FMX.Chart.Series; Myc.FMX.Chart.Series;
const
RESIZE_HOT_ZONE = 4;
MIN_PANEL_HEIGHT = 30;
MIN_PANEL_WEIGHT = 0.05;
{ TMycChart } { TMycChart }
constructor TMycChart.Create(AOwner: TComponent); constructor TMycChart.Create(AOwner: TComponent);
@@ -136,7 +187,14 @@ begin
FLookback := TWriteable<Int64>.CreateWriteable(1000); FLookback := TWriteable<Int64>.CreateWriteable(1000);
FViewStartIndex := 0; FViewStartIndex := 0;
FViewCount := 100; FViewCount := 100;
FIsDragging := false; FIsDragging := false;
FIsResizing := false;
FResizePanelIndex := -1;
FHotResizePanelIndex := -1;
FIsMouseInControl := false;
FNeedRepaint := TFlag.CreateFlag();
FIdleSubscrId := FIdleSubscrId :=
TMessageManager TMessageManager
@@ -144,7 +202,7 @@ begin
.SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end); .SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end);
// Create the default panel. // Create the default panel.
AddPanel; AddPanel.Weight := 1.0;
end; end;
destructor TMycChart.Destroy; destructor TMycChart.Destroy;
@@ -173,16 +231,6 @@ begin
Result := FPanelList.Count; Result := FPanelList.Count;
end; end;
function TMycChart.SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TMycChart.TLayer;
begin
FXAxisSeries.Free;
// Note: TChartCustomLayer needs its owner to be a TPanel. We use the first panel.
var counter := TChartSeriesCounter<T>.Create;
DataProvider.Link(counter);
FXAxisSeries := TChartCustomLayer<Int64>.Create(Self.Panels[0], counter.Sender);
Result := FXAxisSeries;
end;
procedure TMycChart.DoIdle; procedure TMycChart.DoIdle;
var var
prevTotalCount, newTotalCount: Int64; prevTotalCount, newTotalCount: Int64;
@@ -202,11 +250,12 @@ begin
for var panel in FPanelList do for var panel in FPanelList do
begin begin
var panelChanged, panelRepaint: Boolean; var panelChanged, panelRepaint: Boolean;
panel.UpdateAndCheckChanges(prevTotalCount, FViewStartIndex, panelChanged, panelRepaint); panel.Update(prevTotalCount, FViewStartIndex, panelChanged, panelRepaint);
seriesChanged := seriesChanged or panelChanged; seriesChanged := seriesChanged or panelChanged;
seriesRepaint := seriesRepaint or panelRepaint; seriesRepaint := seriesRepaint or panelRepaint;
end; end;
// When we are not displaying live data, adjust start index accordingly
if (FViewStartIndex > 0) then if (FViewStartIndex > 0) then
begin begin
countDelta := newTotalCount - prevTotalCount; countDelta := newTotalCount - prevTotalCount;
@@ -224,6 +273,16 @@ begin
Repaint; Repaint;
end; end;
procedure TMycChart.DoMouseLeave;
begin
inherited;
if FIsMouseInControl then
begin
FIsMouseInControl := false;
Repaint;
end;
end;
procedure TMycChart.Paint; procedure TMycChart.Paint;
var var
rect, panelRect: TRectF; rect, panelRect: TRectF;
@@ -231,7 +290,7 @@ var
buttonColor: TAlphaColor; buttonColor: TAlphaColor;
path: TPathData; path: TPathData;
masterTotalCount: Int64; masterTotalCount: Int64;
panelHeight: Single; panelHeight, totalWeight, currentY: Single;
begin begin
inherited; inherited;
rect := Self.LocalRect; rect := Self.LocalRect;
@@ -244,12 +303,20 @@ begin
end; end;
masterTotalCount := FXAxisSeries.Series.TotalCount; masterTotalCount := FXAxisSeries.Series.TotalCount;
panelHeight := rect.Height / PanelCount;
// Calculate total weight for proportional panel height
totalWeight := 0;
for var panel in FPanelList do
totalWeight := totalWeight + panel.Weight;
if (totalWeight <= 0) then
totalWeight := PanelCount; // Fallback to equal distribution
currentY := rect.Top;
for var i := 0 to PanelCount - 1 do for var i := 0 to PanelCount - 1 do
begin begin
var panel := Panels[i]; var panel := Panels[i];
panelRect := TRectF.Create(rect.Left, rect.Top + (i * panelHeight), rect.Right, rect.Top + ((i + 1) * panelHeight)); panelHeight := (panel.Weight / totalWeight) * rect.Height;
panelRect := TRectF.Create(rect.Left, currentY, rect.Right, currentY + panelHeight);
// Draw panel content // Draw panel content
panel.Paint(Self.Canvas, panelRect, masterTotalCount, FViewStartIndex, FViewCount); panel.Paint(Self.Canvas, panelRect, masterTotalCount, FViewStartIndex, FViewCount);
@@ -261,8 +328,14 @@ begin
Canvas.Stroke.Thickness := 1; Canvas.Stroke.Thickness := 1;
Canvas.DrawLine(PointF(panelRect.Left, panelRect.Top), PointF(panelRect.Right, panelRect.Top), 1); Canvas.DrawLine(PointF(panelRect.Left, panelRect.Top), PointF(panelRect.Right, panelRect.Top), 1);
end; end;
currentY := currentY + panelHeight;
end; end;
// Draw crosshair if mouse is in control
if FIsMouseInControl and Assigned(FXAxisSeries) and not FIsDragging and not FIsResizing then
FXAxisSeries.Paint(Self.Canvas, rect, FViewStartIndex, FViewCount, FMousePos);
// --- Draw the "jump to latest" button (code unchanged) --- // --- Draw the "jump to latest" button (code unchanged) ---
isButtonVisible := (FViewStartIndex > 0); isButtonVisible := (FViewStartIndex > 0);
@@ -312,7 +385,17 @@ begin
begin begin
FJumpButtonPressed := true; FJumpButtonPressed := true;
Repaint; Repaint;
exit; // Prevent chart dragging exit;
end;
// Check for starting a resize
if (Button = TMouseButton.mbLeft) and (FHotResizePanelIndex >= 0) then
begin
FIsResizing := true;
FResizePanelIndex := FHotResizePanelIndex;
FDragStartPoint := TPointF.Create(X, Y);
Capture;
exit;
end; end;
inherited MouseDown(Button, Shift, X, Y); inherited MouseDown(Button, Shift, X, Y);
@@ -320,61 +403,111 @@ begin
begin begin
FIsDragging := true; FIsDragging := true;
FDragStartPoint := TPointF.Create(X, Y); FDragStartPoint := TPointF.Create(X, Y);
Capture;
end; end;
end; end;
procedure TMycChart.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TMycChart.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin begin
// Check if a button press was active
if FJumpButtonPressed then
begin
FJumpButtonPressed := false;
// If mouse is released over the button, trigger the action
if (FViewStartIndex > 0) and FJumpButtonRect.Contains(PointF(X, Y)) then
begin
FViewStartIndex := 0; // Jump to the latest candle
end;
Repaint;
exit;
end;
inherited MouseUp(Button, Shift, X, Y); inherited MouseUp(Button, Shift, X, Y);
if (Button = TMouseButton.mbLeft) then if (Button = TMouseButton.mbLeft) then
begin begin
FIsDragging := false; // Stop resizing
if FIsResizing then
begin
FIsResizing := false;
FResizePanelIndex := -1;
// Force cursor update on next move by resetting hot index
if (FHotResizePanelIndex <> -1) then
begin
FHotResizePanelIndex := -1;
Self.Cursor := crDefault;
end;
end;
// Stop panning
if FIsDragging then
begin
FIsDragging := false;
end;
// Stop button press
if FJumpButtonPressed then
begin
FJumpButtonPressed := false;
// If mouse is released over the button, trigger the action
if (FViewStartIndex > 0) and FJumpButtonRect.Contains(PointF(X, Y)) then
begin
FViewStartIndex := 0; // Jump to the latest candle
end;
Repaint;
end;
// Release mouse capture if it was held by this control
ReleaseCapture;
end; end;
end; end;
procedure TMycChart.MouseMove(Shift: TShiftState; X, Y: Single); procedure TMycChart.MouseMove(Shift: TShiftState; X, Y: Single);
var var
isHot: Boolean; dx, dy: Single;
dx: Single;
indexDelta: Int64; indexDelta: Int64;
maxIndex: Int64; maxIndex: Int64;
begin begin
// Update button hot state
if (FViewStartIndex > 0) then
begin
isHot := FJumpButtonRect.Contains(PointF(X, Y));
if (isHot <> FJumpButtonHot) then
begin
FJumpButtonHot := isHot;
Repaint;
end;
end
else
begin
// Ensure hot state is off when button is not visible
if FJumpButtonHot then
begin
FJumpButtonHot := false;
Repaint;
end;
end;
// Panning logic
inherited MouseMove(Shift, X, Y); inherited MouseMove(Shift, X, Y);
if not FIsMouseInControl then
FIsMouseInControl := true;
FMousePos := TPointF.Create(X, Y);
// --- Panel Resizing Logic ---
if FIsResizing then
begin
dy := Y - FDragStartPoint.Y;
var panel1 := Panels[FResizePanelIndex];
var panel2 := Panels[FResizePanelIndex + 1];
// Get total weight and height of the two panels being resized
var totalWeight := 0.0;
for var panel in FPanelList do
totalWeight := totalWeight + panel.Weight;
if (totalWeight <= 0) then
totalWeight := PanelCount;
var h1 := (panel1.Weight / totalWeight) * Self.Height;
var h2 := (panel2.Weight / totalWeight) * Self.Height;
var twoPanelWeight := panel1.Weight + panel2.Weight;
var newH1 := h1 + dy;
var newH2 := h2 - dy;
// Clamp to minimum height
if (newH1 < MIN_PANEL_HEIGHT) then
begin
newH2 := newH2 + (newH1 - MIN_PANEL_HEIGHT);
newH1 := MIN_PANEL_HEIGHT;
end;
if (newH2 < MIN_PANEL_HEIGHT) then
begin
newH1 := newH1 + (newH2 - MIN_PANEL_HEIGHT);
newH2 := MIN_PANEL_HEIGHT;
end;
// Recalculate weights based on new clamped heights
if (newH1 + newH2 > 0) then
begin
panel1.Weight := (newH1 / (newH1 + newH2)) * twoPanelWeight;
panel2.Weight := (newH2 / (newH1 + newH2)) * twoPanelWeight;
end;
FDragStartPoint.Y := Y; // Update start point for next move
Repaint;
exit; // Do not continue with other mouse move logic
end;
// --- Chart Panning Logic ---
if FIsDragging then if FIsDragging then
begin begin
dx := X - FDragStartPoint.X; dx := X - FDragStartPoint.X;
@@ -383,24 +516,16 @@ begin
exit; exit;
end; end;
// An X-axis series is required for panning
if not Assigned(FXAxisSeries) or (FViewCount <= 0) then if not Assigned(FXAxisSeries) or (FViewCount <= 0) then
begin
exit; exit;
end;
indexDelta := Round(dx / (Self.Width / FViewCount)); indexDelta := Round(dx / (Self.Width / FViewCount));
// To move content left (natural), view must shift to newer data (lower index).
// Drag left (dx < 0) -> FViewStartIndex must decrease.
// The formula for that is FViewStartIndex := FViewStartIndex + indexDelta; -> StartIndex + (negative) = decrease.
FViewStartIndex := FViewStartIndex + indexDelta; FViewStartIndex := FViewStartIndex + indexDelta;
// Clamp values // Clamp values
maxIndex := FXAxisSeries.Series.Count - FViewCount; maxIndex := FXAxisSeries.Series.Count - FViewCount;
if (maxIndex < 0) then if (maxIndex < 0) then
maxIndex := 0; maxIndex := 0;
if (FViewStartIndex < 0) then if (FViewStartIndex < 0) then
FViewStartIndex := 0; FViewStartIndex := 0;
if (FViewStartIndex > maxIndex) then if (FViewStartIndex > maxIndex) then
@@ -408,9 +533,43 @@ begin
FDragStartPoint.X := X; FDragStartPoint.X := X;
FDragStartPoint.Y := Y; FDragStartPoint.Y := Y;
Repaint; Repaint;
exit;
end; end;
// --- Separator Hit-Testing and Cursor Change ---
var currentHotIndex := -1;
if (PanelCount > 1) then
begin
var totalWeight := 0.0;
for var panel in FPanelList do
totalWeight := totalWeight + panel.Weight;
if (totalWeight <= 0) then
totalWeight := PanelCount;
var currentY: Single := 0;
for var i := 0 to PanelCount - 2 do
begin
currentY := currentY + (Panels[i].Weight / totalWeight) * Self.Height;
if (Abs(Y - currentY) < RESIZE_HOT_ZONE) then
begin
currentHotIndex := i;
break;
end;
end;
end;
if (currentHotIndex <> FHotResizePanelIndex) then
begin
FHotResizePanelIndex := currentHotIndex;
if (FHotResizePanelIndex <> -1) then
Self.Cursor := crVSplit
else
Self.Cursor := crDefault;
end;
// --- Jump Button Hot State Logic and final repaint for crosshair ---
Repaint;
end; end;
procedure TMycChart.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); procedure TMycChart.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
@@ -471,22 +630,17 @@ begin
Repaint; Repaint;
end; end;
{ TMycChart.TLayer } function TMycChart.SetXAxisSeries(Timeframe: TTimeframe; const DataProvider: IMycDataProvider<TDateTime>): TMycChart.TXAxisLayer;
constructor TMycChart.TLayer.Create(AOwner: TPanel);
begin begin
inherited Create; FXAxisSeries.Free;
FParent := AOwner;
end;
function TMycChart.TLayer.GetOwner: TMycChart; FXAxisSeries := TChartXAxisTimestampLayer.Create(Self, Timeframe, DataProvider);
begin Result := FXAxisSeries;
Result := FParent.Owner;
end; end;
procedure TMycChart.TLayer.Repaint; procedure TMycChart.TLayer.Repaint;
begin begin
FParent.Owner.Repaint; Owner.Repaint;
end; end;
{ TMycChart.TPanel } { TMycChart.TPanel }
@@ -495,7 +649,8 @@ constructor TMycChart.TPanel.Create(AOwner: TMycChart);
begin begin
inherited Create; inherited Create;
FOwner := AOwner; FOwner := AOwner;
FSeriesList := TObjectList<TMycChart.TLayer>.Create(true); FSeriesList := TObjectList<TMycChart.TDataLayer>.Create(true);
FWeight := 0.2;
end; end;
destructor TMycChart.TPanel.Destroy; destructor TMycChart.TPanel.Destroy;
@@ -506,9 +661,9 @@ end;
function TMycChart.TPanel.AddDoubleSeries( function TMycChart.TPanel.AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>; const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single const ALineWidth: Single = 1.5
): TMycChart.TLayer; ): TMycChart.TDataLayer;
begin begin
Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth); Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth);
FSeriesList.Add(Result); FSeriesList.Add(Result);
@@ -516,8 +671,9 @@ end;
function TMycChart.TPanel.AddOhlcSeries( function TMycChart.TPanel.AddOhlcSeries(
const DataProvider: IMycDataProvider<TOhlcItem>; const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor, ADownColor: TAlphaColor const AUpColor: TAlphaColor = TAlphaColors.Green;
): TMycChart.TLayer; const ADownColor: TAlphaColor = TAlphaColors.Red
): TMycChart.TDataLayer;
begin begin
Result := Result :=
TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor)); TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor));
@@ -603,7 +759,20 @@ begin
end; end;
end; end;
procedure TMycChart.TPanel.UpdateAndCheckChanges(MasterTotalCount, ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); procedure TMycChart.TPanel.SetWeight(const Value: Single);
begin
// Ensure weight does not become too small or negative
var clampedValue := Max(MIN_PANEL_WEIGHT, Value);
if FWeight <> clampedValue then
begin
FWeight := clampedValue;
// Do not call Repaint here, it will be handled by the mouse move logic
if Assigned(FOwner) and (not FOwner.FIsResizing) then
FOwner.Repaint;
end;
end;
procedure TMycChart.TPanel.Update(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean);
var var
seriesCount: Int64; seriesCount: Int64;
begin begin
@@ -623,4 +792,121 @@ begin
end; end;
end; end;
{ TMycChart.TDataLayer }
constructor TMycChart.TDataLayer.Create(AParent: TPanel);
begin
inherited Create;
FParent := AParent;
end;
function TMycChart.TDataLayer.GetOwner: TMycChart;
begin
Result := FParent.Owner;
end;
{ TMycChart.TAxisLayer }
constructor TMycChart.TAxisLayer.Create(AOwner: TMycChart);
begin
inherited Create;
FOwner := AOwner;
end;
function TMycChart.TAxisLayer.GetOwner: TMycChart;
begin
Result := FOwner;
end;
procedure TMycChart.TAxisLayer.Paint(
const Canvas: TCanvas;
const Viewport: TRectF;
ViewStartIndex, ViewCount: Int64;
const MousePos: TPointF
);
begin
// Do nothing in base class
end;
{ TMycChart.TXAxisLayer }
procedure TMycChart.TXAxisLayer.Paint(
const Canvas: TCanvas;
const Viewport: TRectF;
ViewStartIndex, ViewCount: Int64;
const MousePos: TPointF
);
var
idx: Int64;
caption: string;
captionRect: TRectF;
padding: Single;
indexFromMouse: Double;
candleX: Single;
textSize: TRectF;
begin
// Do not draw crosshair if mouse is over the jump button
if Owner.JumpButtonRect.Contains(MousePos) then
exit;
// Do not draw if no data or view is invalid
if (not Assigned(Series)) or (Series.Count = 0) or (ViewCount <= 1) then
exit;
// 1. Calculate the data index from the mouse X position
indexFromMouse := ViewStartIndex + (Viewport.Right - MousePos.X) * (ViewCount - 1) / Viewport.Width;
idx := Round(indexFromMouse);
// Check if the calculated index is valid for the series
if (idx < 0) or (idx >= Series.Count) then
exit;
// Snap X to the candle's center
candleX := Viewport.Right - ((idx - ViewStartIndex) * (ViewCount - 1)) / (ViewCount - 1) * (Viewport.Width / (ViewCount - 1));
// 2. Draw the vertical line at the snapped position
Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := TAlphaColors.Gray;
Canvas.Stroke.Thickness := 1;
Canvas.DrawLine(PointF(candleX, Viewport.Top), PointF(candleX, Viewport.Bottom), 1.0);
// 3. Get the caption for the index
caption := GetCaption(idx);
if caption.IsEmpty then
exit;
// 4. Draw the caption with a background
padding := 1;
Canvas.Font.Size := 9;
// Measure text size
textSize := TRectF.Create(0, 0, 10000, 10000);
Canvas.MeasureText(textSize, caption, False, [], TTextAlign.Leading);
// Position the caption box, centered on the snapped candle X
captionRect.Left := candleX - (textSize.Width / 2) - padding;
captionRect.Width := textSize.Width + (padding * 2);
captionRect.Top := Viewport.Bottom - textSize.Height - (padding * 2);
captionRect.Height := textSize.Height + (padding * 2);
// Adjust position to not go outside the viewport
if captionRect.Left < Viewport.Left then
captionRect.SetLocation(Viewport.Left, captionRect.Top);
if captionRect.Right > Viewport.Right then
captionRect.SetLocation(Viewport.Right - captionRect.Width, captionRect.Top);
// Draw the styled box
Canvas.Fill.Color := TAlphaColors.White;
Canvas.Fill.Kind := TBrushKind.Solid;
Canvas.Stroke.Kind := TBrushKind.Solid;
Canvas.Stroke.Color := TAlphaColors.Gray;
Canvas.Stroke.Thickness := 1;
Canvas.FillRect(captionRect, 2, 2, AllCorners, 1.0);
Canvas.DrawRect(captionRect, 2, 2, AllCorners, 1.0);
// Draw the text
Canvas.Fill.Color := TAlphaColors.Black;
Canvas.FillText(captionRect, caption, false, 1.0, [], TTextAlign.Center, TTextAlign.Center);
end;
end. end.
+21 -21
View File
@@ -36,19 +36,19 @@ type
class function CalculateWMA(const Series: TMycDataArray<Double>; const Period: Integer): Double; static; class function CalculateWMA(const Series: TMycDataArray<Double>; const Period: Integer): Double; static;
public public
// Simple Moving Average // Simple Moving Average
class function CreateSMA(Period: Integer): TFunc<Double, Double>; static; class function CreateSMA(Period: Integer): TIndicatorFunc<Double, Double>; static;
// Exponential Moving Average // Exponential Moving Average
class function CreateEMA(Period: Integer): TFunc<Double, Double>; static; class function CreateEMA(Period: Integer): TIndicatorFunc<Double, Double>; static;
// Hull Moving Average // Hull Moving Average
class function CreateHMA(Period: Integer): TFunc<Double, Double>; static; class function CreateHMA(Period: Integer): TIndicatorFunc<Double, Double>; static;
// Relative Strength Index // Relative Strength Index
class function CreateRSI(Period: Integer): TFunc<Double, Double>; static; class function CreateRSI(Period: Integer): TIndicatorFunc<Double, Double>; static;
// Moving Average Convergence Divergence // Moving Average Convergence Divergence
class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TFunc<Double, TMacdResult>; static; class function CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc<Double, TMacdResult>; static;
// Stochastic Oscillator // Stochastic Oscillator
class function CreateStochastic(KPeriod, DPeriod: Integer): TFunc<TOhlcItem, TStochasticResult>; static; class function CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc<TOhlcItem, TStochasticResult>; static;
// Bollinger Bands // Bollinger Bands
class function CreateBollingerBands(Period: Integer; Multiplier: Double): TFunc<Double, TBollingerBandsResult>; static; class function CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc<Double, TBollingerBandsResult>; static;
end; end;
implementation implementation
@@ -112,11 +112,11 @@ begin
Result := numerator / denominator; Result := numerator / denominator;
end; end;
class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TFunc<Double, TBollingerBandsResult>; class function TIndicators.CreateBollingerBands(Period: Integer; Multiplier: Double): TIndicatorFunc<Double, TBollingerBandsResult>;
begin begin
var sourceData := TMycDataArray<Double>.CreateEmpty; var sourceData := TMycDataArray<Double>.CreateEmpty;
Result := Result :=
function(Value: Double): TBollingerBandsResult function(const Value: Double): TBollingerBandsResult
var var
stdDev: Double; stdDev: Double;
begin begin
@@ -135,14 +135,14 @@ begin
end; end;
end; end;
class function TIndicators.CreateEMA(Period: Integer): TFunc<Double, Double>; class function TIndicators.CreateEMA(Period: Integer): TIndicatorFunc<Double, Double>;
begin begin
var lastEma: Double := Double.NaN; var lastEma: Double := Double.NaN;
var sourceData := TMycDataArray<Double>.CreateEmpty; var sourceData := TMycDataArray<Double>.CreateEmpty;
var multiplier := 2 / (Period + 1); var multiplier := 2 / (Period + 1);
Result := Result :=
function(Value: Double): Double function(const Value: Double): Double
begin begin
sourceData := sourceData.Add(Value, Period); sourceData := sourceData.Add(Value, Period);
@@ -166,7 +166,7 @@ begin
end; end;
end; end;
class function TIndicators.CreateHMA(Period: Integer): TFunc<Double, Double>; class function TIndicators.CreateHMA(Period: Integer): TIndicatorFunc<Double, Double>;
begin begin
var periodHalf := Period div 2; var periodHalf := Period div 2;
var periodSqrt := Round(Sqrt(Period)); var periodSqrt := Round(Sqrt(Period));
@@ -174,7 +174,7 @@ begin
var diffSeries := TMycDataArray<Double>.CreateEmpty; var diffSeries := TMycDataArray<Double>.CreateEmpty;
Result := Result :=
function(Value: Double): Double function(const Value: Double): Double
var var
price: Double; price: Double;
wmaHalf, wmaFull, diff: Double; wmaHalf, wmaFull, diff: Double;
@@ -208,14 +208,14 @@ begin
end; end;
end; end;
class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TFunc<Double, TMacdResult>; class function TIndicators.CreateMACD(FastPeriod, SlowPeriod, SignalPeriod: Integer): TIndicatorFunc<Double, TMacdResult>;
begin begin
var emaFast := CreateEMA(FastPeriod); var emaFast := CreateEMA(FastPeriod);
var emaSlow := CreateEMA(SlowPeriod); var emaSlow := CreateEMA(SlowPeriod);
var emaSignal := CreateEMA(SignalPeriod); var emaSignal := CreateEMA(SignalPeriod);
Result := Result :=
function(Value: Double): TMacdResult function(const Value: Double): TMacdResult
var var
fastVal, slowVal: Double; fastVal, slowVal: Double;
begin begin
@@ -240,14 +240,14 @@ begin
end; end;
end; end;
class function TIndicators.CreateRSI(Period: Integer): TFunc<Double, Double>; class function TIndicators.CreateRSI(Period: Integer): TIndicatorFunc<Double, Double>;
begin begin
var avgGain: Double := Double.NaN; var avgGain: Double := Double.NaN;
var avgLoss: Double := Double.NaN; var avgLoss: Double := Double.NaN;
var sourceData := TMycDataArray<Double>.CreateEmpty; var sourceData := TMycDataArray<Double>.CreateEmpty;
Result := Result :=
function(Value: Double): Double function(const Value: Double): Double
var var
change, gain, loss, rs: Double; change, gain, loss, rs: Double;
gainSum, lossSum: Double; gainSum, lossSum: Double;
@@ -299,11 +299,11 @@ begin
end; end;
end; end;
class function TIndicators.CreateSMA(Period: Integer): TFunc<Double, Double>; class function TIndicators.CreateSMA(Period: Integer): TIndicatorFunc<Double, Double>;
begin begin
var sourceData := TMycDataArray<Double>.CreateEmpty; var sourceData := TMycDataArray<Double>.CreateEmpty;
Result := Result :=
function(Value: Double): Double function(const Value: Double): Double
begin begin
sourceData := sourceData.Add(Value, Period); sourceData := sourceData.Add(Value, Period);
if (sourceData.Count >= Period) then if (sourceData.Count >= Period) then
@@ -313,13 +313,13 @@ begin
end; end;
end; end;
class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TFunc<TOhlcItem, TStochasticResult>; class function TIndicators.CreateStochastic(KPeriod, DPeriod: Integer): TIndicatorFunc<TOhlcItem, TStochasticResult>;
begin begin
var sourceData := TMycDataArray<TOhlcItem>.CreateEmpty; var sourceData := TMycDataArray<TOhlcItem>.CreateEmpty;
var smaD := CreateSMA(DPeriod); var smaD := CreateSMA(DPeriod);
Result := Result :=
function(Value: TOhlcItem): TStochasticResult function(const Value: TOhlcItem): TStochasticResult
var var
i: Integer; i: Integer;
highestHigh, lowestLow: Double; highestHigh, lowestLow: Double;
+4
View File
@@ -3,6 +3,10 @@ unit Myc.Trade.Types;
interface interface
type type
TTimeframe = (S, S5, S15, S30, M, M2, M3, M5, M10, M15, M30, H, H2, H3, H4, H8, H12, D, D2, D3, W, MN, MN3, MN6, Y);
TIndicatorFunc<S, T> = reference to function(const Value: S): T;
// A data record for an Ask/Bid price pair. // A data record for an Ask/Bid price pair.
TAskBidItem = packed record TAskBidItem = packed record
Ask: Double; Ask: Double;