/
trail.go
326 lines (294 loc) · 9.79 KB
/
trail.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
package main
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"sync"
)
type Darwin struct {
Name string `json:"name"`
TrailingSL string `json:"trailingSL"`
}
type Config struct {
AuthToken string `json:"authtoken"`
ConsumerKey string `json:"consumerkey"`
ConsumerSecret string `json:"consumersecret"`
RefreshToken string `json:"refreshtoken"`
InvestorID int `json:"investorid"`
Darwins []Darwin `json:"darwins"`
}
type InvestorAccount struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Threshold struct {
Type string `json:"type"`
OrderID int `json:"orderId"`
Amount float32 `json:"amount"`
Quote float32 `json:"quote"`
}
type CurrentPosition struct {
Pname string `json:"productname"`
Thresholds []Threshold `json:"thresholds"`
Cquote float32 `json:"currentquote"`
}
// Send GET request and check authn token
func sendGet(url string, conf Config) string {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
os.Exit(1)
}
req.Header.Set("Authorization", "Bearer "+conf.AuthToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending GET request:", err)
os.Exit(1)
}
defer resp.Body.Close()
var ret string
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
ret = string(bodyBytes)
} else if resp.StatusCode == http.StatusUnauthorized {
ret = "unauthorized"
} else {
ret = "unknown"
}
return ret
}
func saveondisk(conf Config, f string, debug bool) {
// Save the new config on disk
file, err := json.MarshalIndent(conf, "", " ")
if err != nil {
fmt.Println("Error marshalling JSON:", err)
os.Exit(1)
}
err = os.WriteFile(f, file, 0600)
if err != nil {
fmt.Println("Error writing to file:", err)
os.Exit(1)
}
if debug {
fmt.Println("New authentication tokens saved on disk!")
}
}
func refresh(oldconf Config, filename string, debug bool) Config {
if debug {
log.Println("Expired authentication token. Refreshing...")
}
// Send POST request
client := &http.Client{}
data := "grant_type=refresh_token&refresh_token=" + oldconf.RefreshToken
req, err := http.NewRequest("POST", "https://api.darwinex.com/token", strings.NewReader(data))
if err != nil {
fmt.Println("Error creating request:", err)
os.Exit(1)
}
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(oldconf.ConsumerKey+":"+oldconf.ConsumerSecret)))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error refreshing the authn token: ", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Println("Error refreshing the authn token. Got status code", resp.Status)
fmt.Println("Please refresh the tokens manually from the Darwinex website, and try again.")
os.Exit(1)
}
// Parse the JSON response
type Refresh struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
var newref Refresh
err = json.NewDecoder(resp.Body).Decode(&newref)
if err != nil {
fmt.Println("Error parsing JSON response of the refresh query:", err)
os.Exit(1)
}
oldconf.AuthToken = newref.AccessToken
oldconf.RefreshToken = newref.RefreshToken
saveondisk(oldconf, filename, debug)
return oldconf
}
func sendPut(wg *sync.WaitGroup, url string, darname string, newstop float64, amount float32, conf Config) {
defer wg.Done()
client := &http.Client{}
data := `{"amount":` + strconv.FormatFloat(float64(amount), 'f', 2, 32) + `,"quote":` + strconv.FormatFloat(newstop, 'f', 2, 32) + `}`
req, err := http.NewRequest("PUT", url, strings.NewReader(data))
if err != nil {
fmt.Println("Error creating PUT request:", err)
os.Exit(1)
}
req.Header.Set("Authorization", "Bearer "+conf.AuthToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending PUT request:", err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
log.Println("Trailing stop-loss order updated for " + darname + ". New stop-loss value: " + strconv.FormatFloat(newstop, 'f', 2, 32))
} else {
log.Println("Unknown error while updating the trailing stop-loss order for " + darname + ". Got status code " + resp.Status)
}
}
func main() {
filename := flag.String("f", "config.json", "Your config file")
debug := flag.Bool("d", false, "Shows debug info")
justinv := flag.Bool("i", false, "Gets the available accounts (with their associated investorID) and exits. Use it to get the ID of the investor account you want to use, and add it to the config file")
flag.Parse()
// Read the JSON file
file, err := os.ReadFile(*filename)
if err != nil {
fmt.Println("Error reading config file:", err)
os.Exit(1)
}
// Parse the JSON data into a Config struct
var config Config
err = json.Unmarshal(file, &config)
if err != nil {
fmt.Println("Error parsing JSON:", err)
os.Exit(1)
}
// Sanitization process
for _, darwin := range config.Darwins {
regex := regexp.MustCompile(`^\d+(\.\d+)?%?$`)
if !regex.MatchString(darwin.TrailingSL) {
fmt.Println("Error: trailingSL must be a number or a percentage, e.g. 46.5 or 2.53%")
os.Exit(1)
}
}
// Check if config is empty
if config.AuthToken == "" || config.ConsumerKey == "" || config.ConsumerSecret == "" || config.RefreshToken == "" || len(config.Darwins) == 0 {
fmt.Println("Unexpected JSON format or empty value found")
os.Exit(1)
}
// Check if any Darwin struct is empty
for _, darwin := range config.Darwins {
if darwin.Name == "" || darwin.TrailingSL == "" {
fmt.Println("Unexpected JSON format or empty value found")
os.Exit(1)
}
}
if *justinv {
invResp := sendGet("https://api.darwinex.com/investoraccountinfo/2.0/investoraccounts", config)
if invResp == "unknown" {
fmt.Println("Unknown error while getting the investor ID")
os.Exit(1)
} else if invResp == "unauthorized" {
config = refresh(config, *filename, *debug)
invResp = sendGet("https://api.darwinex.com/investoraccountinfo/2.0/investoraccounts", config)
}
if invResp == "unknown" || invResp == "unauthorized" {
fmt.Println("Unknown error getting the investorID. Can not proceed!")
os.Exit(1)
}
// Parse the JSON response
var investorAccounts []InvestorAccount
err = json.NewDecoder(strings.NewReader(invResp)).Decode(&investorAccounts)
if err != nil {
fmt.Println("Error parsing JSON response for the investorID query:", err)
os.Exit(1)
}
for _, investorAccount := range investorAccounts {
fmt.Println("Account Name:", investorAccount.Name, "-> Investor ID:", investorAccount.ID)
}
os.Exit(0)
}
// Check if the investorID is set
if config.InvestorID == 0 {
fmt.Println("Please use the -i flag to get the available accounts and their associated investorID, and add one to the config file")
os.Exit(0)
}
posResp := sendGet("https://api.darwinex.com/investoraccountinfo/2.0/investoraccounts/"+strconv.Itoa(config.InvestorID)+"/currentpositions", config)
if posResp == "unknown" {
fmt.Println("Unknown error while getting the current positions")
os.Exit(1)
} else if posResp == "unauthorized" {
config = refresh(config, *filename, *debug)
posResp = sendGet("https://api.darwinex.com/investoraccountinfo/2.0/investoraccounts/"+strconv.Itoa(config.InvestorID)+"/currentpositions", config)
}
if posResp == "unknown" || posResp == "unauthorized" {
fmt.Println("Unknown error getting the current positions. Can not proceed!")
os.Exit(1)
}
// Parse the JSON response
var positions []CurrentPosition
err = json.NewDecoder(strings.NewReader(posResp)).Decode(&positions)
if err != nil {
fmt.Println("Error parsing JSON response for the currentpositions query:", err)
os.Exit(1)
}
wg := new(sync.WaitGroup)
flag1 := false
for _, position := range positions {
posname := position.Pname
if strings.Contains(position.Pname, ".") {
posname = strings.Split(position.Pname, ".")[0]
}
for _, darwin := range config.Darwins {
if posname == darwin.Name {
flag3 := false
for _, threshold := range position.Thresholds {
if threshold.Type == "STOP_LOSS" {
flag1 = true
flag3 = true
var magicnumber float64
if strings.Contains(darwin.TrailingSL, "%") {
magicnumber, err = strconv.ParseFloat(strings.ReplaceAll(darwin.TrailingSL, "%", ""), 32)
if err != nil {
fmt.Println("Error parsing the trailingSL value:", err)
os.Exit(1)
}
magicnumber = (magicnumber / 100) * float64(position.Cquote)
} else {
magicnumber, err = strconv.ParseFloat(darwin.TrailingSL, 32)
if err != nil {
fmt.Println("Error parsing the trailingSL value:", err)
os.Exit(1)
}
}
if magicnumber+0.005 < float64(position.Cquote-threshold.Quote) {
wg.Add(1)
go sendPut(wg, "https://api.darwinex.com/trading/1.1/investoraccounts/"+strconv.Itoa(config.InvestorID)+"/conditionalorders/"+strconv.Itoa(threshold.OrderID), darwin.Name, float64(position.Cquote)-magicnumber, threshold.Amount, config)
} else {
if *debug {
log.Println("Stop-loss checked but not modified for", darwin.Name)
}
}
break
}
}
if !flag3 {
fmt.Println("WARNING: No stop-loss found for", darwin.Name, "so I can not update it. Please set a stop-loss order manually in the Darwinex website.")
}
break
}
}
}
if !flag1 {
fmt.Println("WARNING: No stop-loss order found for any of the Darwins in the config file.")
}
wg.Wait()
}