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     ///
128     Json accountInfos()
129     {
130         static immutable METHOD_URL = "/v1/account_infos";
131 
132         return request!Json(METHOD_URL, Json.emptyObject);
133     }
134 
135     ///
136     OrderHistoryResult orderHistory()
137     {
138         static immutable METHOD_URL = "/v1/orders/hist";
139 
140         return request!OrderHistoryResult(METHOD_URL, Json.emptyObject);
141     }
142 
143     ///
144     BFXOrderStatus newOrder(string symbol, string amount, string price,
145             string side, string ordertype)
146     {
147 
148         static immutable METHOD_URL = "/v1/order/new";
149 
150         Json params = Json.emptyObject;
151         params["symbol"] = symbol;
152         params["amount"] = amount;
153         params["price"] = price;
154         params["exchange"] = "bitfinex";
155         params["side"] = side;
156         params["type"] = ordertype;
157 
158         return request!BFXOrderStatus(METHOD_URL, params);
159     }
160 
161     ///
162     BFXOrderStatus orderStatus(long id)
163     {
164         static immutable METHOD_URL = "/v1/order/status";
165 
166         Json params = Json.emptyObject;
167         params["order_id"] = id;
168 
169         return request!BFXOrderStatus(METHOD_URL, params);
170     }
171 
172     private auto request(T)(string path, Json postData = Json.emptyObject)
173     {
174         import std.digest.sha : SHA384, toHexString, LetterCase;
175         import std.conv : to;
176         import std.base64 : Base64;
177         import std.digest.hmac : hmac;
178         import std.string : representation;
179 
180         postData["request"] = path;
181         postData["nonce"] = Clock.currStdTime().to!string;
182 
183         auto res = requestHTTP(API_URL ~ path, (scope HTTPClientRequest req) {
184 
185             string bodyData = postData.toString;
186             string payload = Base64.encode(cast(ubyte[]) bodyData);
187 
188             //logInfo("payload: %s", payload);
189 
190             string signature = payload.representation.hmac!SHA384(secret.representation)
191                 .toHexString!(LetterCase.lower).idup;
192 
193             req.method = HTTPMethod.POST;
194             req.headers["X-BFX-APIKEY"] = key;
195             req.headers["X-BFX-PAYLOAD"] = payload;
196             req.headers["X-BFX-SIGNATURE"] = signature;
197             req.headers["Content-Type"] = "application/json";
198             req.headers["Content-Length"] = (bodyData.length).to!string;
199 
200             req.bodyWriter.write(bodyData);
201         });
202         scope (exit)
203         {
204             res.dropBody();
205         }
206 
207         if (res.statusCode == 200)
208         {
209             auto json = res.readJson();
210 
211             //logInfo("Response: %s", json);
212 
213             return deserializeJson!T(json);
214         }
215         else
216         {
217             logDebug("API Error: %s", res.bodyReader.readAllUTF8());
218             logError("API Error Code: %s", res.statusCode);
219             throw new Exception("API Error");
220         }
221     }
222 }