仮想通貨の自動売買プログラムを作る過程を紹介しています。
ここまでは自動売買プログラムの画面など、基本的な部分を作ってきましたが、今回からやっと実際の取引をしていきます。
最初に描いたとおり、以下が今回プログラムで実現したいことです。
- 成行で買い注文を入れる
- 一定の儲けが出たら利確して、さらにそこで成行入れる
今回はこの最初の成行注文を入れるところからです。
細かいアルゴリズムについて考える
今回は以下のような感じで処理をするプログラムとします。
- 成行注文を入れる (枚数は変更できるようにする)
- 成行注文が確約した値段を確認する
- 確約した値段 + x で指値注文を入れる (x は後で変えられるようにする)
- 指値注文が通るのを待つ。完了したら再度 1. から繰り返す。
実際のところ、特殊注文の IFD を使えばいくつか省略することもできますが、成行注文だと確約する値段が多少ずれたりして 1 万円プラスで終わった!って思ったのに 9730 円だったりします。(逆に 10300円だったりする場合もあります)
大した問題ではないんですが、せっかく自動売買プログラムを書くのでちゃんと確約した値段+x で売り注文を入れるようにしていきます。
成行注文を入れるための C# コード
では成行注文を入れるためのコードを追加していきます。
成行注文の枚数は後で変えられるようにしますが、とりあえず処理をみるために 0.001 を成行で買うためのコードを追加します。
Lightning API Playground から成り行き注文は POST /v1/me/sendchildorder の命令に以下の BODY を指定することで実現できることを確認。
1 2 3 4 5 6 7 8 9 |
{ "product_code": "FX_BTC_JPY", "child_order_type": "MARKET", "side": "BUY", "price": 0, "size": 0.001, "minute_to_expire": 43200, "time_in_force": "GTC" } |
これの C# のサンプルコードを参考に、自分の自動売買プログラムに変更を加えていく。というかこれはほぼコピペで行ける。
先頭に足りてなかった using System.Security.Cryptography を追加。(API キーを暗号化するために以下が必要)
1 |
using System.Security.Cryptography; |
とりあえずテスト用に残してある Button1 をクリックすると該当コードが実行されるにようにするために、これから追加する関数 MakeMarketOrder() の呼び出しを追加。
1 2 3 4 |
private void button1_Click(object sender, EventArgs e) { MakeMarketOrder(); } |
そして MakeMarketOrder() の定義。ここはほぼコピペ。Console.WriteLine のだった部分を TextBox1 に出力するように変更しただけ。
※最初の 2 行で指定している apiKey と apiSecret は隠すために変更してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
static readonly string apiKey = "xxxxxxxxxxxxxxx"; static readonly string apiSecret = "xxxxxxxxxxxxxxxxxxxx"; //成り行き注文を入れる public async Task MakeMarketOrder() { var method = "POST"; var path = "/v1/me/sendchildorder"; var query = ""; var body = @"{ ""product_code"": ""FX_BTC_JPY"", ""child_order_type"": ""MARKET"", ""side"": ""BUY"", ""price"": 0, ""size"": 0.001, ""minute_to_expire"": 43200, ""time_in_force"": ""GTC"" }"; using (var client = new HttpClient()) using (var request = new HttpRequestMessage(new HttpMethod(method), path + query)) using (var content = new StringContent(body)) { client.BaseAddress = endpointUri; content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); request.Content = content; var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var data = timestamp + method + path + query + body; var hash = SignWithHMACSHA256(data, apiSecret); request.Headers.Add("ACCESS-KEY", apiKey); request.Headers.Add("ACCESS-TIMESTAMP", timestamp); request.Headers.Add("ACCESS-SIGN", hash); var message = await client.SendAsync(request); var response = await message.Content.ReadAsStringAsync(); textBox1.AppendText(response + System.Environment.NewLine); } } static string SignWithHMACSHA256(string data, string secret) { using (var encoder = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { var hash = encoder.ComputeHash(Encoding.UTF8.GetBytes(data)); return ToHexString(hash); } } static string ToHexString(byte[] bytes) { var sb = new StringBuilder(bytes.Length * 2); foreach (var b in bytes) { sb.Append(b.ToString("x2")); } return sb.ToString(); } |
この時点で自動売買プログラムを実行してボタンを押すレスポンスがちゃんと返ってくることを確認。
実際に bitFlyer Lightning にログインして、建玉にも追加されていることを確認。
成行注文の枚数を指定できるようにする
成行注文の枚数を 0.001 から変更できるようにしたいので、フォーム上に成行注文の枚数を指定できる場所と、そこに指定された枚数の成行注文を入れるようにコードを変更していきます。
フォームの編集
これまでやってきたのとツールボックスから必要なコントロールを持ってきて、フォームの見た目を更新します。
枚数を指定するには TextBox より NumericUpDown の方が便利なのでこれを使用。成行注文を入れる Button と Label も追加します。あと、せっかくなので GroupBox も使ってちゃんと枠も作っていきます。
まずは GroupBox をドラッグして、以下のプロパティを変更します。Button1 が真ん中で邪魔だったので端っこにずらしました。(Name のプロパティ値が上の方にあるので、スクショには含まれていません)
- Name: GroupBoxMarketOrder
- Font: MS UI Gothic, 12pt
- Text: 成行注文
この GroupBox の枠の中にラベルとテキストボックスとボタンを配置。
それぞれ以下のプロパティを変更
- Label
- Name: labelMarketOrder
- Text: 注文枚数
- NumericUpDown
- DecimalPlaces: 3
- Increment: 0.001
- Name: numericUpDownMarketOrder
- Value: 0.001
- Button
- Name: buttonMarketOrder
- Text 発注
見た目は後でいくらでも変更できるのでとりあえずはこれで良し。
フォームの内容に合うようにコードを編集
フォームに合わせてコードを以下2つに対応するように変更する。
- Button1 がクリックされた時に呼ばれる関数を buttonMarketOrder にうつす
- 成行注文の枚数を 0.001 固定からフォームに指定されている枚数に変更
1. は簡単。button1_Click で呼んでいる関数を buttonMarketOrder_Click で呼ぶように変更すればいいだけ。ついでに Task の名前を変えておく。(追加したボタンをダブルクリックしたら、buttonMarketOrder_Click の枠が自動的にできるのでそこにコピー)
1 2 3 4 5 6 7 8 9 |
private void button1_Click(object sender, EventArgs e) { //Task Job1 = MakeMarketOrder(); //これを消して } private void buttonMarketOrder_Click(object sender, EventArgs e) { Task JobMarketOrder = MakeMarketOrder(); //ここにコピー } |
2. は以下の body に書いている 0.001 をフォームの値に変更する。
1 2 3 4 5 6 7 8 9 |
var body = @"{ ""product_code"": ""FX_BTC_JPY"", ""child_order_type"": ""MARKET"", ""side"": ""BUY"", ""price"": 0, ""size"": 0.001, ""minute_to_expire"": 43200, ""time_in_force"": ""GTC"" }"; |
これも 0.001 の部分を numericUpDownMarketOrder.Value で指定されている値を持ってくるようにすれば対応できる。
1 2 3 4 5 6 7 8 9 |
var body = @"{ ""product_code"": ""FX_BTC_JPY"", ""child_order_type"": ""MARKET"", ""side"": ""BUY"", ""price"": 0, ""size"": " + numericUpDownMarketOrder.Value + @", ""minute_to_expire"": 43200, ""time_in_force"": ""GTC"" }"; |
関数に汎用性を持たせる
成行注文を MakeMarketOrder の中でレスポンスの処理をするのではなく、MakeMarketOrder 関数の戻り値をレスポンスにして、呼び出し側でレスポンスのハンドリングをするように変更。
レスポンスは STRING 値なので関数の定義方法を以下のように変更。
1 |
public async Task<string> MakeMarketOrder() |
ついでにレスポンスをちゃんと受け取っているかのエラーハンドリングを追加。関数が STRING を返すようになったので、エラーの場合には ERROR_NO_RESPONSE を返す。
1 2 3 4 5 6 |
var response = await message.Content.ReadAsStringAsync(); if (response == "[]") { textBox1.AppendText(" Error: GetOrderInformationWithID() returning ERROR_NO_RESPONSE" + System.Environment.NewLine); return "ERROR_NO_RESPONSE"; } |
エラーがなかったら、そのままレスポンスを返す。
1 |
return response; |
レスポンスの JSON の分解などのハンドリングは呼び出し側に任せる。
自動売買プログラムの作成状況
ひとまず今回は成行注文を入れるところまで。
今の自動売買プログラムの見た目はこんな感じ。
以下がコードの全容。APIKEY と Secret は “xxxxxx” 変更しているので、コピーする場合にはそこを変更するのをお忘れなく。(この時点でコピーする人はいないと思うが・・)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Net.Http; using System.Net.Http.Headers; using Newtonsoft.Json; using System.Security.Cryptography; namespace BtcFxTemp { public partial class Form1 : Form { static readonly Uri endpointUri = new Uri("https://api.bitflyer.jp"); static readonly string apiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; static readonly string apiSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //成り行き注文を入れる public async Task<string> MakeMarketOrder() { var method = "POST"; var path = "/v1/me/sendchildorder"; var query = ""; var body = @"{ ""product_code"": ""FX_BTC_JPY"", ""child_order_type"": ""MARKET"", ""side"": ""BUY"", ""price"": 0, ""size"": " + numericUpDownMarketOrder.Value + @", ""minute_to_expire"": 43200, ""time_in_force"": ""GTC"" }"; using (var client = new HttpClient()) using (var request = new HttpRequestMessage(new HttpMethod(method), path + query)) using (var content = new StringContent(body)) { client.BaseAddress = endpointUri; content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); request.Content = content; var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var data = timestamp + method + path + query + body; var hash = SignWithHMACSHA256(data, apiSecret); request.Headers.Add("ACCESS-KEY", apiKey); request.Headers.Add("ACCESS-TIMESTAMP", timestamp); request.Headers.Add("ACCESS-SIGN", hash); var message = await client.SendAsync(request); var response = await message.Content.ReadAsStringAsync(); if (response == "[]") { textBox1.AppendText(" Error: GetOrderInformationWithID() returning ERROR_NO_RESPONSE" + System.Environment.NewLine); return "ERROR_NO_RESPONSE"; } return response; } } static string SignWithHMACSHA256(string data, string secret) { using (var encoder = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { var hash = encoder.ComputeHash(Encoding.UTF8.GetBytes(data)); return ToHexString(hash); } } static string ToHexString(byte[] bytes) { var sb = new StringBuilder(bytes.Length * 2); foreach (var b in bytes) { sb.Append(b.ToString("x2")); } return sb.ToString(); } ///v1/me/sendchildorder で受け取るレスポンスの型を定義 public class JsonSendChildOrder { public string child_order_acceptance_id; } ///v1/ticker で受け取るレスポンスの型を定義 public class JsonTicker { public string product_code { get; set; } public DateTime timestamp { get; set; } public int tick_id { get; set; } public double best_bid { get; set; } public double best_ask { get; set; } public double best_bid_size { get; set; } public double best_ask_size { get; set; } public double total_bid_depth { get; set; } public double total_ask_depth { get; set; } public double ltp { get; set; } public double volume { get; set; } public double volume_by_product { get; set; } } //採取取引価格を TextBoxPrice に出力する public async Task GetCurrentBtcFxPrice() { while (true) { var method = "GET"; var path = "/v1/ticker"; var query = "?product_code=FX_BTC_JPY"; using (var client = new HttpClient()) using (var request = new HttpRequestMessage(new HttpMethod(method), path + query)) { client.BaseAddress = endpointUri; var message = await client.SendAsync(request); var response = await message.Content.ReadAsStringAsync(); var DesirializedResponse = JsonConvert.DeserializeObject<JsonTicker>(response); var CurrentPrice = String.Format("{0:#,0}", DesirializedResponse.ltp); textBoxPrice.Text = CurrentPrice; } await Task.Delay(1000); } } public Form1() { InitializeComponent(); Task JobUpdatePrice = GetCurrentBtcFxPrice(); } private void button1_Click(object sender, EventArgs e) { } private void buttonMarketOrder_Click(object sender, EventArgs e) { Task job1 = RunThisWhenButtonMarketOrderIsClicked(); } public async Task RunThisWhenButtonMarketOrderIsClicked() { textBox1.AppendText("RunThisWhenButtonMarketOrderIsClicked():IN" + System.Environment.NewLine); //MakeMarketOrder() で成行注文を発行 string ResponseMakeMarketOrder = await MakeMarketOrder(); textBox1.AppendText(ResponseMakeMarketOrder + System.Environment.NewLine); //以下のようなレスポンスの文字列から値だけを取り出す //{ "child_order_acceptance_id":"JRF20180314-102635-135926"} var DesirializedResponse = JsonConvert.DeserializeObject<JsonSendChildOrder>(ResponseMakeMarketOrder); var ChildOrderAcceptanceID = DesirializedResponse.child_order_acceptance_id; //処理をした旨を通知 textBox1.AppendText("--------------------------" + System.Environment.NewLine); textBox1.AppendText("Market Order made: " + numericUpDownMarketOrder.Value + System.Environment.NewLine); textBox1.AppendText(ChildOrderAcceptanceID + System.Environment.NewLine); textBox1.AppendText("--------------------------" + System.Environment.NewLine); } } } |
さて、次は成行注文の確約した値段 + x を売値として注文を入れる処理を追加します。(ほぼ単純な指値注文)