1 module trading.bitstamp; 2 3 import vibe.d; 4 5 static struct BS 6 { 7 /// 8 struct TickerResult 9 { 10 string timestamp; 11 string last; 12 string open; 13 string volume; 14 string low; 15 string vwap; 16 string high; 17 string ask; 18 string bid; 19 } 20 21 /// 22 struct OrderStatus 23 { 24 /// 25 struct Transaction 26 { 27 /// 28 string xrp; 29 /// 30 string price; 31 /// 32 string fee; 33 /// 34 int type; 35 /// 36 int tid; 37 /// 38 string usd; 39 /// 40 string datetime; 41 } 42 43 /// 44 string status; 45 /// 46 Transaction[] transactions; 47 48 /// 49 float priceAverage() const 50 { 51 float avg = 0; 52 53 foreach(t; transactions) 54 avg += t.price.to!float; 55 56 return avg / cast(float)transactions.length; 57 } 58 59 unittest 60 { 61 OrderStatus st; 62 Transaction t; 63 t.price = "1.0"; 64 st.transactions ~= t; 65 t.price = "2.0"; 66 st.transactions ~= t; 67 t.price = "3.0"; 68 st.transactions ~= t; 69 70 assert(st.transactions.length == 3); 71 assert(st.priceAverage == 2.0f); 72 } 73 74 /// 75 bool isStatusFinished() const { return this.status == "Finished"; } 76 /// 77 bool isStatusOpen() const { return this.status == "Open"; } 78 /// 79 bool isStatusInQueue() const { return this.status == "In Queue"; } 80 } 81 82 /// 83 struct Transaction 84 { 85 /// 86 float xrp_usd; 87 /// 88 float btc; 89 /// 90 string xrp; 91 /// 92 float eur; 93 /// 94 string fee; 95 /// 96 string type; 97 /// 98 int order_id; 99 /// 100 string usd; 101 /// 102 int id; 103 /// 104 string datetime; 105 } 106 107 alias Transactions = Transaction[]; 108 109 /// 110 struct Order 111 { 112 /// 113 string price; 114 /// 115 string amount; 116 /// 117 string type; 118 /// 119 string datetime; 120 /// 121 string id; 122 } 123 124 /// 125 struct Balance 126 { 127 @optional string xrp_balance; 128 @optional string xrp_reserved; 129 @optional string xrp_available; 130 131 @optional string usd_balance; 132 @optional string usd_reserved; 133 @optional string usd_available; 134 135 @optional string eur_balance; 136 @optional string eur_reserved; 137 @optional string eur_available; 138 139 @optional string btc_balance; 140 @optional string btc_reserved; 141 @optional string btc_available; 142 143 float fee; 144 } 145 } 146 147 /// 148 @path("/v2/") 149 interface BitstampPublicAPI 150 { 151 /// 152 @method(HTTPMethod.GET) 153 @path("ticker/:pair") 154 BS.TickerResult ticker(string _pair); 155 } 156 157 /// 158 @path("/v2/") 159 interface BitstampPrivateAPI 160 { 161 /// 162 BS.OrderStatus orderStatus(int order_id); 163 /// 164 BS.Transactions transactions(string pair); 165 /// 166 BS.Order sellMarket(string pair, float amount); 167 /// 168 BS.Order buyMarket(string pair, float amount); 169 /// 170 BS.Balance balance(string pair); 171 } 172 173 /// 174 final class Bitstamp : BitstampPublicAPI, BitstampPrivateAPI 175 { 176 private static immutable API_URL = "https://www.bitstamp.net/api"; 177 178 private string customerId; 179 private string key; 180 private string secret; 181 182 private BitstampPublicAPI publicApi; 183 184 /// 185 this(string customerId, string key, string secret) 186 { 187 this.customerId = customerId; 188 this.key = key; 189 this.secret = secret; 190 this.publicApi = new RestInterfaceClient!BitstampPublicAPI(API_URL); 191 } 192 193 /// 194 BS.TickerResult ticker(string pair) 195 { 196 return publicApi.ticker(pair); 197 } 198 199 unittest 200 { 201 auto api = new Bitstamp("", "", ""); 202 auto res = api.ticker("xrpusd"); 203 assert(res.last != ""); 204 } 205 206 /// 207 BS.OrderStatus orderStatus(int order_id) 208 { 209 static immutable METHOD_URL = "/order_status/"; 210 211 string[string] params; 212 params["id"] = to!string(order_id); 213 214 return request!(BS.OrderStatus)(METHOD_URL, params); 215 } 216 217 /// 218 BS.Transactions transactions(string pair) 219 { 220 static immutable METHOD_URL = "/v2/user_transactions/"; 221 222 string[string] params; 223 params["limit"] = "1000"; 224 225 return request!(BS.Transactions)(METHOD_URL ~ pair ~ "/", params); 226 } 227 228 /// 229 BS.Order buyMarket(string pair, float amount) 230 { 231 static immutable METHOD_URL = "/v2/buy/market/"; 232 233 string[string] params; 234 params["amount"] = to!string(amount); 235 236 return request!(BS.Order)(METHOD_URL ~ pair ~ "/", params); 237 } 238 239 /// 240 BS.Order sellMarket(string pair, float amount) 241 { 242 static immutable METHOD_URL = "/v2/sell/market/"; 243 244 string[string] params; 245 params["amount"] = to!string(amount); 246 247 return request!(BS.Order)(METHOD_URL ~ pair ~ "/", params); 248 } 249 250 /// 251 BS.Balance balance(string pair) 252 { 253 static immutable METHOD_URL = "/v2/balance/"; 254 255 string[string] params; 256 257 return request!(BS.Balance)(METHOD_URL ~ pair ~ "/", params); 258 } 259 260 private auto request(T)(string path, string[string] params) 261 { 262 import std.digest.sha : SHA256, toHexString, LetterCase; 263 import std.conv : to; 264 import std.digest.hmac : hmac; 265 import std..string : representation; 266 import std.array : Appender; 267 268 auto res = requestHTTP(API_URL ~ path, (scope HTTPClientRequest req) { 269 270 string nonce = Clock.currStdTime().to!string; 271 string payload = nonce ~ this.customerId ~ this.key; 272 273 string signature = payload.representation.hmac!SHA256(secret.representation) 274 .toHexString!(LetterCase.upper).idup; 275 276 params["nonce"] = nonce; 277 params["key"] = this.key; 278 params["signature"] = signature; 279 280 Appender!string app; 281 app.formEncode(params); 282 283 string bodyData = app.data; 284 285 req.method = HTTPMethod.POST; 286 req.headers["Content-Type"] = "application/x-www-form-urlencoded"; 287 req.headers["Content-Length"] = (app.data.length).to!string; 288 289 req.bodyWriter.write(app.data); 290 }); 291 scope (exit) 292 { 293 res.dropBody(); 294 } 295 296 if (res.statusCode == 200) 297 { 298 auto json = res.readJson(); 299 300 scope(failure) 301 { 302 logError("Response deserialize failed: %s", json); 303 } 304 305 return deserializeJson!T(json); 306 } 307 else 308 { 309 logDebug("API Error: %s", res.bodyReader.readAllUTF8()); 310 logError("API Error Code: %s", res.statusCode); 311 throw new Exception("API Error"); 312 } 313 } 314 }