diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index a55b017..7cf3b33 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -12,7 +12,8 @@ uses DynamicFMXControl in 'DynamicFMXControl.pas', FirstStrategy in 'FirstStrategy.pas', Myc.Fmx.Chart in 'Myc.Fmx.Chart.pas', - Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas'; + Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', + Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas'; {$R *.res} diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index ecdfe00..236e6a0 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -141,6 +141,7 @@ + Base diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas index a9a546f..405cc0d 100644 --- a/AuraTrader/FirstStrategy.pas +++ b/AuraTrader/FirstStrategy.pas @@ -12,25 +12,6 @@ uses type TTimeframe = (M1, M5, H1, D); - IMycConverter = interface(IMycProcessor) - function GetSender: IMycDataProvider; - property Sender: IMycDataProvider read GetSender; - end; - - TMycConverter = class abstract(TMycProcessor, IMycConverter) - private - FSender: TMycDataProvider; - function GetSender: IMycDataProvider; - protected - // Broadcasts the given data to all linked processors. - procedure Broadcast(const Value: T); - - public - constructor Create; - destructor Destroy; override; - property Sender: IMycDataProvider read GetSender; - end; - TMycGenericConverter = class(TMycConverter) type TConvertFunc = reference to function(const Value: S): T; @@ -255,29 +236,4 @@ begin Broadcast(FFunc(Value)); end; -{ TMycConverter } - -constructor TMycConverter.Create; -begin - inherited Create; - FSender := TMycDataProvider.Create(Self); -end; - -destructor TMycConverter.Destroy; -begin - FSender.Free; - inherited Destroy; -end; - -procedure TMycConverter.Broadcast(const Value: T); -begin - var cValue := Value; - FSender.Notify(function(const Processor: IMycProcessor): Boolean begin Result := Processor.ProcessData(cValue) end); -end; - -function TMycConverter.GetSender: IMycDataProvider; -begin - Result := FSender; -end; - end. diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index b1815fb..210c2d5 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -165,6 +165,10 @@ begin var OhlcPoint: IMycConverter>, TDataPoint> := TTicksToTimeframe.Create(M1); + var Timestamps: IMycConverter, TDateTime> := + TMycGenericConverter, TDateTime> + .Create(function(const Ohlc: TDataPoint): TDateTime begin Result := Ohlc.Time; end); + var Ohlc: IMycConverter, TOhlcItem> := TMycGenericConverter, TOhlcItem> .Create(function(const Ohlc: TDataPoint): TOhlcItem begin Result := Ohlc.Data; end); @@ -189,13 +193,14 @@ begin // // chart.AddDoubleSeries(Hull.Sender); + chart.SetXAxisSeries(OHLC.Sender); + chart.AddOhlcSeries(Ohlc.Sender); + var done := ExecuteStrategy(Symbol, OhlcPoint); ///// FProcessDone := TState.All([FProcessDone, done]); - - chart.AddOhlcSeries(OhlcPoint.Sender); end; procedure TForm1.TreeViewDblClick(Sender: TObject); diff --git a/Src/Myc.Fmx.Chart.Series.pas b/Src/Myc.Fmx.Chart.Series.pas new file mode 100644 index 0000000..23cf722 --- /dev/null +++ b/Src/Myc.Fmx.Chart.Series.pas @@ -0,0 +1,282 @@ +unit Myc.FMX.Chart.Series; + +interface + +uses + System.SysUtils, + System.SyncObjs, + System.UITypes, + FMX.Graphics, + Myc.Trade.DataArray, + Myc.Trade.DataPoint, + Myc.Lazy, + Myc.Fmx.Chart; + +type + TChartSeriesCounter = class(TMycConverter) + private + FCount: Int64; + protected + function ProcessData(const Value: T): Boolean; override; + public + constructor Create; + end; + + TChartSeriesReceiver = class(TMycProcessor) + strict private + FCurrData: TMycDataArray; + FLookback: TWriteable; + private + FData: TWriteable>; + protected + function ProcessData(const Value: T): Boolean; override; + public + constructor Create(const ALookback: TWriteable); + property Data: TWriteable> read FData; + end; + + TChartSeriesProcessor = class(TMycChart.TSeries) + strict private + FDataSeries: TMycDataArray; + FLock: TSpinLock; + private + FData: TMycDataArray; + FDataProvider: IMycDataProvider; + FReceiver: TChartSeriesReceiver; + FReceiverTag: TTag; + protected + function GetCount: Int64; override; + function GetTotalCount: Int64; override; + procedure Update; override; + + public + constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); + destructor Destroy; override; + property Data: TMycDataArray read FData; + end; + + { TChartOhlcSeries } + TChartOhlcSeries = class(TChartSeriesProcessor) + private + FUpColor: TAlphaColor; + FDownColor: TAlphaColor; + protected + function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; + public + constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider; const AUpColor, ADownColor: TAlphaColor); + end; + + { TChartLineSeries } + TChartLineSeries = class(TChartSeriesProcessor) + private + FLineColor: TAlphaColor; + FLineWidth: Single; + protected + function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; + public + constructor Create( + AOwner: TMycChart; + const ADataProvider: IMycDataProvider; + const ALineColor: TAlphaColor; + ALineWidth: Single + ); + end; + +implementation + +uses + System.Types, + System.Math, + FMX.Types; + +constructor TChartSeriesCounter.Create; +begin + inherited Create; + FCount := 0; +end; + +function TChartSeriesCounter.ProcessData(const Value: T): Boolean; +begin + Result := true; + Broadcast(FCount); + inc(FCount); +end; + +{ TChartSeriesReceiver } + +constructor TChartSeriesReceiver.Create(const ALookback: TWriteable); +begin + inherited Create; + FLookback := ALookback; + FCurrData := TMycDataArray.CreateEmpty; + FData := TWriteable>.CreateWriteable(FCurrData).Protect; +end; + +function TChartSeriesReceiver.ProcessData(const Value: T): Boolean; +begin + Result := true; + FCurrData := FCurrData.Add(Value, FLookback.Value); + FData.Value := FCurrData; +end; + +{ TChartSeriesProcessor } + +constructor TChartSeriesProcessor.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); +begin + inherited Create(AOwner); + FLock := TSpinLock.Create(false); + FDataSeries := TMycDataArray.CreateEmpty; + FData := FDataSeries; + FDataProvider := ADataProvider; + FReceiver := TChartSeriesReceiver.Create(AOwner.Lookback); + FReceiverTag := FDataProvider.Link(FReceiver); +end; + +destructor TChartSeriesProcessor.Destroy; +begin + FDataProvider.Unlink(FReceiverTag); + inherited; +end; + +function TChartSeriesProcessor.GetCount: Int64; +begin + Result := FData.Count; +end; + +function TChartSeriesProcessor.GetTotalCount: Int64; +begin + Result := FData.TotalCount; +end; + +procedure TChartSeriesProcessor.Update; +begin + FData := FReceiver.Data.Value; +end; + +{ TChartLineSeries } + +constructor TChartLineSeries.Create( + AOwner: TMycChart; + const ADataProvider: IMycDataProvider; + const ALineColor: TAlphaColor; + ALineWidth: Single +); +begin + inherited Create(AOwner, ADataProvider); + FLineColor := ALineColor; + FLineWidth := ALineWidth; +end; + +function TChartLineSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; +var + i: Int64; +begin + MinValue := MaxDouble; + MaxValue := -MaxDouble; + for i := First to Last do + begin + var v := Data.Items[i]; + if not IsNaN(v) then + begin + MinValue := Min(MinValue, v); + MaxValue := Max(MaxValue, v); + end; + end; + Result := (MinValue <> MaxDouble); +end; + +procedure TChartLineSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); +var + points: TPathData; + i, n: Int64; +begin + points := TPathData.Create; + try + // Skip warmup data + n := First; + while (n <= Last) and (IsNaN(Data[n])) do + inc(n); + if (n > Last) then + exit; + + points.MoveTo(TPointF.Create(XForm(n), YForm(Data[n]))); + for i := n + 1 to Last do + if not IsNaN(Data[i]) then + points.LineTo(TPointF.Create(XForm(i), YForm(Data[i]))); + + Canvas.Stroke.Color := FLineColor; + Canvas.Stroke.Thickness := FLineWidth; + Canvas.DrawPath(points, 1); + finally + points.Free; + end; +end; + +{ TChartOhlcSeries } + +constructor TChartOhlcSeries.Create( + AOwner: TMycChart; + const ADataProvider: IMycDataProvider; + const AUpColor, ADownColor: TAlphaColor +); +begin + inherited Create(AOwner, ADataProvider); + FUpColor := AUpColor; + FDownColor := ADownColor; +end; + +function TChartOhlcSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; +var + i: Int64; +begin + MinValue := MaxDouble; + MaxValue := -MaxDouble; + + for i := First to Last do + begin + var item := Data.Items[i]; + MinValue := Min(MinValue, item.Low); + MaxValue := Max(MaxValue, item.High); + end; + Result := (MinValue <> MaxDouble); +end; + +procedure TChartOhlcSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); +var + i: Int64; + x, candleWidth: Single; + yOpen, yHigh, yLow, yClose: Single; + item: TOhlcItem; + isUp: boolean; +begin + candleWidth := Max(2, 0.8 * Abs((XForm(First + 1) - XForm(First)))); + + for i := First to Last do + begin + item := Data.Items[i]; + x := XForm(i); + yOpen := YForm(item.Open); + yClose := YForm(item.Close); + yHigh := YForm(item.High); + yLow := YForm(item.Low); + isUp := item.Close >= item.Open; + + if isUp then + Canvas.Stroke.Color := FUpColor + else + Canvas.Stroke.Color := FDownColor; + Canvas.Stroke.Thickness := 1.0; + + Canvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1); + + Canvas.Fill.Color := Canvas.Stroke.Color; + if isUp then + Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1) + else + Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1); + end; +end; + +end. diff --git a/AuraTrader/Myc.Fmx.Chart.pas b/Src/Myc.Fmx.Chart.pas similarity index 61% rename from AuraTrader/Myc.Fmx.Chart.pas rename to Src/Myc.Fmx.Chart.pas index 046f36f..83d041b 100644 --- a/AuraTrader/Myc.Fmx.Chart.pas +++ b/Src/Myc.Fmx.Chart.pas @@ -16,7 +16,6 @@ uses FMX.Graphics, FMX.Forms, Myc.Trade.DataPoint, - Myc.Signals, Myc.Lazy; type @@ -30,10 +29,10 @@ type protected function GetCount: Int64; virtual; abstract; function GetTotalCount: Int64; virtual; abstract; - function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract; procedure Update; virtual; abstract; + function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; // The signature is changed to support viewport painting with an index offset - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); virtual; abstract; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); virtual; property MainSeries: TSeries read GetMainSeries; public constructor Create(AOwner: TMycChart); @@ -44,6 +43,7 @@ type private FSeriesList: TList; + FXAxisSeries: TSeries; FLookback: TWriteable; FIdleSubscrId: TMessageSubscriptionId; FViewStartIndex: Int64; @@ -64,9 +64,14 @@ type constructor Create(AOwner: TComponent); override; destructor Destroy; override; - // Creates an OHLC candlestick/bar series and returns it + // 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): TSeries; + + // 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 DataProvider: IMycDataProvider; const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red ): TSeries; @@ -85,78 +90,7 @@ implementation uses System.Math, - System.SyncObjs, - WinApi.Windows, - Myc.Trade.DataArray; - -type - TChartSeriesReceiver = class(TMycProcessor) - strict private - FCurrData: TMycDataArray; - FLookback: TWriteable; - private - FData: TWriteable>; - protected - function ProcessData(const Value: T): Boolean; override; - public - constructor Create(const ALookback: TWriteable); - property Data: TWriteable> read FData; - end; - - TChartSeriesProcessor = class(TMycChart.TSeries) - strict private - FDataSeries: TMycDataArray; - FLock: TSpinLock; - private - FData: TMycDataArray; - FDataProvider: IMycDataProvider; - FReceiver: TChartSeriesReceiver; - FReceiverTag: TTag; - protected - function GetCount: Int64; override; - function GetTotalCount: Int64; override; - procedure Update; override; - - public - constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); - destructor Destroy; override; - property Data: TMycDataArray read FData; - end; - - { TChartOhlcSeries } - TChartOhlcSeries = class(TChartSeriesProcessor>) - private - FUpColor: TAlphaColor; - FDownColor: TAlphaColor; - protected - function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; - public - constructor Create( - AOwner: TMycChart; - const ADataProvider: IMycDataProvider>; - const AUpColor, ADownColor: TAlphaColor - ); - end; - - { TChartLineSeries } - TChartLineSeries = class(TChartSeriesProcessor) - private - FLineColor: TAlphaColor; - FLineWidth: Single; - protected - function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; - public - constructor Create( - AOwner: TMycChart; - const ADataProvider: IMycDataProvider; - const ALineColor: TAlphaColor; - ALineWidth: Single - ); - end; - -{ TMycChart.TSeries } + Myc.FMX.Chart.Series; constructor TMycChart.TSeries.Create(AOwner: TMycChart); begin @@ -166,9 +100,17 @@ end; function TMycChart.TSeries.GetMainSeries: TSeries; begin - Result := nil; - if (Owner.FSeriesList.Count > 0) then - Result := Owner.FSeriesList[0]; + Result := Owner.FXAxisSeries; +end; + +function TMycChart.TSeries.GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; +begin + Result := false; +end; + +procedure TMycChart.TSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); +begin + // nothing end; { TMycChart } @@ -177,6 +119,7 @@ constructor TMycChart.Create(AOwner: TComponent); begin inherited Create(AOwner); FSeriesList := TObjectList.Create(true); + FXAxisSeries := nil; FLookback := TWriteable.CreateWriteable(1000); // Load more data for panning FViewStartIndex := 0; @@ -193,10 +136,24 @@ destructor TMycChart.Destroy; begin TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubscrId); + FXAxisSeries.Free; FSeriesList.Free; inherited; end; +function TMycChart.SetXAxisSeries(const DataProvider: IMycDataProvider): TSeries; +begin + // Free the old one if it exists, and create the new master series + FXAxisSeries.Free; + + var counter := TChartSeriesCounter.Create; + + DataProvider.Link(counter); + + FXAxisSeries := TChartSeriesProcessor.Create(Self, counter.Sender); + Result := FXAxisSeries; +end; + function TMycChart.AddDoubleSeries( const DataProvider: IMycDataProvider; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; @@ -208,7 +165,7 @@ begin end; function TMycChart.AddOhlcSeries( - const DataProvider: IMycDataProvider>; + const DataProvider: IMycDataProvider; const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red ): TSeries; @@ -219,18 +176,26 @@ end; procedure TMycChart.DoIdle; begin - if (FSeriesList.Count = 0) then + // An X-axis series is required for any operation + if not Assigned(FXAxisSeries) then exit; - var prevTotalCount := FSeriesList[0].TotalCount; + // First update the master series + var prevTotalCount := FXAxisSeries.TotalCount; + FXAxisSeries.Update; + + // Check if the master series itself has changed + var newTotalCount := FXAxisSeries.TotalCount; + var seriesChanged := prevTotalCount <> newTotalCount; + + // Repaint if the last bar is visible, so we display live data + var seriesRepaint := FViewStartIndex = 0; - var seriesRepaint := false; - var seriesChanged := false; for var series in FSeriesList do begin var seriesCount := series.TotalCount; - // Check if this series may need a repaint + // 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; @@ -241,16 +206,15 @@ begin seriesChanged := true; end; - // Don't move with live data, if the last bar isn't visible + // Don't move chart, if the last bar isn't visible if FViewStartIndex > 0 then begin - var newTotalCount := FSeriesList[0].TotalCount; var 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 FViewStartIndex := newTotalCount - FViewCount; end; @@ -339,7 +303,8 @@ begin exit; end; - if (FSeriesList.Count = 0) or (FViewCount <= 0) then + // An X-axis series is required for panning + if not Assigned(FXAxisSeries) or (FViewCount <= 0) then begin exit; end; @@ -352,7 +317,7 @@ begin FViewStartIndex := FViewStartIndex + indexDelta; // Clamp values - mainSeries := FSeriesList[0]; + mainSeries := FXAxisSeries; maxIndex := mainSeries.Count - FViewCount; if (maxIndex < 0) then maxIndex := 0; @@ -380,9 +345,11 @@ var begin Handled := true; - if (FSeriesList.Count = 0) then + // An X-axis series is required for zooming + if not Assigned(FXAxisSeries) then exit; - mainSeries := FSeriesList[0]; + + mainSeries := FXAxisSeries; if (mainSeries.Count = 0) or (Width <= 0) then exit; @@ -456,7 +423,8 @@ begin inherited; rect := Self.LocalRect; - if (FSeriesList.Count = 0) or (FViewCount <= 1) then + // The chart requires an X-axis series to be set before it can draw anything. + if not Assigned(FXAxisSeries) or (FXAxisSeries.Count <= 1) or (FViewCount <= 1) then begin Canvas.Fill.Color := TAlphaColors.Gray; Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center); @@ -464,7 +432,7 @@ begin end; rangeInitialized := false; - mainSeries := FSeriesList[0]; + mainSeries := FXAxisSeries; masterTotalCount := mainSeries.TotalCount; for series in FSeriesList do @@ -566,179 +534,4 @@ begin end; end; -{ TChartSeriesReceiver } - -constructor TChartSeriesReceiver.Create(const ALookback: TWriteable); -begin - inherited Create; - FLookback := ALookback; - FCurrData := TMycDataArray.CreateEmpty; - FData := TWriteable>.CreateWriteable(FCurrData).Protect; -end; - -function TChartSeriesReceiver.ProcessData(const Value: T): Boolean; -begin - Result := true; - FCurrData := FCurrData.Add(Value, FLookback.Value); - FData.Value := FCurrData; -end; - -{ TChartSeriesProcessor } - -constructor TChartSeriesProcessor.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); -begin - inherited Create(AOwner); - FLock := TSpinLock.Create(false); - FDataSeries := TMycDataArray.CreateEmpty; - FData := FDataSeries; - FDataProvider := ADataProvider; - FReceiver := TChartSeriesReceiver.Create(AOwner.FLookback); - FReceiverTag := FDataProvider.Link(FReceiver); -end; - -destructor TChartSeriesProcessor.Destroy; -begin - FDataProvider.Unlink(FReceiverTag); - inherited; -end; - -function TChartSeriesProcessor.GetCount: Int64; -begin - Result := FData.Count; -end; - -function TChartSeriesProcessor.GetTotalCount: Int64; -begin - Result := FData.TotalCount; -end; - -procedure TChartSeriesProcessor.Update; -begin - FData := FReceiver.Data.Value; -end; - -{ TChartOhlcSeries } - -constructor TChartOhlcSeries.Create( - AOwner: TMycChart; - const ADataProvider: IMycDataProvider>; - const AUpColor, ADownColor: TAlphaColor -); -begin - inherited Create(AOwner, ADataProvider); - FUpColor := AUpColor; - FDownColor := ADownColor; -end; - -function TChartOhlcSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; -var - i: Int64; -begin - MinValue := MaxDouble; - MaxValue := -MaxDouble; - - for i := First to Last do - begin - var dp := Data.Items[i]; - MinValue := Min(MinValue, dp.Data.Low); - MaxValue := Max(MaxValue, dp.Data.High); - end; - Result := (MinValue <> MaxDouble); -end; - -procedure TChartOhlcSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); -var - i: Int64; - x, candleWidth: Single; - yOpen, yHigh, yLow, yClose: Single; - item: TDataPoint; - isUp: boolean; -begin - candleWidth := Max(2, 0.8 * Abs((XForm(First + 1) - XForm(First)))); - - for i := First to Last do - begin - item := Data.Items[i]; - x := XForm(i); - yOpen := YForm(item.Data.Open); - yClose := YForm(item.Data.Close); - yHigh := YForm(item.Data.High); - yLow := YForm(item.Data.Low); - isUp := item.Data.Close >= item.Data.Open; - - if isUp then - Canvas.Stroke.Color := FUpColor - else - Canvas.Stroke.Color := FDownColor; - Canvas.Stroke.Thickness := 1.0; - - Canvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1); - - Canvas.Fill.Color := Canvas.Stroke.Color; - if isUp then - Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1) - else - Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1); - end; -end; - -{ TChartLineSeries } - -constructor TChartLineSeries.Create( - AOwner: TMycChart; - const ADataProvider: IMycDataProvider; - const ALineColor: TAlphaColor; - ALineWidth: Single -); -begin - inherited Create(AOwner, ADataProvider); - FLineColor := ALineColor; - FLineWidth := ALineWidth; -end; - -function TChartLineSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; -var - i: Int64; -begin - MinValue := MaxDouble; - MaxValue := -MaxDouble; - for i := First to Last do - begin - var v := Data.Items[i]; - if not IsNaN(v) then - begin - MinValue := Min(MinValue, v); - MaxValue := Max(MaxValue, v); - end; - end; - Result := (MinValue <> MaxDouble); -end; - -procedure TChartLineSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); -var - points: TPathData; - i, n: Int64; -begin - points := TPathData.Create; - try - // Skip warmup data - n := First; - while (n <= Last) and (IsNaN(Data[n])) do - inc(n); - if (n > Last) then - exit; - - points.MoveTo(TPointF.Create(XForm(n), YForm(Data[n]))); - for i := n + 1 to Last do - if not IsNaN(Data[i]) then - points.LineTo(TPointF.Create(XForm(i), YForm(Data[i]))); - - Canvas.Stroke.Color := FLineColor; - Canvas.Stroke.Thickness := FLineWidth; - Canvas.DrawPath(points, 1); - finally - points.Free; - end; -end; - end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 5673b76..40b1bf4 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -66,7 +66,7 @@ type private FProc: TProc; protected - function ProcessData(const Value: T): Boolean; override; + function ProcessData(const Value: T): Boolean; override; final; public constructor Create(const AProc: TProc); end; @@ -81,6 +81,27 @@ type constructor Create(const Controller: IInterface; const AProc: TProc); end; + IMycConverter = interface(IMycProcessor) + function GetSender: IMycDataProvider; + property Sender: IMycDataProvider read GetSender; + end; + + TMycConverter = class abstract(TMycProcessor, IMycConverter) + private + FSender: TMycDataProvider; + function GetSender: IMycDataProvider; + protected + function ProcessData(const Value: S): Boolean; override; abstract; + + // Broadcasts the given data to all linked processors. + procedure Broadcast(const Value: T); + + public + constructor Create; + destructor Destroy; override; + property Sender: IMycDataProvider read GetSender; + end; + // An immutable time-ordered series of data points. // The most recent element has the logical index 0. IDataSeries = interface @@ -508,4 +529,29 @@ begin end; end; +{ TMycConverter } + +constructor TMycConverter.Create; +begin + inherited Create; + FSender := TMycDataProvider.Create(Self); +end; + +destructor TMycConverter.Destroy; +begin + FSender.Free; + inherited Destroy; +end; + +procedure TMycConverter.Broadcast(const Value: T); +begin + var cValue := Value; + FSender.Notify(function(const Processor: IMycProcessor): Boolean begin Result := Processor.ProcessData(cValue) end); +end; + +function TMycConverter.GetSender: IMycDataProvider; +begin + Result := FSender; +end; + end.