ろうそく足チャート - peace098beat/windows_applicaciton GitHub Wiki

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;



namespace PloniexApi.Net.FormDemo
{

    /// <summary>
    /// ろうそく足チャート
    /// </summary>
    class CandleChart : Panel
    {
        /// <summary>
        /// チャートデータ
        /// </summary>
        public List<MyMarketChartData> markets = null;

        // マウスドラッグ
        private bool fDrag = false;

        /*************************************************************************
         *
         * 描画関連メンバ変数
         * 
         *************************************************************************/
        #region 
        //ろうそく足の本数
        const int NDATA = 500;

        // サイズ
        static Padding mrgn = new Padding(50, 30, 50, 80); // Chart1のグラフマージン

        // サイズ
        const int HEIGHT2 = 45; // Chart2の高さ(リサイズにより変わらない)
        const int MARGIN2 = 20; // Chart1とChart2のグラフの隙間

        // マージン
        int horzMrgn = mrgn.Left + mrgn.Right;
        int vertMrgn = mrgn.Top + mrgn.Bottom;

        // テキスト描画のプロパティ
        TextFormatFlags mid = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter; // センター寄オプション
        TextFormatFlags left = TextFormatFlags.Left | TextFormatFlags.VerticalCenter; // センター寄オプション

        // フォント
        Font font = new Font("Arial", 9);
        Font font2 = new Font("Arial", 7);

        // ペン
        Pen penRed = new Pen(Color.Red, 2);
        Pen penBlue = new Pen(Color.Blue, 2);
        Pen penGrayDot = new Pen(Color.Gray, 1) { DashStyle = DashStyle.Dot }; // 罫線点線

        int nBgn; // 表示する先頭レコード番号 [0, data.Length-NDATA]
        int xOffset = 0;
        double vMax, vMin;
        double[] Tick = {1e-8, 5e-8, 1e-7, 5e-7, 1e-6, 5e-6, 1e-5, 5e-5,
                         1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2, 1e-1, 5e-1, 1, 2, 5, 10, 20, 50, 100, 200, 500, 10000, 20000, 50000 };

        Rectangle rectGraph; // Chart1とChart2をあわせたRectangle
        #endregion

        /*************************************************************************
        *
        * 座標変換
        * 
        *************************************************************************/

        /// <summary> グラフ座標からウィンドウ座標へ変換 </summary>
        /// <param name="x">グラフ座標上の正規化された位置(0~1)</param>
        /// <returns></returns>
        int nx(double x)
        {
            return (int)(rectGraph.Left + x * rectGraph.Width);
        }

        /// <summary>グラフ座標からウィンドウ座標へ変換</summary>
        /// <param name="y">グラフ座標上の正規化された位置(0~1)</param>
        /// <returns></returns>
        int ny(double y)
        {
            return (int)(rectGraph.Top + (1.0 - y) * rectGraph.Height);
        }

        /// <summary>グラフ座標からウィンドウ座標へ変換</summary>
        /// <param name="y">グラフ座標上の正規化された位置(0~1)</param>
        /// <returns></returns>
        int ny2(double y)
        {
            return ny(0) + MARGIN2 + (int)((1.0 - y) * HEIGHT2);
        }


        // *************************************************************************//

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public CandleChart()
        {
            this.SetStyle(ControlStyles.ResizeRedraw, true);
            this.DoubleBuffered = true; // ダブルバッファリング: ちらつき防止
            this.Size = new Size(600, 400);

            this.MouseMove += new MouseEventHandler(mouseMove);
            this.MouseDown += new MouseEventHandler(mouseDown);
            this.MouseUp += new MouseEventHandler(mouseUp);
        }

        /// <summary>
        /// チャートデータのセット
        /// </summary>
        /// <param name="_markets"></param>
        public void SetMarketChartData(List<MyMarketChartData> _markets)
        {
            this.markets = _markets;

            // 最大値,最小値検索
            foreach (var chartData in markets)
            {
                this.vMax = Math.Max(vMax, chartData.Close);
                this.vMin = Math.Min(vMin, chartData.Close);
            }

            // 先頭番号(描画するNDATA分ずらしておく)
            nBgn = markets.Count - NDATA;
        }


