diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index d5fa573..018becb 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -13,7 +13,8 @@ uses FirstStrategy in 'FirstStrategy.pas', Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', - Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas'; + Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas', + Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas'; {$R *.res} diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index 6670364..b01b2ad 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -4,7 +4,7 @@ 20.3 FMX True - Release + Debug Win64 AuraTrader 3 @@ -142,6 +142,7 @@ + Base @@ -185,6 +186,12 @@ true + + + AuraTrader.rsm + true + + AuraTrader.exe diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas index 6b7e93a..16f8b47 100644 --- a/AuraTrader/FirstStrategy.pas +++ b/AuraTrader/FirstStrategy.pas @@ -8,6 +8,7 @@ uses Myc.Signals, Myc.Lazy, Myc.TaskManager, + Myc.Trade.Types, Myc.Trade.DataPoint, Myc.Trade.DataArray; diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index 3f0a087..40b78f4 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -27,6 +27,7 @@ uses FMX.Memo, FMX.Objects, Myc.Futures, + Myc.Trade.Types, Myc.Trade.DataStream, Myc.Trade.DataPoint, Myc.Trade.DataProvider, @@ -183,17 +184,17 @@ begin var Hull: IMycConverter := TGenericIndicator.Create(TIndicators.CreateHMA(150)); Closes.Sender.Link(Hull); - chart.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); + chart.Panels[0].AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue); // Add SMA (Simple Moving Average) var Sma: IMycConverter := TGenericIndicator.Create(TIndicators.CreateSMA(50)); Closes.Sender.Link(Sma); - chart.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); + chart.Panels[0].AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow); // Add EMA (Exponential Moving Average) var Ema: IMycConverter := TGenericIndicator.Create(TIndicators.CreateEMA(21)); Closes.Sender.Link(Ema); - chart.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); + chart.Panels[0].AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua); // Add Bollinger Bands (20, 2.0) var Boli: IMycConverter := @@ -204,30 +205,26 @@ begin TMycGenericConverter .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end); Boli.Sender.Link(BoliUpper); - chart.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray); + chart.Panels[0].AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray); var BoliMiddle: IMycConverter := TMycGenericConverter .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end); Boli.Sender.Link(BoliMiddle); - chart.AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0); + chart.Panels[0].AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0); var BoliLower: IMycConverter := TMycGenericConverter .Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end); Boli.Sender.Link(BoliLower); - chart.AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray); + chart.Panels[0].AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray); - var rsiChart := TMycChart.Create(Self); - AlignControl(rsiChart); - rsiChart.Height := Layout.ChildrenRect.Width * 9 / 32; - rsiChart.Lookback.Value := 50000; - rsiChart.SetXAxisSeries(Timestamps.Sender); + chart.AddPanel; // Add RSI (Relative Strength Index) var Rsi: IMycConverter := TGenericIndicator.Create(TIndicators.CreateRSI(14)); Closes.Sender.Link(Rsi); - rsiChart.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); + chart.Panels[1].AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia); { // Add MACD (12, 26, 9) var Macd: IMycConverter := TGenericIndicator.Create(TIndicators.CreateMACD(12, 26, 9)); @@ -260,7 +257,7 @@ begin OhlcPoint.Sender.Link(TimeStamps); chart.SetXAxisSeries(Timestamps.Sender); - chart.AddOhlcSeries(Ohlc.Sender); + chart.Panels[0].AddOhlcSeries(Ohlc.Sender); var done := ExecuteStrategy(Symbol, OhlcPoint); diff --git a/Src/Myc.Fmx.Chart.Series.pas b/Src/Myc.Fmx.Chart.Series.pas index 214f982..3dbfe24 100644 --- a/Src/Myc.Fmx.Chart.Series.pas +++ b/Src/Myc.Fmx.Chart.Series.pas @@ -9,6 +9,7 @@ uses FMX.Graphics, Myc.Signals, Myc.Lazy, + Myc.Trade.Types, Myc.Trade.DataArray, Myc.Trade.DataPoint, Myc.Fmx.Chart; @@ -66,7 +67,7 @@ type procedure Update; override; procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; public - constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); + constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider); destructor Destroy; override; end; @@ -80,7 +81,7 @@ type procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; public constructor Create( - AOwner: TMycChart; + AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const AUpColor, ADownColor: TMutable ); @@ -96,7 +97,7 @@ type procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; public constructor Create( - AOwner: TMycChart; + AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single @@ -176,13 +177,13 @@ end; { TChartLineLayer } constructor TChartLineLayer.Create( - AOwner: TMycChart; + AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single ); begin - inherited Create(AOwner, ADataProvider); + inherited Create(AParent, ADataProvider); FLineColor := ALineColor; FLineWidth := ALineWidth; end; @@ -235,12 +236,12 @@ end; { TChartOhlcLayer } constructor TChartOhlcLayer.Create( - AOwner: TMycChart; + AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const AUpColor, ADownColor: TMutable ); begin - inherited Create(AOwner, ADataProvider); + inherited Create(AParent, ADataProvider); FUpColor := AUpColor; FDownColor := ADownColor; @@ -302,10 +303,10 @@ end; { TChartCustomLayer } -constructor TChartCustomLayer.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); +constructor TChartCustomLayer.Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider); begin - inherited Create(AOwner); - FSeries := TChartSeriesProcessor.Create(ADataProvider, AOwner.Lookback.AsMutable); + inherited Create(AParent); + FSeries := TChartSeriesProcessor.Create(ADataProvider, AParent.Owner.Lookback.AsMutable); end; destructor TChartCustomLayer.Destroy; diff --git a/Src/Myc.Fmx.Chart.pas b/Src/Myc.Fmx.Chart.pas index b263d14..30d432f 100644 --- a/Src/Myc.Fmx.Chart.pas +++ b/Src/Myc.Fmx.Chart.pas @@ -15,6 +15,7 @@ uses FMX.Controls, FMX.Graphics, FMX.Forms, + Myc.Trade.Types, Myc.Trade.DataPoint, Myc.Signals, Myc.Lazy; @@ -23,6 +24,9 @@ type TMycChart = class(TStyledControl) public type + TPanel = class; + + // Abstract base for a data series within the chart. TSeries = class abstract(TObject) protected function GetCount: Int64; virtual; abstract; @@ -32,25 +36,54 @@ type property TotalCount: Int64 read GetTotalCount; end; + // Abstract base for a drawable layer in a panel. TLayer = class abstract(TObject) private - FOwner: TMycChart; + FParent: TPanel; + function GetOwner: TMycChart; protected function GetSeries: TSeries; virtual; abstract; procedure Update; virtual; abstract; - function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract; - // The signature is changed to support viewport painting with an index offset + function GetValueRange(First, Last: Int64; out Min, Max: Double): Boolean; virtual; abstract; procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); virtual; abstract; public - constructor Create(AOwner: TMycChart); + constructor Create(AOwner: TPanel); procedure Repaint; - property Owner: TMycChart read FOwner; + property Owner: TMycChart read GetOwner; property Series: TSeries read GetSeries; end; + // A panel is a rectangular area in the chart with its own Y-axis, which can contain multiple layers. + TPanel = class(TObject) + private + FOwner: TMycChart; + FSeriesList: TObjectList; + protected + procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64); + procedure UpdateAndCheckChanges(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); + + public + constructor Create(AOwner: TMycChart); + destructor Destroy; override; + + function AddOhlcSeries( + const DataProvider: IMycDataProvider; + const AUpColor: TAlphaColor = TAlphaColors.Green; + const ADownColor: TAlphaColor = TAlphaColors.Red + ): TMycChart.TLayer; + + function AddDoubleSeries( + const DataProvider: IMycDataProvider; + const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; + const ALineWidth: Single = 1.5 + ): TMycChart.TLayer; + + property Owner: TMycChart read FOwner; + end; + private - FSeriesList: TList; - FXAxisSeries: TLayer; + FPanelList: TObjectList; + FXAxisSeries: TMycChart.TLayer; FLookback: TWriteable; FIdleSubscrId: TMessageSubscriptionId; FViewStartIndex: Int64; @@ -61,6 +94,8 @@ type FJumpButtonHot: Boolean; FJumpButtonPressed: Boolean; FNeedRepaint: TFlag; + function GetPanel(Index: Integer): TPanel; + function GetPanelCount: Integer; protected procedure Paint; override; procedure DoIdle; @@ -72,27 +107,16 @@ type constructor Create(AOwner: TComponent); override; destructor Destroy; override; + // Adds a new, empty panel to the bottom of the chart. + function AddPanel: TPanel; + // Sets the master series that defines the time scale (X-axis). This series is not drawn. - // The chart will not render any data until the X-axis series is set. - function SetXAxisSeries(const DataProvider: IMycDataProvider): TLayer; - - // Creates an OHLC candlestick/bar series and returns it. - // The data provider directly supplies OHLC items, as timestamps are managed by the data series internally. - function AddOhlcSeries( - const DataProvider: IMycDataProvider; - const AUpColor: TAlphaColor = TAlphaColors.Green; - const ADownColor: TAlphaColor = TAlphaColors.Red - ): TLayer; - - // Creates a simple line series for double values and returns it - function AddDoubleSeries( - const DataProvider: IMycDataProvider; - const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; - const ALineWidth: Single = 1.5 - ): TLayer; + function SetXAxisSeries(const DataProvider: IMycDataProvider): TMycChart.TLayer; property Lookback: TWriteable read FLookback write FLookback; property NeedRepaint: TFlag read FNeedRepaint; + property PanelCount: Integer read GetPanelCount; + property Panels[Index: Integer]: TPanel read GetPanel; default; end; implementation @@ -106,18 +130,21 @@ uses constructor TMycChart.Create(AOwner: TComponent); begin inherited Create(AOwner); - FSeriesList := TObjectList.Create(true); + FPanelList := TObjectList.Create(true); FXAxisSeries := nil; - FLookback := TWriteable.CreateWriteable(1000); // Load more data for panning + FLookback := TWriteable.CreateWriteable(1000); FViewStartIndex := 0; - FViewCount := 100; // Show 100 items by default + FViewCount := 100; FIsDragging := false; FIdleSubscrId := TMessageManager .DefaultManager .SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end); + + // Create the default panel. + AddPanel; end; destructor TMycChart.Destroy; @@ -125,97 +152,159 @@ begin TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubscrId); FXAxisSeries.Free; - FSeriesList.Free; + FPanelList.Free; inherited; end; -function TMycChart.SetXAxisSeries(const DataProvider: IMycDataProvider): TLayer; +function TMycChart.AddPanel: TPanel; +begin + Result := TPanel.Create(Self); + FPanelList.Add(Result); + Repaint; +end; + +function TMycChart.GetPanel(Index: Integer): TPanel; +begin + Result := FPanelList[Index]; +end; + +function TMycChart.GetPanelCount: Integer; +begin + Result := FPanelList.Count; +end; + +function TMycChart.SetXAxisSeries(const DataProvider: IMycDataProvider): TMycChart.TLayer; begin - // Free the old one if it exists, and create the new master series FXAxisSeries.Free; - + // Note: TChartCustomLayer needs its owner to be a TPanel. We use the first panel. var counter := TChartSeriesCounter.Create; - DataProvider.Link(counter); - - FXAxisSeries := TChartCustomLayer.Create(Self, counter.Sender); + FXAxisSeries := TChartCustomLayer.Create(Self.Panels[0], counter.Sender); Result := FXAxisSeries; end; -function TMycChart.AddDoubleSeries( - const DataProvider: IMycDataProvider; - const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; - const ALineWidth: Single = 1.5 -): TLayer; -begin - Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth); - FSeriesList.Add(Result); -end; - -function TMycChart.AddOhlcSeries( - const DataProvider: IMycDataProvider; - const AUpColor: TAlphaColor = TAlphaColors.Green; - const ADownColor: TAlphaColor = TAlphaColors.Red -): TLayer; -begin - Result := - TChartOhlcLayer.Create(Self, DataProvider, TMutable.Constant(AUpColor), TMutable.Constant(ADownColor)); - FSeriesList.Add(Result); -end; - procedure TMycChart.DoIdle; +var + prevTotalCount, newTotalCount: Int64; + seriesChanged, seriesRepaint, repaintSignal: Boolean; + countDelta: Int64; begin - // An X-axis series is required for any operation if not Assigned(FXAxisSeries) then exit; - // First update the master series - var prevTotalCount := FXAxisSeries.Series.TotalCount; + prevTotalCount := FXAxisSeries.Series.TotalCount; FXAxisSeries.Update; + newTotalCount := FXAxisSeries.Series.TotalCount; - // Check if the master series itself has changed - var newTotalCount := FXAxisSeries.Series.TotalCount; - var seriesChanged := prevTotalCount <> newTotalCount; + seriesChanged := prevTotalCount <> newTotalCount; + seriesRepaint := (FViewStartIndex = 0); - // Repaint if the last bar is visible, so we display live data - var seriesRepaint := FViewStartIndex = 0; - - for var series in FSeriesList do + for var panel in FPanelList do begin - var seriesCount := series.Series.TotalCount; - - // Check if this series may need a repaint (if it lags behind the master series and these lags are currently visible) - if FViewStartIndex <= prevTotalCount - seriesCount then - seriesRepaint := true; - - // Update the series - series.Update; - - if seriesCount <> series.Series.TotalCount then - seriesChanged := true; + var panelChanged, panelRepaint: Boolean; + panel.UpdateAndCheckChanges(prevTotalCount, FViewStartIndex, panelChanged, panelRepaint); + seriesChanged := seriesChanged or panelChanged; + seriesRepaint := seriesRepaint or panelRepaint; end; - // Don't move chart, if the last bar isn't visible - if FViewStartIndex > 0 then + if (FViewStartIndex > 0) then begin - var countDelta := newTotalCount - prevTotalCount; - if countDelta > 0 then + countDelta := newTotalCount - prevTotalCount; + if (countDelta > 0) then begin - // Adjust the start index based on the absolute change in total data points FViewStartIndex := FViewStartIndex + countDelta; - // Clamp to beginning of dataset - if FViewStartIndex + FViewCount > newTotalCount then + if (FViewStartIndex + FViewCount > newTotalCount) then FViewStartIndex := newTotalCount - FViewCount; end; end; - // finally, check the repaint signal - var repaintSignal := false; // FNeedRepaint.Reset; + repaintSignal := FNeedRepaint.Reset; if repaintSignal or (seriesRepaint and seriesChanged) then Repaint; end; +procedure TMycChart.Paint; +var + rect, panelRect: TRectF; + isButtonVisible: Boolean; + buttonColor: TAlphaColor; + path: TPathData; + masterTotalCount: Int64; + panelHeight: Single; +begin + inherited; + rect := Self.LocalRect; + + if (not Assigned(FXAxisSeries)) or (FXAxisSeries.Series.Count <= 1) or (FViewCount <= 1) or (PanelCount = 0) then + begin + Canvas.Fill.Color := TAlphaColors.Gray; + Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center); + Exit; + end; + + masterTotalCount := FXAxisSeries.Series.TotalCount; + panelHeight := rect.Height / PanelCount; + + for var i := 0 to PanelCount - 1 do + begin + var panel := Panels[i]; + panelRect := TRectF.Create(rect.Left, rect.Top + (i * panelHeight), rect.Right, rect.Top + ((i + 1) * panelHeight)); + + // Draw panel content + panel.Paint(Self.Canvas, panelRect, masterTotalCount, FViewStartIndex, FViewCount); + + // Draw separator line + if (i > 0) then + begin + Canvas.Stroke.Color := TAlphaColors.Gray; + Canvas.Stroke.Thickness := 1; + Canvas.DrawLine(PointF(panelRect.Left, panelRect.Top), PointF(panelRect.Right, panelRect.Top), 1); + end; + end; + + // --- Draw the "jump to latest" button (code unchanged) --- + isButtonVisible := (FViewStartIndex > 0); + + if isButtonVisible then + begin + FJumpButtonRect := TRectF.Create(Self.Width - 44, Self.Height - 44, Self.Width - 10, Self.Height - 10); + + if FJumpButtonPressed then + buttonColor := $FF707070 + else if FJumpButtonHot then + buttonColor := $FF505050 + else + buttonColor := $FF303030; + + Canvas.Fill.Color := buttonColor; + Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7); + + Canvas.Stroke.Color := TAlphaColors.White; + Canvas.Stroke.Thickness := 1.5; + + path := TPathData.Create; + try + var cx := FJumpButtonRect.CenterPoint.X; + var cy := FJumpButtonRect.CenterPoint.Y; + path.MoveTo(PointF(cx - 5, cy - 6)); + path.LineTo(PointF(cx, cy)); + path.LineTo(PointF(cx - 5, cy + 6)); + path.MoveTo(PointF(cx + 2, cy - 6)); + path.LineTo(PointF(cx + 7, cy)); + path.LineTo(PointF(cx + 2, cy + 6)); + Canvas.DrawPath(path, 1); + finally + path.Free; + end; + end + else + begin + FJumpButtonRect := TRectF.Empty; + FJumpButtonPressed := false; + end; +end; + procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin // Check for button press first @@ -382,153 +471,156 @@ begin Repaint; end; -procedure TMycChart.Paint; +{ TMycChart.TLayer } - function CalcView(Series: TSeries; MasterCount: Int64; out First: Int64; out Last: Int64): Boolean; +constructor TMycChart.TLayer.Create(AOwner: TPanel); +begin + inherited Create; + FParent := AOwner; +end; + +function TMycChart.TLayer.GetOwner: TMycChart; +begin + Result := FParent.Owner; +end; + +procedure TMycChart.TLayer.Repaint; +begin + FParent.Owner.Repaint; +end; + +{ TMycChart.TPanel } + +constructor TMycChart.TPanel.Create(AOwner: TMycChart); +begin + inherited Create; + FOwner := AOwner; + FSeriesList := TObjectList.Create(true); +end; + +destructor TMycChart.TPanel.Destroy; +begin + FSeriesList.Free; + inherited Destroy; +end; + +function TMycChart.TPanel.AddDoubleSeries( + const DataProvider: IMycDataProvider; + const ALineColor: TAlphaColor; + const ALineWidth: Single +): TMycChart.TLayer; +begin + Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth); + FSeriesList.Add(Result); +end; + +function TMycChart.TPanel.AddOhlcSeries( + const DataProvider: IMycDataProvider; + const AUpColor, ADownColor: TAlphaColor +): TMycChart.TLayer; +begin + Result := + TChartOhlcLayer.Create(Self, DataProvider, TMutable.Constant(AUpColor), TMutable.Constant(ADownColor)); + FSeriesList.Add(Result); +end; + +procedure TMycChart.TPanel.Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount, ViewStartIndex, ViewCount: Int64); +var + localMin, localMax, seriesMin, seriesMax, padding: Double; + rangeInitialized: Boolean; + seriesFirst, seriesLast: Int64; + xTransform, yTransform: TFunc; + + function CalcView(Series: TMycChart.TSeries; out First, Last: Int64): Boolean; + var + seriesTotalCount, offset, startIdx: Int64; begin - var seriesTotalCount := series.TotalCount; - var offset := MasterCount - seriesTotalCount; - var startIdx := FViewStartIndex - offset; + seriesTotalCount := series.TotalCount; + offset := MasterTotalCount - seriesTotalCount; + startIdx := ViewStartIndex - offset; First := Max(0, startIdx); - Last := Min(series.Count, startIdx + FViewCount) - 1; - - Result := First <= Last - 1; + Last := Min(series.Count, startIdx + ViewCount) - 1; + Result := First <= Last; end; -var - rect: TRectF; - globalMin, globalMax, seriesMin, seriesMax, padding: Double; - rangeInitialized: Boolean; - xTransform: TFunc; - yTransform: TFunc; - isButtonVisible: Boolean; - buttonColor: TAlphaColor; - path: TPathData; - masterTotalCount, seriesFirst, seriesLast: Int64; begin - inherited; - rect := Self.LocalRect; - - // The chart requires an X-axis series to be set before it can draw anything. - if not Assigned(FXAxisSeries) or (FXAxisSeries.Series.Count <= 1) or (FViewCount <= 1) then - begin - Canvas.Fill.Color := TAlphaColors.Gray; - Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center); - Exit; - end; - rangeInitialized := false; - masterTotalCount := FXAxisSeries.Series.TotalCount; + // 1. Calculate value range (Y-Axis) for this panel only for var series in FSeriesList do begin - if CalcView(series.Series, masterTotalCount, seriesFirst, seriesLast) then + if CalcView(series.Series, seriesFirst, seriesLast) then begin if series.GetValueRange(seriesFirst, seriesLast, seriesMin, seriesMax) then begin if not rangeInitialized then begin - globalMin := seriesMin; - globalMax := seriesMax; + localMin := seriesMin; + localMax := seriesMax; rangeInitialized := true; end else begin - globalMin := Min(globalMin, seriesMin); - globalMax := Max(globalMax, seriesMax); + localMin := Min(localMin, seriesMin); + localMax := Max(localMax, seriesMax); end; end; end; end; if not rangeInitialized then - Exit; + Exit; // Nothing to draw in this panel - padding := (globalMax - globalMin) * 0.1; - globalMin := globalMin - padding; - globalMax := globalMax + padding; + padding := (localMax - localMin) * 0.1; + localMin := localMin - padding; + localMax := localMax + padding; - if (globalMax - globalMin) = 0 then - exit; + if (localMax - localMin) = 0 then + localMax := localMin + 1; // Avoid division by zero - // Base transform maps an index from the viewport to a screen coordinate + // 2. Create transforms for this panel yTransform := - function(value: Double): Single begin Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height; end; + function(value: Double): Single + begin + Result := Viewport.Top + (1 - (value - localMin) / (localMax - localMin)) * Viewport.Height; + end; - // --- Draw Series with Offset Compensation --- - // The masterTotalCount is already calculated from the range finding loop + // 3. Paint all layers in this panel for var series in FSeriesList do begin - if CalcView(series.Series, masterTotalCount, seriesFirst, seriesLast) then + if CalcView(series.Series, seriesFirst, seriesLast) then begin - var offset := masterTotalCount - series.Series.TotalCount; + var offset := MasterTotalCount - series.Series.TotalCount; xTransform := function(index: Double): Single begin - Result := rect.Right - (((index + offset - FViewStartIndex) / (FViewCount - 1)) * rect.Width); + Result := Viewport.Right - (((index + offset - ViewStartIndex) / (ViewCount - 1)) * Viewport.Width); end; - series.Paint(Self.Canvas, seriesFirst, seriesLast, xTransform, yTransform); + series.Paint(Canvas, seriesFirst, seriesLast, xTransform, yTransform); end; end; - - // --- Draw the "jump to latest" button --- - isButtonVisible := (FViewStartIndex > 0); - - if isButtonVisible then - begin - // Define button position and size - FJumpButtonRect := TRectF.Create(Self.Width - 44, Self.Height - 44, Self.Width - 10, Self.Height - 10); - - // Determine color based on state - if FJumpButtonPressed then - buttonColor := $FF707070 // Pressed color - else if FJumpButtonHot then - buttonColor := $FF505050 // Hover color - else - buttonColor := $FF303030; // Default color - - // Draw button background - Canvas.Fill.Color := buttonColor; - Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7); - - // Draw an icon - Canvas.Stroke.Color := TAlphaColors.White; - Canvas.Stroke.Thickness := 1.5; - - path := TPathData.Create; - try - var cx := FJumpButtonRect.CenterPoint.X; - var cy := FJumpButtonRect.CenterPoint.Y; - path.MoveTo(PointF(cx - 5, cy - 6)); - path.LineTo(PointF(cx, cy)); - path.LineTo(PointF(cx - 5, cy + 6)); - path.MoveTo(PointF(cx + 2, cy - 6)); - path.LineTo(PointF(cx + 7, cy)); - path.LineTo(PointF(cx + 2, cy + 6)); - Canvas.DrawPath(path, 1); - finally - path.Free; - end; - end - else - begin - // Reset button state when not visible - FJumpButtonRect := TRectF.Empty; - FJumpButtonPressed := false; - end; end; -constructor TMycChart.TLayer.Create(AOwner: TMycChart); +procedure TMycChart.TPanel.UpdateAndCheckChanges(MasterTotalCount, ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); +var + seriesCount: Int64; begin - inherited Create; - FOwner := AOwner; -end; + SeriesChanged := false; + SeriesRepaint := false; + for var series in FSeriesList do + begin + seriesCount := series.Series.TotalCount; -procedure TMycChart.TLayer.Repaint; -begin - FOwner.Repaint; + if (ViewStartIndex <= MasterTotalCount - seriesCount) then + SeriesRepaint := true; + + series.Update; + + if (seriesCount <> series.Series.TotalCount) then + SeriesChanged := true; + end; end; end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 52702a5..a02b888 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -5,25 +5,10 @@ interface uses System.TimeSpan, Myc.Signals, - Myc.Core.Notifier; + Myc.Core.Notifier, + Myc.Trade.Types; type - // A data record for an Ask/Bid price pair. - TAskBidItem = packed record - Ask: Double; - Bid: Double; - constructor Create(AAsk, ABid: Double); - end; - - TOhlcItem = record - Open: Double; - High: Double; - Low: Double; - Close: Double; - Volume: Double; - constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double); - end; - // Represents a time-stamped data point in a series. TDataPoint = record Time: TDateTime; @@ -295,25 +280,6 @@ begin Result := Self.Convert(function(const Ohlc: TDataPoint): Single begin Result := Ohlc.Data.Volume; end); end; -{ TAskBidItem } - -constructor TAskBidItem.Create(AAsk, ABid: Double); -begin - Ask := AAsk; - Bid := ABid; -end; - -{ TOhlcItem } - -constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double); -begin - Open := AOpen; - High := AHigh; - Low := ALow; - Close := AClose; - Volume := AVolume; -end; - { TDataPoint } constructor TDataPoint.Create(ATime: TDateTime; const AData: T); diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index 4cdc180..7c0c342 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -5,8 +5,8 @@ interface uses System.SysUtils, System.Math, - Myc.Trade.DataArray, - Myc.Trade.DataPoint; + Myc.Trade.Types, + Myc.Trade.DataArray; type // Result for the Moving Average Convergence Divergence (MACD) indicator. diff --git a/Src/Myc.Trade.Types.pas b/Src/Myc.Trade.Types.pas new file mode 100644 index 0000000..e123c7d --- /dev/null +++ b/Src/Myc.Trade.Types.pas @@ -0,0 +1,43 @@ +unit Myc.Trade.Types; + +interface + +type + // A data record for an Ask/Bid price pair. + TAskBidItem = packed record + Ask: Double; + Bid: Double; + constructor Create(AAsk, ABid: Double); + end; + + TOhlcItem = record + Open: Double; + High: Double; + Low: Double; + Close: Double; + Volume: Double; + constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double); + end; + +implementation + +{ TAskBidItem } + +constructor TAskBidItem.Create(AAsk, ABid: Double); +begin + Ask := AAsk; + Bid := ABid; +end; + +{ TOhlcItem } + +constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double); +begin + Open := AOpen; + High := AHigh; + Low := ALow; + Close := AClose; + Volume := AVolume; +end; + +end.