1 module trading.kraken;
2 
3 import vibe.d;
4 
5 ///
6 struct BalanceResult
7 {
8 	///
9 	@optional string[string] result;
10 
11 	///
12 	@optional Json error;
13 }
14 
15 ///
16 enum OrderStatus
17 {
18 	pending, // = order pending book entry
19 	open, // = open order
20 	closed, // = closed order
21 	canceled, // = order canceled
22 	expired, // = order expired
23 }
24 
25 ///
26 enum OrderSide
27 {
28 	buy,
29 	sell
30 }
31 
32 unittest
33 {
34 	assert(to!string(OrderSide.buy) == "buy");
35 }
36 
37 ///
38 struct OrderDesc
39 {
40 	string pair;
41 	@byName OrderSide type;
42 	string ordertype;
43 	string price;
44 	string price2;
45 	string leverage;
46 	string order;
47 	@optional string close;
48 }
49 
50 ///
51 struct OrderInfo
52 {
53 	@optional int userref;
54 	float opentm;
55 	float starttm;
56 	float expiretm;
57 	@optional float closetm;
58 	string vol;
59 	string vol_exec;
60 	OrderDesc descr;
61 	string fee;
62 	string price;
63 	@optional string refid;
64 	string oflags;
65 	@byName OrderStatus status;
66 	string misc;
67 	@optional string reason;
68 	string cost;
69 }
70 
71 ///
72 struct QueryOrdersResult
73 {
74 	///
75 	@optional OrderInfo[string] result;
76 
77 	///
78 	@optional Json error;
79 }
80 
81 ///
82 struct AddOrderResult
83 {
84 	///
85 	struct Result
86 	{
87 		///
88 		Json descr;
89 		///
90 		string[] txid;
91 	}
92 
93 	///
94 	@optional Result result;
95 
96 	///
97 	@optional Json error;
98 }
99 
100 ///
101 struct TickerInfo
102 {
103 	/// current (last trade)
104 	string[] c;
105 	/// bid price
106 	string[] b;
107 	/// ask price
108 	string[] a;
109 	/// low
110 	string[] l;
111 	/// high
112 	string[] h;
113 	/// volume
114 	string[2] v;
115 }
116 
117 ///
118 struct TickerResult
119 {
120 	///
121 	TickerInfo[string] result;
122 }
123 
124 ///
125 struct OrderBook
126 {
127 	///
128 	Json[] asks;
129 	///
130 	Json[] bids;
131 }
132 
133 ///
134 struct OrderBookResult
135 {
136 	///
137 	OrderBook[string] result;
138 }
139 
140 ///
141 interface KrakenAPI
142 {
143 	///
144 	TickerResult Ticker(string pair);
145 
146 	///
147 	OrderBookResult OrderBook(string pair);
148 
149 	///
150 	BalanceResult Balance();
151 
152 	///
153 	QueryOrdersResult QueryOrders(string txid);
154 
155 	///
156 	AddOrderResult AddOrder(string pair, OrderSide type, string ordertype,
157 			string price, string volume);
158 }
159 
160 ///
161 final class Kraken : KrakenAPI
162 {
163 	static immutable API_URL = "https://api.kraken.com";
164 
165 	private string key;
166 	private string secret;
167 
168 	this(string key, string secret)
169 	{
170 		this.key = key;
171 		this.secret = secret;
172 	}
173 
174 	TickerResult Ticker(string pair)
175 	{
176 		static immutable METHOD_URL = "/0/public/Ticker";
177 
178 		Json params = Json.emptyObject;
179 		params["pair"] = pair;
180 
181 		return request!TickerResult(METHOD_URL, params);
182 	}
183 
184 	unittest
185 	{
186 		auto api = new Kraken("", "");
187 		auto res = api.Ticker("XRPUSD");
188 		assert(res.result.length > 0);
189 	}
190 
191 	OrderBookResult OrderBook(string pair)
192 	{
193 		static immutable METHOD_URL = "/0/public/Depth";
194 
195 		Json params = Json.emptyObject;
196 		params["pair"] = pair;
197 
198 		return request!OrderBookResult(METHOD_URL, params);
199 	}
200 
201 	BalanceResult Balance()
202 	{
203 		static immutable METHOD_URL = "/0/private/Balance";
204 
205 		return request!BalanceResult(METHOD_URL);
206 	}
207 
208 	QueryOrdersResult QueryOrders(string txid)
209 	{
210 		static immutable METHOD_URL = "/0/private/QueryOrders";
211 
212 		Json params = Json.emptyObject;
213 		params["txid"] = txid;
214 
215 		return request!QueryOrdersResult(METHOD_URL, params);
216 	}
217 
218 	AddOrderResult AddOrder(string pair, OrderSide type, string ordertype,
219 			string price, string volume)
220 	{
221 		static immutable METHOD_URL = "/0/private/AddOrder";
222 
223 		Json params = Json.emptyObject;
224 		params["pair"] = pair;
225 		params["type"] = to!string(type);
226 		params["ordertype"] = ordertype;
227 		params["volume"] = volume;
228 		params["trading_agreement"] = "agree";
229 		if (price.length > 0)
230 			params["price"] = price;
231 
232 		return request!AddOrderResult(METHOD_URL, params);
233 	}
234 
235 	private auto request(T)(string path, Json postData = Json.emptyObject)
236 	{
237 		import std.digest.sha : sha256Of, SHA512;
238 		import std.conv : to;
239 		import std.base64 : Base64;
240 		import std.digest.hmac : hmac;
241 
242 		auto nonce = Clock.currStdTime();
243 
244 		postData["nonce"] = nonce;
245 
246 		auto res = requestHTTP(API_URL ~ path, (scope HTTPClientRequest req) {
247 
248 			string payload = postData.toString;
249 
250 			auto nonceAndData = nonce.to!string ~ payload;
251 
252 			//logInfo("payload: %s",payload);
253 
254 			auto signature = (path.representation ~ sha256Of(nonceAndData)).hmac!SHA512(
255 				Base64.decode(secret));
256 
257 			req.method = HTTPMethod.POST;
258 			req.headers["API-Key"] = key;
259 			req.headers["API-Sign"] = Base64.encode(signature);
260 			req.headers["Content-Type"] = "application/json";
261 			req.headers["Content-Length"] = payload.length.to!string;
262 
263 			req.bodyWriter.write(payload);
264 		});
265 		scope (exit)
266 		{
267 			res.dropBody();
268 		}
269 
270 		if (res.statusCode == 200)
271 		{
272 			auto json = res.readJson();
273 
274 			//logInfo("Response: %s", json);
275 
276 			return deserializeJson!T(json);
277 		}
278 		else
279 		{
280 			logDebug("API Error: %s", res.bodyReader.readAllUTF8());
281 			logError("API Error Code: %s", res.statusCode);
282 			throw new Exception("API Error");
283 		}
284 	}
285 }