        /*************************************************************************
         *
         * マウスイベント
         * 
         *************************************************************************/
        private void mouseMove(object sender, MouseEventArgs e)
        {
            // nBgnの更新
            if (fDrag && (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left)
            {
                // マウスの位置からnBgnを決める
                this.nBgn = (int)((double)(e.X - rectGraph.Left - xOffset) / rectGraph.Width * markets.Count);

                // Clilp : 0 =< nBgn <= markets.Counet-NDATAに収める
                this.nBgn = Math.Min(nBgn, markets.Count - NDATA);
                this.nBgn = Math.Max(0, nBgn);

                this.Refresh();
            }

            // nBgnの更新
            if ((Control.MouseButtons & MouseButtons.Right) == MouseButtons.Right)
            {
                if (ny2(1) < e.Y && e.Y < ny2(0) && nx(0) < e.X && e.X < nx(1))
                {

                    // マウスの位置からnBgnを決める
                    this.nBgn = (int)((double)(e.X - rectGraph.Left) / rectGraph.Width * markets.Count);

                    // Clilp : 0 =< nBgn <= markets.Counet-NDATAに収める
                    this.nBgn = Math.Min(nBgn, markets.Count - NDATA);
                    this.nBgn = Math.Max(0, nBgn);

                    this.Refresh();
                }
            }
        }

        private void mouseDown(object sender, MouseEventArgs e)
        {
            // 選択されている開始・終了位置
            double xBgn = (double)nBgn / markets.Count; // 正規化
            double xEnd = (double)(nBgn + NDATA) / markets.Count; // 正規化

            // マウス位置がChart2の選択範囲に入っているか?
            if (ny2(1) < e.Y && e.Y < ny2(0) && nx(xBgn) < e.X && e.X < nx(xEnd))
            {
                fDrag = true; // ドラッグ開始
                xOffset = e.X - nx(xBgn); // ドラッグ開始時の選択範囲内でのマウス位置
                this.Refresh();
            }

            // nBgnの更新
            if ((Control.MouseButtons & MouseButtons.Right) == MouseButtons.Right)
            {
                if (ny2(1) < e.Y && e.Y < ny2(0) && nx(0) < e.X && e.X < nx(1))
                {

                    // マウスの位置からnBgnを決める
                    this.nBgn = (int)((double)(e.X - rectGraph.Left) / rectGraph.Width * markets.Count);

                    // Clilp : 0 =< nBgn <= markets.Counet-NDATAに収める
                    this.nBgn = Math.Min(nBgn, markets.Count - NDATA);
                    this.nBgn = Math.Max(0, nBgn);

                    this.Refresh();
                }
            }
        }

        private void mouseUp(object sender, MouseEventArgs e)
        {
            if (fDrag)
            {
                fDrag = false;
                this.Refresh();
            }
        }


        /*************************************************************************
         *
         * 再描画メソッド
         * 
         *************************************************************************/
        // 再描画メソッド
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            // もしデータが無い場合は処理無し
            if (this.markets == null) return;

            // グラフィックス
            Graphics g = e.Graphics;

            // 定数
            double max = double.MinValue, min = double.MaxValue; // 現在の表示区間での最大最小

            // 開始点?


            // グラフ(2つあわせた)サイズ ※リサイズのたびに作り直される
            rectGraph = new Rectangle(mrgn.Left, mrgn.Top, ClientSize.Width - horzMrgn, ClientSize.Height - vertMrgn);


            // -- タイトル用Rectangle
            Rectangle RectTitle = new Rectangle(nx(0), ny(1) - 20, nx(1) - nx(0), 14);
            string title = "fichart";
            TextRenderer.DrawText(g, title, font, RectTitle, Color.Black, mid);

            // 現在の表示区間での最大最小
            for (int n = 0; n < NDATA; n++)
            {
                max = Math.Max(max, markets[nBgn + n].High);
                min = Math.Min(min, markets[nBgn + n].Low);
            }

            // チャート1の外枠を表示
            Rectangle RCChart1 = new Rectangle(nx(0), ny(1), nx(1) - nx(0), ny(0) - ny(1));
            g.DrawRectangle(Pens.Black, RCChart1);

            Console.WriteLine($"nBgn:{nBgn},nEnd:{nBgn + NDATA}, Count:{markets.Count}, ");

            #region Chart1 X軸罫線
            // --  x軸罫線,目盛り --
            int XTickWidth = 100;
            int XTickOffset = XTickWidth / 2;
            int XTickHeight = 18;

            int XTickNum = 5;
            int XTickInterval = NDATA / XTickNum;

            for (int n = 0; n < NDATA; n++)
            {
                double x = (n + 0.5) / NDATA; // 区間の中間に目盛り

                if ((n % XTickInterval) == 0 || n == NDATA - 1)
                {
                    if (n > 0 && n < NDATA - 1)
                    {
                        // 縦目盛り軸
                        g.DrawLine(penGrayDot, nx(x), ny(0), nx(x), ny(1));
                    }
                    Rectangle RC_DateText = new Rectangle(nx(x) - XTickOffset, ny(0), XTickWidth, XTickHeight);
                    string dateString = markets[nBgn + n].Time.ToShortDateString();
                    TextRenderer.DrawText(g, dateString, font, RC_DateText, Color.Black, mid);
                }
            }

            #endregion


            // -- Y軸罫線,目盛り --

            // -- Ticks --
            double tick = 0; // 一目盛りの大きさ
            //for (int i = 0; Tick[i]  < (max - min); i++)
            //{
            //    tick = Tick[i];
            //}

            for (int i = 0; i < Tick.Length; i++)
            {
                double _tick = Tick[i];
                if (7 * _tick < (max - min))
                {
                    tick = _tick;
                }
                else
                {
                    continue;
                }
            }

            // Tick単位の大きさに変更
            max = Math.Ceiling(max / tick) * tick; // 大きい側を使用
            min = Math.Floor(min / tick) * tick; // 小さい側を使用

            // Y軸の目盛りの数
            int numTicks = (int)((max - min) / tick);

            #region Y軸罫線
            // -- Y軸罫線,目盛り --
            int nChars = (int)Math.Log10(max) + 1; // 桁数 (あやしい)

            for (int n = 0; n <= numTicks; n++)
            {
                double y = (double)n / numTicks; // 正規化
                if (n > 0 && n <= numTicks)
                {
                    //Y軸の罫線
                    g.DrawLine(penGrayDot, nx(0), ny(y), nx(1), ny(y));
                }
                // Y軸の目盛り文字
                RectTitle = new Rectangle(nx(1), ny(y) - (n > 0 ? 10 : 14), 50, 20);
                string num = (min + n * tick).ToString();
                //string num = (min + n * tick).ToString().PadLeft(nChars);
                TextRenderer.DrawText(g, num, font, RectTitle, Color.Black, left);
            }
            #endregion

            #region ろうそく足
            // -- ろうそく足
            for (int n = 1; n < NDATA; n++)
            {
                double xCenter = (n + 0.5) / NDATA; // 区間の中央

                double yLow = (markets[n + nBgn].Low - min) / (max - min);
                double yHigh = (markets[n + nBgn].High - min) / (max - min);
                double yOpen = (markets[n + nBgn].Open - min) / (max - min);
                double yClose = (markets[n + nBgn].Close - min) / (max - min);

                int top = ny(Math.Max(yOpen, yClose)); // 高値
                int bottom = ny(Math.Min(yOpen, yClose)); // 低値

                // もし同じの場合は1pixずらしておく.
                if (bottom == top) bottom++;

                // ろうそくの色を選択
                Brush brush = (yClose > yOpen) ? Brushes.Red : Brushes.Blue;
                Pen pen = (yClose > yOpen) ? Pens.Red : Pens.Blue;

                // ボックス
                int BoxWidth = 4;
                int BoxHarfWidth = 2;
                int BoxHeight = bottom - top;
                // ボックス表示
                g.FillRectangle(brush, nx(xCenter) - BoxHarfWidth, top, BoxWidth, BoxHeight);

                // 足
                g.DrawLine(pen, nx(xCenter), ny(yLow), nx(xCenter), ny(yHigh));

            }
            #endregion

            #region 移動平均
            Pen penGreenMA = new Pen(Color.Green, 2);
            for (int n = 1; n < NDATA; n++)
            {
                int index = n + nBgn;

                double y1 = (markets[index - 1].WeightedAverage - min) / (max - min);
                double y2 = (markets[index].WeightedAverage - min) / (max - min);

                double x1 = (double)(n - 1) / NDATA;
                double x2 = (double)n / NDATA;

                Point pt1 = new Point(nx(x1), ny(y1));
                Point pt2 = new Point(nx(x2), ny(y2));

                g.DrawLine(penGreenMA, pt1, pt2);

            }
            #endregion


            guide(g);



        }


