/* * NPlot - A charting library for .NET * * CandlePlot.cs * Copyright (C) 2003-2006 Matt Howlett and others. * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of NPlot nor the names of its contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Drawing; using System.Data; namespace NPlot { /// /// Encapsulates open, low, high and close values useful for specifying financial data /// over a time period, together with a [single] x-value indicating the time [period] the /// data corresponds to. /// public class PointOLHC { /// /// Constructor /// /// value representing the time period that the financial values refer to /// The value at open of time period. /// The low value over the time period /// The high value over the time period. /// The value at close of time period. public PointOLHC( double x, double open, double low, double high, double close ) { this.x_ = x; this.open_ = open; this.close_ = close; this.low_ = low; this.high_ = high; } /// /// value representing the time period that the financial values apply to. /// public double X { get { return x_; } set { this.x_ = value; } } private double x_; /// /// The value at open of time period. /// public double Open { get { return open_; } set { open_ = value; } } private double open_; /// /// The value at close of time period. /// public double Close { get { return close_; } set { close_ = value; } } private double close_; /// /// Low value of the time period. /// public double Low { get { return low_; } set { low_ = value; } } private double low_; /// /// High value of the time period. /// public double High { get { return high_; } set { high_ = value; } } private double high_; } /// /// Encapsulates functionality for drawing finacial candle charts. /// public class CandlePlot : BasePlot, IPlot { /// /// /// public abstract class CandleStyle { /// /// /// /// /// public abstract CandleStyle Create(CandleDataAdapter d); } /// /// /// public class Stick : CandleStyle { private Stick() { } /// /// /// /// /// public override CandleStyle Create(CandleDataAdapter d) { return new Stick(); } } /// /// This class is responsible for interpreting the various ways you can /// specify data to CandlePlot objects /// public class CandleDataAdapter { private object openData_; private object lowData_; private object highData_; private object closeData_; private object abscissaData_; private object dataSource_; private string dataMember_; DataRowCollection rows_ = null; // speed optimizations if data is double. private double[] openDataArray_; private double[] lowDataArray_; private double[] highDataArray_; private double[] closeDataArray_; private double[] abscissaDataArray_; private bool useDoublesArrays_; /// /// Constructor /// /// /// /// /// /// /// /// public CandleDataAdapter( object dataSource, string dataMember, object abscissaData, object openData, object lowData, object highData, object closeData ) { this.openData_ = openData; this.lowData_ = lowData; this.highData_ = highData; this.closeData_ = closeData; this.abscissaData_ = abscissaData; this.dataSource_ = dataSource; this.dataMember_ = dataMember; if (dataSource_ != null) { if ( dataSource_ is DataSet ) { if (dataMember_ != null) { rows_ = ((DataTable)((DataSet)dataSource_).Tables[dataMember_]).Rows; } else { rows_ = ((DataTable)((DataSet)dataSource_).Tables[0]).Rows; } } else if (dataSource_ is DataTable ) { rows_ = ((DataTable)dataSource_).Rows; } else { throw new NPlotException ( "not implemented yet" ); } } openDataArray_ = openData_ as System.Double[]; lowDataArray_ = lowData_ as System.Double[]; highDataArray_ = highData_ as System.Double[]; closeDataArray_ = closeData_ as System.Double[]; abscissaDataArray_ = abscissaData_ as System.Double[]; useDoublesArrays_ = ( openDataArray_ != null && lowDataArray_ != null && highDataArray_ != null && lowDataArray_ != null && abscissaDataArray_ != null ); } /// /// Gets the ith point in the candle adapter /// /// index of datapoint to get /// the datapoint. public PointOLHC this[int i] { get { // try a fast track first if (useDoublesArrays_) { return new PointOLHC( abscissaDataArray_[i], openDataArray_[i], lowDataArray_[i], highDataArray_[i], closeDataArray_[i]); } // is the data coming from a data source? else if (rows_ != null) { double x = Utils.ToDouble(((DataRow)(rows_[i]))[(string)abscissaData_]); double open = Utils.ToDouble(((DataRow)(rows_[i]))[(string)openData_]); double low = Utils.ToDouble(((DataRow)(rows_[i]))[(string)lowData_]); double high = Utils.ToDouble(((DataRow)(rows_[i]))[(string)highData_]); double close = Utils.ToDouble(((DataRow)(rows_[i]))[(string)closeData_]); return new PointOLHC(x, open, low, high, close); } // the data is coming from individual arrays. else if (abscissaData_ is Array && openData_ is Array && lowData_ is Array && highData_ is Array && closeData_ is Array) { double x = Utils.ToDouble(((Array)abscissaData_).GetValue(i)); double open = Utils.ToDouble(((Array)openData_).GetValue(i)); double low = Utils.ToDouble(((Array)lowData_).GetValue(i)); double high = Utils.ToDouble(((Array)highData_).GetValue(i)); double close = Utils.ToDouble(((Array)closeData_).GetValue(i)); return new PointOLHC(x, open, low, high, close); } else { throw new NPlotException("not implemented yet"); } } } /// /// The number of datapoints available via the candle adapter. /// /// the number of datapoints available. public int Count { get { // this is inefficient [could set up delegates in constructor]. if (useDoublesArrays_) { return openDataArray_.Length; } if (openData_ == null) { return 0; } if (rows_ != null) { return rows_.Count; } if (openData_ is Array) { int size = ((Array)openData_).Length; if (size != ((Array)closeData_).Length) throw new NPlotException("open and close arrays are not of same length"); if (size != ((Array)lowData_).Length) throw new NPlotException("open and close arrays are not of same length"); if (size != ((Array)highData_).Length) throw new NPlotException("open and close arrays are not of same length"); return size; } throw new NPlotException( "data not in correct format" ); } } /// /// Returns an x-axis that is suitable for drawing the data. /// /// A suitable x-axis. public Axis SuggestXAxis() { double min; double max; double minStep = 0.0; if (this.rows_ == null) { Utils.ArrayMinMax((System.Collections.IList)this.abscissaData_, out min, out max); if (((System.Collections.IList)abscissaData_).Count > 1) { double first = Utils.ToDouble(((Array)abscissaData_).GetValue(0)); double second = Utils.ToDouble(((Array)abscissaData_).GetValue(1)); minStep = Math.Abs(second - first); } if (((System.Collections.IList)abscissaData_).Count > 2) { double first = Utils.ToDouble(((Array)abscissaData_).GetValue(1)); double second = Utils.ToDouble(((Array)abscissaData_).GetValue(2)); if (Math.Abs(second - first) < minStep) minStep = Math.Abs(second - first); } if (((System.Collections.IList)abscissaData_)[0] is DateTime) { return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0); } else { return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0); } } else { Utils.RowArrayMinMax(this.rows_, out min, out max, (string)this.abscissaData_); if (rows_.Count > 1) { double first = Utils.ToDouble(rows_[0][(string)abscissaData_]); double second = Utils.ToDouble(rows_[1][(string)abscissaData_]); minStep = Math.Abs(second - first); } if (rows_.Count > 2) { double first = Utils.ToDouble(rows_[1][(string)abscissaData_]); double second = Utils.ToDouble(rows_[2][(string)abscissaData_]); if (Math.Abs(second - first) < minStep) minStep = Math.Abs(second - first); } if ((rows_[0])[(string)abscissaData_] is DateTime) { return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0); } else { return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0); } } } /// /// Returns a y-axis that is suitable for drawing the data. /// /// A suitable y-axis. public Axis SuggestYAxis() { double min_l; double max_l; double min_h; double max_h; if (this.rows_ == null) { Utils.ArrayMinMax((System.Collections.IList)lowData_, out min_l, out max_l); Utils.ArrayMinMax((System.Collections.IList)highData_, out min_h, out max_h); } else { Utils.RowArrayMinMax(this.rows_, out min_l, out max_l, (string)this.lowData_); Utils.RowArrayMinMax(this.rows_, out min_h, out max_h, (string)this.highData_); } Axis a = new LinearAxis( min_l, max_h ); a.IncreaseRange( 0.08 ); return a; } } /// /// Default constructor. /// public CandlePlot() { } /// /// Calculates the physical (not world) separation between abscissa values. /// /// Candle adapter containing data /// Physical x axis the data is plotted against. /// physical separation between abscissa values. private static float CalculatePhysicalSeparation( CandleDataAdapter cd, PhysicalAxis xAxis ) { if (cd.Count > 1) { int xPos1 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[0]).X, false )).X; int xPos2 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[1]).X, false )).X; int minDist = xPos2 - xPos1; if (cd.Count > 2) { // to be pretty sure we get the smallest gap. int xPos3 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[2]).X, false)).X; if (xPos3 - xPos2 < minDist) minDist = xPos3 - xPos2; if (cd.Count > 3) { int xPos4 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[3]).X, false)).X; if (xPos4 - xPos3 < minDist) minDist = xPos4 - xPos3; } } return minDist; } return 0.0f; } /// /// Draws the candle plot on a GDI+ surface agains the provided x and y axes. /// /// The GDI+ surface on which to draw. /// The X-Axis to draw against. /// The Y-Axis to draw against. public void Draw( Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis ) { CandleDataAdapter cd = new CandleDataAdapter( this.DataSource, this.DataMember, this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData ); Brush bearishBrush = new SolidBrush( BearishColor ); Brush bullishBrush = new SolidBrush( BullishColor ); uint offset = 0; if (this.centered_) { offset = (uint)(CalculatePhysicalSeparation(cd,xAxis) / 2.0f); } uint addAmount = (uint)StickWidth/2; uint stickWidth = (uint)StickWidth; if (StickWidth == AutoScaleStickWidth) { // default addAmount = 2; stickWidth = 4; float minDist = CalculatePhysicalSeparation( cd, xAxis ); addAmount = (uint)(minDist / 3); stickWidth = addAmount * 2; } Pen p = new Pen(this.color_); /* // brant hyatt proposed. if (this.Style == Styles.Stick) { p.Width = stickWidth; addAmount = stickWidth + 2; } */ for (int i=0; i 0) { if ( ((PointOLHC)cd[i]).Close > ((PointOLHC)cd[i-1]).Close) { p.Color = BullishColor; } else { p.Color = BearishColor; } } */ g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 ); g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos+offset, yPos3 ); g.DrawLine( p, xPos+offset, yPos4, xPos+addAmount+offset, yPos4 ); } else if (this.Style == Styles.Filled) { g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 ); if (yPos3 > yPos4) { g.FillRectangle( bullishBrush, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 ); g.DrawRectangle( p, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 ); } else if (yPos3 < yPos4) { g.FillRectangle( bearishBrush, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 ); g.DrawRectangle( p, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 ); } else { g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos-addAmount+stickWidth+offset, yPos3 ); } } } } } /// /// Returns an x-axis that is suitable for drawing this plot. /// /// A suitable x-axis. public Axis SuggestXAxis() { CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData ); return candleData.SuggestXAxis(); } /// /// Returns a y-axis that is suitable for drawing this plot. /// /// A suitable y-axis. public Axis SuggestYAxis() { CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData ); return candleData.SuggestYAxis(); } /// /// Gets or sets the data, or column name for the open values. /// public object OpenData { get { return openData_; } set { openData_ = value; } } private object openData_ = null; /// /// Gets or sets the data, or column name for the interval low values. /// public object LowData { get { return lowData_; } set { lowData_ = value; } } private object lowData_ = null; /// /// Gets or sets the data, or column name for the interval high values. /// public object HighData { get { return highData_; } set { highData_ = value; } } private object highData_ = null; /// /// Gets or sets the data, or column name for the close values. /// public object CloseData { get { return closeData_; } set { closeData_ = value; } } private object closeData_ = null; /// /// Gets or sets the data, or column name for the abscissa [x] axis. /// public object AbscissaData { get { return abscissaData_; } set { abscissaData_ = value; } } private object abscissaData_ = null; /// /// Draws a representation of this plot in the legend. /// /// The graphics surface on which to draw. /// A rectangle specifying the bounds of the area in the legend set aside for drawing. public virtual void DrawInLegend( Graphics g, Rectangle startEnd ) { Pen p = new Pen(this.color_); g.DrawLine( p, startEnd.Left, (startEnd.Top + startEnd.Bottom)/2, startEnd.Right, (startEnd.Top + startEnd.Bottom)/2 ); } /// /// Color of this plot [excluding interior of filled boxes if Style is fill]. To /// change the Bullish and Bearish colours in Filled mode, use the BullishColor /// and BearishColor properties. /// public System.Drawing.Color Color { get { return color_; } set { color_ = value; } } Color color_ = Color.Black; /// /// Possible CandleStick styles. /// public enum Styles { /// /// Draw vertical line between low and high, tick on left for open and tick on right for close. /// Stick, /// /// Draw vertical line between low and high and place on top of this a box with bottom /// and top determined by open and high values. The box is filled using the colors specified /// in BullishColor and BearishColor properties. /// Filled } /// /// Specifies the CandleStick style to use. /// public Styles Style = Styles.Filled; /// /// If CandlePlot.Style is Filled, then bullish open-close moves are displayed in this color. /// public Color BullishColor = Color.White; /// /// If CandlePlot.Style is Filled, then bearish moves are displayed in this color. /// public Color BearishColor = Color.Black; /// /// Width of each stick in pixels. It is best if this is an odd number. /// public int StickWidth { get { return stickWidth_; } set { if (value < 1) { throw new NPlotException( "Stick width must be greater than 0." ); } stickWidth_ = value; } } private int stickWidth_ = AutoScaleStickWidth; /// /// If stick width is set equal to this value, the width will be /// automatically scaled dependant on the space between sticks. /// public const int AutoScaleStickWidth = 0; /// /// If true (default), bars will be centered on the abscissa times. /// If false, bars will be drawn between the corresponding abscissa time /// and the next abscissa time. /// /// public bool Centered { get { return centered_; } set { centered_ = value; } } private bool centered_ = true; /// /// Write data associated with the plot as text. /// /// the string builder to write to. /// Only write out data in this region if onlyInRegion is true. /// If true, only data in region is written, else all data is written. /// TODO: not implemented. public void WriteData( System.Text.StringBuilder sb, RectangleD region, bool onlyInRegion ) { } } }