forked from richox/okcoin-leeks-reaper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
trading_okcoin.groovy
280 lines (252 loc) · 12.5 KB
/
trading_okcoin.groovy
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
#!/usr/bin/env groovy
@Grapes([
@Grab("org.oxerr:okcoin-client-rest:3.0.0"),
@Grab("org.slf4j:slf4j-log4j12:1.7.21"),
])
import groovy.json.JsonSlurper
import java.util.concurrent.Executors
import java.util.concurrent.ThreadPoolExecutor
import org.apache.log4j.PropertyConfigurator
import org.knowm.xchange.ExchangeSpecification
import org.knowm.xchange.currency.CurrencyPair
import org.oxerr.okcoin.rest.OKCoinExchange
import org.oxerr.okcoin.rest.dto.OrderData
import org.oxerr.okcoin.rest.dto.Status
import org.oxerr.okcoin.rest.dto.Trade
import org.oxerr.okcoin.rest.dto.Type
import org.oxerr.okcoin.rest.service.polling.OKCoinAccountService
import org.oxerr.okcoin.rest.service.polling.OKCoinMarketDataService
import org.oxerr.okcoin.rest.service.polling.OKCoinTradeService
import org.slf4j.LoggerFactory
class Trading {
def logger = LoggerFactory.getLogger(Trading.class)
def cfg
Trading(cfg) {
this.cfg = cfg
}
def createExchange(apiKey, secKey) {
def exchange = new OKCoinExchange()
def exchangeSpec = new ExchangeSpecification(exchange.class)
exchangeSpec.setApiKey(apiKey)
exchangeSpec.setSecretKey(secKey)
exchange.applySpecification(exchangeSpec)
return exchange
}
def ignoreException = {Closure f -> try {f()} catch (all) {}}
def start() {
def accountExchange = createExchange(cfg.account.apikey, cfg.account.seckey) as OKCoinExchange
def tradeExchange = createExchange(cfg.trade.apikey, cfg.trade.seckey) as OKCoinExchange
def account = accountExchange.pollingAccountService as OKCoinAccountService
def market = accountExchange.pollingMarketDataService as OKCoinMarketDataService
def trader1 = tradeExchange.pollingTradeService as OKCoinTradeService
def trader2 = tradeExchange.pollingTradeService as OKCoinTradeService
def threadExecutor = Executors.newCachedThreadPool() as ThreadPoolExecutor
def trading = false
// 更新历史交易数据,用于计算成交量
def trades
def lastTradeId
def vol = 0
def updateTrades = {
trades = market.getTrades("btc_cny", null) as Trade[]
vol = 0.7 * vol + 0.3 * trades.sum(0.0) {
it.tid > lastTradeId ? it.amount : 0
} // 本次tick交易量 = 上次tick交易量*0.7 + 本次tick期间实际发生的交易量*0.3,用于平滑和减少噪音
lastTradeId = trades[-1].tid
}
updateTrades()
// 更新盘口数据,用于计算价格
def orderBook
def prices = [trades[-1].price] * 15
def bidPrice
def askPrice
def updateOrderBook = {
orderBook = market.getOrderBook(CurrencyPair.BTC_CNY, 2)
// 计算提单价格
bidPrice = orderBook.bids[0].limitPrice * 0.618 + orderBook.asks[0].limitPrice * 0.382 + 0.01
askPrice = orderBook.bids[0].limitPrice * 0.382 + orderBook.asks[0].limitPrice * 0.618 - 0.01
// 更新时间价格序列
// 本次tick价格 = (买1+卖1)*0.35 + (买2+卖2) * 0.10 + (买3+卖3)*0.05
prices = prices[1 .. -1] + [(
(orderBook.bids[0].limitPrice + orderBook.asks[0].limitPrice) / 2 * 0.7 +
(orderBook.bids[1].limitPrice + orderBook.asks[1].limitPrice) / 2 * 0.2 +
(orderBook.bids[2].limitPrice + orderBook.asks[2].limitPrice) / 2 * 0.1)]
}
updateOrderBook()
// 更新仓位
def userInfo
def btc
def cny
def p = 0.5
threadExecutor.execute {
while (true) {
if (trading) {
sleep 5
continue
}
def t = System.currentTimeMillis()
ignoreException {
// 这里有一个仓位平衡的辅助策略
// 仓位平衡策略是在仓位偏离50%时,通过不断提交小单来使仓位回归50%的策略,
// 这个辅助策略可以有效减少趋势策略中趋势反转+大滑点带来的大幅回撤
def orders = (
p < 0.48 ? {
cny -= 300.0
trader2.batchTrade("btc_cny", Type.BUY, [
new OrderData(orderBook.bids[0].limitPrice + 0.00, 0.010G, Type.BUY),
new OrderData(orderBook.bids[0].limitPrice + 0.01, 0.010G, Type.BUY),
new OrderData(orderBook.bids[0].limitPrice + 0.02, 0.010G, Type.BUY),
] as OrderData[])
}() :
p > 0.52 ? {
btc -= 0.030
trader2.batchTrade("btc_cny", Type.SELL, [
new OrderData(orderBook.asks[0].limitPrice - 0.00, 0.010G, Type.SELL),
new OrderData(orderBook.asks[0].limitPrice - 0.01, 0.010G, Type.SELL),
new OrderData(orderBook.asks[0].limitPrice - 0.02, 0.010G, Type.SELL),
] as OrderData[])
}() :
null)
userInfo = account.userInfo
btc = userInfo.info.funds.free.btc
cny = userInfo.info.funds.free.cny
p = btc * prices[-1] / (btc * prices[-1] + cny)
if (orders != null) {
sleep 400
trader2.cancelOrder("btc_cny", orders.orderInfo.collect {it.orderId} as long[])
}
}
while (System.currentTimeMillis() - t < 500) {
sleep 5
}
}
}
// 定时扫描、取消失效的旧订单
// 策略执行中难免会有不能成交、取消失败遗留下来的旧订单,
// 定时取消掉这些订单防止占用资金
threadExecutor.execute {
while (true) {
ignoreException {
trader2.openOrders.openOrders
.grep {it.timestamp.time - System.currentTimeMillis() < -10000} // orders before 10s
.each {
trader2.cancelOrder(it.id)
}
}
sleep 60000
}
}
// main loop
def ts1 = 0
def ts0 = 0
for (def numTick = 0; ; numTick++) {
while (System.currentTimeMillis() - ts0 < cfg.tick.interval) {
sleep 5
}
trading = false
ts1 = ts0
ts0 = System.currentTimeMillis()
try {
updateTrades()
updateOrderBook()
logger.info("tick: ${ts0-ts1}, {}, net: {}, total: {}, p: {} - {}/{}, v: {}",
String.format("%.2f", prices[-1]),
String.format("%.2f", userInfo.info.funds.asset.net),
String.format("%.2f", userInfo.info.funds.asset.total),
String.format("%.2f", p),
String.format("%.3f", btc),
String.format("%.2f", cny),
String.format("%.2f", vol))
def burstPrice = prices[-1] * cfg.burst.threshold.pct
def bull = false
def bear = false
def tradeAmount = 0
// 趋势策略,价格出现方向上的突破时开始交易
if (numTick > 2 && (
prices[-1] - prices[-6 .. -2].max() > +burstPrice ||
prices[-1] - prices[-6 .. -3].max() > +burstPrice && prices[-1] > prices[-2]
)) {
bull = true
tradeAmount = cny / bidPrice * 0.99
}
if (numTick > 2 && (
prices[-1] - prices[-6 .. -2].min() < -burstPrice ||
prices[-1] - prices[-6 .. -3].min() < -burstPrice && prices[-1] < prices[-2]
)) {
bear = true
tradeAmount = btc
}
// 下单力度计算
// 1. 小成交量的趋势成功率比较低,减小力度
// 2. 过度频繁交易有害,减小力度
// 3. 短时价格波动过大,减小力度
// 4. 盘口价差过大,减少力度
if (vol < cfg.burst.threshold.vol) tradeAmount *= vol / cfg.burst.threshold.vol
if (numTick < 5) tradeAmount *= 0.80
if (numTick < 10) tradeAmount *= 0.80
if (bull && prices[-1] < prices[0 .. -1].max()) tradeAmount *= 0.90
if (bear && prices[-1] > prices[0 .. -1].min()) tradeAmount *= 0.90
if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 2) tradeAmount *= 0.90
if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 3) tradeAmount *= 0.90
if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 4) tradeAmount *= 0.90
if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 2) tradeAmount *= 0.90
if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 3) tradeAmount *= 0.90
if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 4) tradeAmount *= 0.90
if (tradeAmount >= 0.1) { // 最后下单量小于0.1BTC的就不操作了
def tradePrice = bull ? bidPrice : askPrice
trading = true
while (tradeAmount >= 0.1) {
def orderId = bull // 提单
? trader1.trade("btc_cny", Type.BUY, bidPrice, tradeAmount).orderId
: trader1.trade("btc_cny", Type.SELL, askPrice, tradeAmount).orderId
ignoreException { // 等待200ms后取消挂单
sleep 200
trader1.cancelOrder("btc_cny", orderId)
}
// 获取订单状态
def order
while (order == null || order.status == Status.CANCEL_REQUEST_IN_PROCESS) {
order = trader1.getOrder("btc_cny", orderId).orders[0]
}
logger.warn("TRADING: {} price: {}, amount: {}, dealAmount: {}",
bull ? '++':'--',
String.format("%.2f", bull ? bidPrice : askPrice),
String.format("%.3f", tradeAmount),
String.format("%.3f", order.dealAmount))
tradeAmount -= order.dealAmount
tradeAmount -= 0.01
tradeAmount *= 0.98 // 每轮循环都少量削减力度
if (order.status == Status.CANCELLED) {
updateOrderBook() // 更新盘口,更新后的价格高于提单价格也需要削减力度
while (bull && bidPrice - tradePrice > +0.1) {
tradeAmount *= 0.99
tradePrice += 0.1
}
while (bear && askPrice - tradePrice < -0.1) {
tradeAmount *= 0.99
tradePrice -= 0.1
}
}
}
numTick = 0
}
} catch (InterruptedException e) {
logger.error("interrupted: ", e)
break
} catch (all) {
logger.error("unhandled exception: ", all)
continue
}
}
}
}
// configure logging
_prop = new Properties()
_prop.setProperty("log4j.rootLogger", "INFO, trading")
_prop.setProperty("log4j.appender.trading", "org.apache.log4j.ConsoleAppender")
_prop.setProperty("log4j.appender.trading.Target", "System.err")
_prop.setProperty("log4j.appender.trading.layout", "org.apache.log4j.PatternLayout")
_prop.setProperty("log4j.appender.trading.layout.ConversionPattern", "[%d{yyyy-MM-dd HH:mm:ss}] %p %m %n")
PropertyConfigurator.configure(_prop)
// start trading
_trading = new Trading(new ConfigSlurper().parse(new File(System.getProperty("cfg")).text))
_trading.start()