        void fillPolygon(Graphics g, int bgn, int end, Brush brush)
        {
            int n = bgn;
            double x = 0, y;
            double xBgn = (double)bgn / markets.Count;

            Point[] pt = new Point[end - bgn + 3];
            pt[0] = new Point(nx(xBgn) + 1, ny2(0));  // 原点
            while (n < end)
            {
                x = (n + 0.5) / markets.Count;
                y = markets[n].Close / vMax;
                pt[++n - bgn] = new Point(nx(x) + 1, ny2(y)); // 折れ線グラフに相当
            }
            pt[1 + n - bgn] = new Point(nx(x) + 1, ny2(0));
            pt[2 + n - bgn] = new Point(nx(xBgn) + 1, ny2(0));
            g.FillPolygon(brush, pt);

        }

        // 画面下のガイド
        void guide(Graphics g)
        {
            // -- 画面下のガイド用
            Rectangle RCChart2 = new Rectangle(nx(0), ny2(1), nx(1) - nx(0), ny2(0) - ny2(1));

            g.DrawRectangle(Pens.Black, RCChart2);

            fillPolygon(g, 0, markets.Count, Brushes.Gray);
            fillPolygon(g, nBgn, nBgn + NDATA, Brushes.Pink);


            // 目盛り文字
            string month = null;
            for (int n = 0; n < markets.Count; n++)
            {

                double x = (double)n / markets.Count;

                if (markets[n].Time.Month.ToString() != month)
                {
                    month = markets[n].Time.Month.ToString();

                    if (int.Parse(month) % 3 == 0) // 間引き
                    {
                        int text_width = 80;
                        int text_height = 18;
                        int offset_x = text_width / 2;
                        int offset_y = -0;
                        Rectangle RCChart2Text = new Rectangle(nx(x) - offset_x, ny2(0) - offset_y, text_width, text_height);
                        string text = $"{markets[n].Time.Year}.{markets[n].Time.Month}";
                        TextRenderer.DrawText(g, text, font2, RCChart2Text, Color.Black, Color.White, mid);
                    }
                }
            }

        }
    }



