1 module trading.bitfinex;
2 
3 import vibe.d;
4 
5 ///
6 alias TickerResult = float[10];
7 
8 ///
9 @path("/v2/")
10 interface BitfinexPublicAPIv2
11 {
12     ///
13     @method(HTTPMethod.GET)
14     @path("ticker/:symbol")
15     TickerResult ticker(string _symbol);
16 }
17 
18 ///
19 @path("/v1/")
20 interface BitfinexPublicAPI
21 {
22     ///
23     @method(HTTPMethod.GET)
24     @path("symbols")
25     string[] symbols();
26 }
27 
28 ///
29 struct OrderHistoryItem
30 {
31     Json exchange;
32     bool is_cancelled;
33     string avg_execution_price;
34     string timestamp;
35     string price;
36     bool is_live;
37     bool is_hidden;
38     string type;
39     string executed_amount;
40     string src;
41     long cid;
42     string cid_date;
43     long id;
44     string symbol;
45     bool was_forced;
46     string original_amount;
47     string side;
48     Json gid;
49     Json oco_order;
50     string remaining_amount;
51 }
52 
53 ///
54 alias OrderHistoryResult = OrderHistoryItem[];
55 
56 ///
57 struct BFXOrderStatus
58 {
59     string exchange;
60     string avg_execution_price;
61     bool is_live;
62     bool is_cancelled;
63     bool is_hidden;
64     bool was_forced;
65     long id;
66     long cid;
67     string remaining_amount;
68     string executed_amount;
69     string timestamp;
70     string price;
71     string type;
72     string src;
73     string cid_date; //"2018-01-16"
74     string symbol; //"xrpusd"
75     string original_amount;
76     string side;
77     Json gid;
78     Json oco_order;
79 }
80 
81 ///
82 interface BitfinexPrivateAPI
83 {
84     ///
85     Json accountInfos();
86     ///
87     OrderHistoryResult orderHistory();
88     ///
89     BFXOrderStatus newOrder(string symbol, string amount, string price,
90             string side, string ordertype);
91     ///
92     BFXOrderStatus orderStatus(long id);
93 }
94 
95 ///
96 final class Bitfinex : BitfinexPublicAPI, BitfinexPublicAPIv2, BitfinexPrivateAPI
97 {
98     private static immutable API_URL = "https://api.bitfinex.com";
99 
100     private string key;
101     private string secret;
102 
103     private BitfinexPublicAPI publicApi;
104     private BitfinexPublicAPIv2 publicApiV2;
105 
106     ///
107     this(string key, string secret)
108     {
109         this.key = key;
110         this.secret = secret;
111         publicApiV2 = new RestInterfaceClient!BitfinexPublicAPIv2(API_URL);
112         publicApi = new RestInterfaceClient!BitfinexPublicAPI(API_URL);
113     }
114 
115     ///
116     TickerResult ticker(string symbol)
117     {
118         return publicApiV2.ticker(symbol);
119     }
120 
121     ///
122     string[] symbols()
123     {
124         return publicApi.symbols();
125     }
126 
127     unittest
128     {
129         auto api = new Bitfinex("", "");
130         auto res = api.symbols();
131         assert(res.length > 0);
132     }
133 
134     ///
135     Json accountInfos()
136     {
137         static immutable METHOD_URL = "/v1/account_infos";
138 
139         return request!Json(METHOD_URL, Json.emptyObject);
140     }
141 
142     ///
143     OrderHistoryResult orderHistory()
144     {
145         static immutable METHOD_URL = "/v1/orders/hist";
146 
147         return request!OrderHistoryResult(METHOD_URL, Json.emptyObject);
148     }
149 
150     ///
151     BFXOrderStatus newOrder(string symbol, string amount, string price,
152             string side, string ordertype)
153     {
154 
155         static immutable METHOD_URL = "/v1/order/new";
156 
157         Json params = Json.emptyObject;
158         params["symbol"] = symbol;
159         params["amount"] = amount;
160         params["price"] = price;
161         params["exchange"] = "bitfinex";
162         params["side"] = side;
163         params["type"] = ordertype;
164 
165         return request!BFXOrderStatus(METHOD_URL, params);
166     }
167 
168     ///
169     BFXOrderStatus orderStatus(long id)
170     {
171         static immutable METHOD_URL = "/v1/order/status";
172 
173         Json params = Json.emptyObject;
174         params["order_id"] = id;
175 
176         return request!BFXOrderStatus(METHOD_URL, params);
177     }
178 
179     private auto request(T)(string path, Json postData = Json.emptyObject)
180     {
181         import std.digest.sha : SHA384, toHexString, LetterCase;
182         import std.conv : to;
183         import std.base64 : Base64;
184         import std.digest.hmac : hmac;
185         import std.string : representation;
186 
187         postData["request"] = path;
188         postData["nonce"] = Clock.currStdTime().to!string;
189 
190         auto res = requestHTTP(API_URL ~ path, (scope HTTPClientRequest req) {
191 
192             string bodyData = postData.toString;
193             string payload = Base64.encode(cast(ubyte[]) bodyData);
194 
195             //logInfo("payload: %s", payload);
196 
197             string signature = payload.representation.hmac!SHA384(secret.representation)
198                 .toHexString!(LetterCase.lower).idup;
199 
200             req.method = HTTPMethod.POST;
201             req.headers["X-BFX-APIKEY"] = key;
202             req.headers["X-BFX-PAYLOAD"] = payload;
203             req.headers["X-BFX-SIGNATURE"] = signature;
204             req.headers["Content-Type"] = "application/json";
205             req.headers["Content-Length"] = (bodyData.length).to!string;
206 
207             req.bodyWriter.write(bodyData);
208         });
209         scope (exit)
210         {
211             res.dropBody();
212         }
213 
214         if (res.statusCode == 200)
215         {
216             auto json = res.readJson();
217 
218             //logInfo("Response: %s", json);
219 
220             return deserializeJson!T(json);
221         }
222         else
223         {
224             logDebug("API Error: %s", res.bodyReader.readAllUTF8());
225             logError("API Error Code: %s", res.statusCode);
226             throw new Exception("API Error");
227         }
228     }
229 }