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 }