    /// <summary>
    /// チャートデータ
    /// </summary>
    class MyMarketChartData
    {
        public DateTime Time;
        public double Open;
        public double Close;
        public double High;
        public double Low;
        public double VolumeBase;
        public double VolumeQuote;
        public double WeightedAverage;


        public MyMarketChartData(DateTime time,
            double open, double close, double high, double low,
            double volumebase, double volumequote, double weitedaverage)
        {
            this.Time = time;
            this.Open = open;
            this.Close = close;
            this.High = high;
            this.Low = low;
            this.VolumeBase = volumebase;
            this.VolumeQuote = volumequote;
            this.WeightedAverage = weitedaverage;
        }
    }
}

#コントロールフォーム例

using Jojatekok.PoloniexAPI;
using Jojatekok.PoloniexAPI.MarketTools;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace PloniexApi.Net.FormDemo
{
    public partial class Form4 : Form
    {
        private PoloniexClient PloniexClient { get; set; }


        public Form4()
        {
            InitializeComponent();

            PloniexClient = new PoloniexClient(ApiKeys.PublicKey, ApiKeys.PrivateKey);

            string pair1 = "BTC"; 
            string pair2 = "ETH";
            CurrencyPair pair = new CurrencyPair(pair1, pair2);

            string backuppath = $"CharData_{pair.ToString()}.bit";

            if (System.IO.File.Exists(backuppath) == false)
            {
                var task = LoadMarketSummaryAsync();
            }

            var markets = (IList<IMarketChartData>)FiBinaryWriter.LoadFromBinaryFile(backuppath);

            List < MyMarketChartData > chartData = new List<MyMarketChartData>();

            for (int i = 0; i < markets.Count; i++)
            {
                var _ChartData = new MyMarketChartData(
                        markets[i].Time,
                        markets[i].Open,
                        markets[i].Close,
                        markets[i].High,
                        markets[i].Low,
                        markets[i].VolumeBase,
                        markets[i].VolumeQuote,
                        markets[i].WeightedAverage);
                chartData.Add(_ChartData);
            }

            candleChart1.SetMarketChartData(chartData);


        }

        /// <summary>
        /// WebAPIにてチャートデータ取得
        /// </summary>
        /// <returns></returns>
        private async Task LoadMarketSummaryAsync()
        {

            string pair1 = "BTC";
            string pair2 = "ETH";
            CurrencyPair pair = new CurrencyPair(pair1, pair2);

            var markets = await PloniexClient.Markets.GetChartDataAsync(
                                                                    currencyPair: pair,
                                                                    period: MarketPeriod.Minutes15,
                                                                    startTime: new DateTime(2010, 5, 12, 20, 30, 15, 123),
                                                                    endTime: new DateTime(2017, 5, 31, 20, 30, 15, 123)
                                                                    );

            markets.RemoveAt(0);

            string backuppath = $"CharData_{pair.ToString()}.bit";

            FiBinaryWriter.SaveToBinaryFile(markets, backuppath);
        }

    }
}
⚠️ **GitHub.com Fallback** ⚠️