-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathminitrader.go
256 lines (224 loc) · 7.33 KB
/
minitrader.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
package gominitrader
import (
"errors"
"log"
"sync"
"time"
)
type Minitrader struct {
Epic string
Timeframe Timeframe
Status MinitraderStatus
MarketStatus MinitraderMarketStatus
Strategy Strategy
InvestmentPercentage float64
StopLossPercentage float64
ProfitPercentage float64
capitalClient *CapitalClientAPI
candlesChannel chan Candles // TODO: Implement "Pipeline" Pattern To Handle Larger Data Efficiently
activeDealReference string
payedPrice float64
volatileAmountAvailable float64
volatileInvestmentPercentage float64
}
type MinitraderStatus string
const (
// healthy statuses
NEW MinitraderStatus = "NEW"
RUNNING MinitraderStatus = "RUNNING"
HOLDING MinitraderStatus = "HOLDING"
SELL_ORDER_ACTIVE MinitraderStatus = "SELL_ORDER_ACTIVE"
BUY_ORDER_ACTIVE MinitraderStatus = "BUY_ORDER_ACTIVE"
// error statuses
ERROR_ON_UPDATE_CANDLES_DATA MinitraderStatus = "ERROR_ON_UPDATE_CANDLES_DATA"
ERROR_ON_MAKING_ORDER MinitraderStatus = "ERROR_ON_MAKING_ORDER"
ERROR_ON_DELETING_ORDER MinitraderStatus = "ERROR_ON_DELETING_ORDER"
)
type MinitraderMarketStatus string
const (
TRADEABLE MinitraderMarketStatus = "TRADEABLE"
CLOSED MinitraderMarketStatus = "CLOSED"
)
type Timeframe string
const (
MINUTE Timeframe = "MINUTE"
MINUTE_5 Timeframe = "MINUTE_5"
MINUTE_15 Timeframe = "MINUTE_15"
MINUTE_30 Timeframe = "MINUTE_30"
HOUR Timeframe = "HOUR"
HOUR_4 Timeframe = "HOUR_4"
DAY Timeframe = "DAY"
WEEK Timeframe = "WEEK"
)
var TimeframeMinuteMap = map[Timeframe]int{
MINUTE: 1,
MINUTE_5: 5,
MINUTE_15: 15,
MINUTE_30: 30,
HOUR: 60,
HOUR_4: 240,
DAY: 1440,
WEEK: 10080,
}
func NewMinitrader(epic string, investmentPercentage float64, stopLossPercentage float64, profitPercentage float64, timeframe Timeframe, strategy Strategy) *Minitrader {
return &Minitrader{
Epic: epic,
InvestmentPercentage: investmentPercentage,
Timeframe: timeframe,
Strategy: strategy,
Status: NEW,
StopLossPercentage: stopLossPercentage,
ProfitPercentage: profitPercentage,
candlesChannel: make(chan Candles),
volatileInvestmentPercentage: investmentPercentage,
}
}
func (minitrader *Minitrader) Start(waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
defer close(minitrader.candlesChannel)
minitrader.Status = RUNNING
for candles := range minitrader.candlesChannel {
signal, price := minitrader.Strategy(candles)
err := minitrader.Effect(signal, price)
log.Printf("Epic: %s - Timeframe: %v - Signal: %v - Price: %v", minitrader.Epic, minitrader.Timeframe, signal, price)
if err != nil {
log.Printf("Error On Minitrader Effect: %v", err)
return
}
}
}
func (minitrader *Minitrader) Effect(signal Signal, price float64) error {
if minitrader.MarketStatus == CLOSED {
return errors.New("Unable To Do Trading; Market Closed")
}
// quick sell out with looses
lowerBoundPrice := minitrader.payedPrice * (1 + (100-minitrader.StopLossPercentage)/100)
if (minitrader.Status == HOLDING) && (lowerBoundPrice >= price) {
err := minitrader.deleteOrder(minitrader.activeDealReference)
if err != nil {
minitrader.Status = ERROR_ON_DELETING_ORDER
return err
}
}
// sell out with profit
upperBoundPrice := minitrader.payedPrice * (1 + (100+minitrader.ProfitPercentage)/100)
if (minitrader.Status == HOLDING) && (upperBoundPrice <= price) {
err := minitrader.makeOrderAndWaitUntilComplete(minitrader.Epic, SELL, LIMIT, price)
if err != nil {
minitrader.Status = ERROR_ON_MAKING_ORDER
return err
}
}
// make a buy/sell order and wait 3:30 minutes or less if order has been completed before wait time.
if minitrader.Status == RUNNING && signal == BUY { // || (minitrader.Status == HOLDING && signal == SELL) {
err := minitrader.makeOrderAndWaitUntilComplete(minitrader.Epic, signal, LIMIT, price)
if err != nil {
minitrader.Status = ERROR_ON_MAKING_ORDER
return err
}
}
return nil
}
func (minitrader *Minitrader) makeOrderAndWaitUntilComplete(epic string, signal Signal, orderType OrderType, targetPrice float64) error {
var amount float64
var err error
var dealReference string
// update minitrader status and get amount available
// from preferred account or get amount from position/order confirmation
if signal == BUY {
minitrader.Status = BUY_ORDER_ACTIVE
amount = minitrader.volatileAmountAvailable // TODO; Double check if quantity good (amount)
} else {
minitrader.Status = SELL_ORDER_ACTIVE
amount, err = minitrader.getAmountFromPositionOrderConfirmation()
}
if err != nil {
return err
}
// create a working order and retry if it fails
orderResponse, err := minitrader.createWorkingOrderWithRetries(epic, signal, orderType, targetPrice, amount)
if err != nil {
return err
}
dealReference = orderResponse.DealReference
// check if the working order status it was successfully completed
confirmationStatus, err := minitrader.waitUntilConfirmationWithRetries(dealReference)
if err != nil {
return err
}
if confirmationStatus == string(DELETED) {
minitrader.Status = RUNNING
return nil
}
// update minitrader status, active deal reference and payed price
if signal == BUY {
minitrader.Status = HOLDING
minitrader.activeDealReference = dealReference
minitrader.payedPrice = targetPrice
} else {
minitrader.Status = RUNNING
minitrader.activeDealReference = ""
minitrader.payedPrice = 0.0
}
return nil
}
func (minitrader *Minitrader) getAmountFromPositionOrderConfirmation() (amount float64, err error) { // TODO: Unused
tryCounter := 0
for tryCounter < 3 {
positionOrderResponse, err := minitrader.capitalClient.GetPositionOrderConfirmation(minitrader.activeDealReference)
if err != nil {
tryCounter++
time.Sleep(time.Second * 5)
continue
}
amount = positionOrderResponse.Size
break
}
if tryCounter == 3 {
return 0, err
}
return amount, nil
}
func (minitrader *Minitrader) createWorkingOrderWithRetries(epic string, signal Signal, orderType OrderType, targetPrice float64, amount float64) (workingOrderResponse WorkingOrderResponse, err error) {
tryCounter := 0
for tryCounter < 3 {
workingOrderResponse, err = minitrader.capitalClient.CreateWorkingOrder(epic, signal, orderType, targetPrice, amount)
if err != nil {
tryCounter++
time.Sleep(time.Second * 5)
continue
}
break
}
if tryCounter == 3 {
return workingOrderResponse, err
}
return workingOrderResponse, nil
}
func (minitrader *Minitrader) waitUntilConfirmationWithRetries(dealReference string) (orderPositionStatus string, err error) {
tryCounter := 0
for tryCounter < 3 {
confirmationResponse, err := minitrader.capitalClient.GetPositionOrderConfirmation(dealReference)
if err != nil {
tryCounter++
time.Sleep(time.Second * 5)
continue
}
orderPositionStatus = confirmationResponse.Status
break
}
if tryCounter == 3 {
return "", err
}
return orderPositionStatus, nil
}
func (minitrader *Minitrader) deleteOrder(dealReference string) error {
_, err := minitrader.capitalClient.DeleteWorkingOrder(dealReference)
if err != nil {
return err
}
minitrader.activeDealReference = ""
minitrader.Status = RUNNING
minitrader.payedPrice = 0.0
return nil
}