├── README.md ├── conf └── example.cfg ├── img └── screenshot.png └── trading_okcoin.groovy /README.md: -------------------------------------------------------------------------------- 1 | OKCoin韭菜收割机 2 | ================ 3 | 4 | 这是一个在OKCoin比特币交易平台上的高频交易机器人程序,从2016年6月策略基本定型,到2017年1月中旬,这个策略成功的把最初投入的6000块钱刷到了250000。由于近日央行对比特币实行高压政策,各大平台都停止了配资,并且开始征收交易费,该策略实际上已经失效了。 5 | 6 | ![image](https://github.com/richox/okcoin-leeks-reaper/raw/master/img/screenshot.png) 7 | 8 | 本机器人程序基于两个主要策略: 9 | 10 | 1. 趋势策略:在价格发生趋势性的波动时,及时下单跟进,即俗话说的**追涨杀跌**。 11 | 2. 平衡策略:仓位偏离50%时,放出小单使仓位逐渐回归50%,防止趋势末期的反转造成回撤,即**收益落袋,不吃鱼尾**。 12 | 13 | 本程序要求平衡仓位,即(本金+融资=融币),使得仓位在50%时净资产不随着价格波动,也保证了发生趋势性波动时**涨跌都赚**。 14 | 15 | 感谢以下两个项目: 16 | 17 | * https://github.com/sutra/okcoin-client 18 | * https://github.com/timmolter/xchange 19 | 20 | 感谢OKCoin: 21 | 22 | * https://www.okcoin.cn 23 | 24 | BTC: 3QFn1qfZMhMQ4FhgENR7fha3T8ZVw1bEeU 25 | -------------------------------------------------------------------------------- /conf/example.cfg: -------------------------------------------------------------------------------- 1 | account.apikey = "d7c3034a-0f95-249b-a5db-8bc36dd1a583" 2 | account.seckey = "1290C32FA5AB5ABAD5A9E4FF2065F93E" 3 | trade.apikey = "22cf5769-8fe8-2eab-bc3f-8b6a513a669e" 4 | trade.seckey = "B330466EAD6A412AB5D4D90E15090166" 5 | 6 | tick.interval = 280 7 | burst.threshold.pct = 0.00005 8 | burst.threshold.vol = 10 9 | -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richox/okcoin-leeks-reaper/df8895853c306dfdafbee20125790f145bbf17c1/img/screenshot.png -------------------------------------------------------------------------------- /trading_okcoin.groovy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | @Grapes([ 4 | @Grab("org.oxerr:okcoin-client-rest:3.0.0"), 5 | @Grab("org.slf4j:slf4j-log4j12:1.7.21"), 6 | ]) 7 | import groovy.json.JsonSlurper 8 | 9 | import java.util.concurrent.Executors 10 | import java.util.concurrent.ThreadPoolExecutor 11 | 12 | import org.apache.log4j.PropertyConfigurator 13 | import org.knowm.xchange.ExchangeSpecification 14 | import org.knowm.xchange.currency.CurrencyPair 15 | import org.oxerr.okcoin.rest.OKCoinExchange 16 | import org.oxerr.okcoin.rest.dto.OrderData 17 | import org.oxerr.okcoin.rest.dto.Status 18 | import org.oxerr.okcoin.rest.dto.Trade 19 | import org.oxerr.okcoin.rest.dto.Type 20 | import org.oxerr.okcoin.rest.service.polling.OKCoinAccountService 21 | import org.oxerr.okcoin.rest.service.polling.OKCoinMarketDataService 22 | import org.oxerr.okcoin.rest.service.polling.OKCoinTradeService 23 | import org.slf4j.LoggerFactory 24 | 25 | class Trading { 26 | def logger = LoggerFactory.getLogger(Trading.class) 27 | def cfg 28 | 29 | Trading(cfg) { 30 | this.cfg = cfg 31 | } 32 | 33 | def createExchange(apiKey, secKey) { 34 | def exchange = new OKCoinExchange() 35 | def exchangeSpec = new ExchangeSpecification(exchange.class) 36 | exchangeSpec.setApiKey(apiKey) 37 | exchangeSpec.setSecretKey(secKey) 38 | exchange.applySpecification(exchangeSpec) 39 | return exchange 40 | } 41 | 42 | def ignoreException = {Closure f -> try {f()} catch (all) {}} 43 | def start() { 44 | def accountExchange = createExchange(cfg.account.apikey, cfg.account.seckey) as OKCoinExchange 45 | def tradeExchange = createExchange(cfg.trade.apikey, cfg.trade.seckey) as OKCoinExchange 46 | def account = accountExchange.pollingAccountService as OKCoinAccountService 47 | def market = accountExchange.pollingMarketDataService as OKCoinMarketDataService 48 | def trader1 = tradeExchange.pollingTradeService as OKCoinTradeService 49 | def trader2 = tradeExchange.pollingTradeService as OKCoinTradeService 50 | def threadExecutor = Executors.newCachedThreadPool() as ThreadPoolExecutor 51 | def trading = false 52 | 53 | // 更新历史交易数据,用于计算成交量 54 | def trades 55 | def lastTradeId 56 | def vol = 0 57 | def updateTrades = { 58 | trades = market.getTrades("btc_cny", null) as Trade[] 59 | vol = 0.7 * vol + 0.3 * trades.sum(0.0) { 60 | it.tid > lastTradeId ? it.amount : 0 61 | } // 本次tick交易量 = 上次tick交易量*0.7 + 本次tick期间实际发生的交易量*0.3,用于平滑和减少噪音 62 | lastTradeId = trades[-1].tid 63 | } 64 | updateTrades() 65 | 66 | // 更新盘口数据,用于计算价格 67 | def orderBook 68 | def prices = [trades[-1].price] * 15 69 | def bidPrice 70 | def askPrice 71 | def updateOrderBook = { 72 | orderBook = market.getOrderBook(CurrencyPair.BTC_CNY, 2) 73 | 74 | // 计算提单价格 75 | bidPrice = orderBook.bids[0].limitPrice * 0.618 + orderBook.asks[0].limitPrice * 0.382 + 0.01 76 | askPrice = orderBook.bids[0].limitPrice * 0.382 + orderBook.asks[0].limitPrice * 0.618 - 0.01 77 | 78 | // 更新时间价格序列 79 | // 本次tick价格 = (买1+卖1)*0.35 + (买2+卖2) * 0.10 + (买3+卖3)*0.05 80 | prices = prices[1 .. -1] + [( 81 | (orderBook.bids[0].limitPrice + orderBook.asks[0].limitPrice) / 2 * 0.7 + 82 | (orderBook.bids[1].limitPrice + orderBook.asks[1].limitPrice) / 2 * 0.2 + 83 | (orderBook.bids[2].limitPrice + orderBook.asks[2].limitPrice) / 2 * 0.1)] 84 | } 85 | updateOrderBook() 86 | 87 | // 更新仓位 88 | def userInfo 89 | def btc 90 | def cny 91 | def p = 0.5 92 | threadExecutor.execute { 93 | while (true) { 94 | if (trading) { 95 | sleep 5 96 | continue 97 | } 98 | def t = System.currentTimeMillis() 99 | ignoreException { 100 | // 这里有一个仓位平衡的辅助策略 101 | // 仓位平衡策略是在仓位偏离50%时,通过不断提交小单来使仓位回归50%的策略, 102 | // 这个辅助策略可以有效减少趋势策略中趋势反转+大滑点带来的大幅回撤 103 | def orders = ( 104 | p < 0.48 ? { 105 | cny -= 300.0 106 | trader2.batchTrade("btc_cny", Type.BUY, [ 107 | new OrderData(orderBook.bids[0].limitPrice + 0.00, 0.010G, Type.BUY), 108 | new OrderData(orderBook.bids[0].limitPrice + 0.01, 0.010G, Type.BUY), 109 | new OrderData(orderBook.bids[0].limitPrice + 0.02, 0.010G, Type.BUY), 110 | ] as OrderData[]) 111 | }() : 112 | p > 0.52 ? { 113 | btc -= 0.030 114 | trader2.batchTrade("btc_cny", Type.SELL, [ 115 | new OrderData(orderBook.asks[0].limitPrice - 0.00, 0.010G, Type.SELL), 116 | new OrderData(orderBook.asks[0].limitPrice - 0.01, 0.010G, Type.SELL), 117 | new OrderData(orderBook.asks[0].limitPrice - 0.02, 0.010G, Type.SELL), 118 | ] as OrderData[]) 119 | }() : 120 | null) 121 | userInfo = account.userInfo 122 | btc = userInfo.info.funds.free.btc 123 | cny = userInfo.info.funds.free.cny 124 | p = btc * prices[-1] / (btc * prices[-1] + cny) 125 | 126 | if (orders != null) { 127 | sleep 400 128 | trader2.cancelOrder("btc_cny", orders.orderInfo.collect {it.orderId} as long[]) 129 | } 130 | } 131 | while (System.currentTimeMillis() - t < 500) { 132 | sleep 5 133 | } 134 | } 135 | } 136 | 137 | // 定时扫描、取消失效的旧订单 138 | // 策略执行中难免会有不能成交、取消失败遗留下来的旧订单, 139 | // 定时取消掉这些订单防止占用资金 140 | threadExecutor.execute { 141 | while (true) { 142 | ignoreException { 143 | trader2.openOrders.openOrders 144 | .grep {it.timestamp.time - System.currentTimeMillis() < -10000} // orders before 10s 145 | .each { 146 | trader2.cancelOrder(it.id) 147 | } 148 | } 149 | sleep 60000 150 | } 151 | } 152 | 153 | // main loop 154 | def ts1 = 0 155 | def ts0 = 0 156 | for (def numTick = 0; ; numTick++) { 157 | while (System.currentTimeMillis() - ts0 < cfg.tick.interval) { 158 | sleep 5 159 | } 160 | trading = false 161 | ts1 = ts0 162 | ts0 = System.currentTimeMillis() 163 | 164 | try { 165 | updateTrades() 166 | updateOrderBook() 167 | 168 | logger.info("tick: ${ts0-ts1}, {}, net: {}, total: {}, p: {} - {}/{}, v: {}", 169 | String.format("%.2f", prices[-1]), 170 | String.format("%.2f", userInfo.info.funds.asset.net), 171 | String.format("%.2f", userInfo.info.funds.asset.total), 172 | String.format("%.2f", p), 173 | String.format("%.3f", btc), 174 | String.format("%.2f", cny), 175 | String.format("%.2f", vol)) 176 | 177 | def burstPrice = prices[-1] * cfg.burst.threshold.pct 178 | def bull = false 179 | def bear = false 180 | def tradeAmount = 0 181 | 182 | // 趋势策略,价格出现方向上的突破时开始交易 183 | if (numTick > 2 && ( 184 | prices[-1] - prices[-6 .. -2].max() > +burstPrice || 185 | prices[-1] - prices[-6 .. -3].max() > +burstPrice && prices[-1] > prices[-2] 186 | )) { 187 | bull = true 188 | tradeAmount = cny / bidPrice * 0.99 189 | } 190 | if (numTick > 2 && ( 191 | prices[-1] - prices[-6 .. -2].min() < -burstPrice || 192 | prices[-1] - prices[-6 .. -3].min() < -burstPrice && prices[-1] < prices[-2] 193 | )) { 194 | bear = true 195 | tradeAmount = btc 196 | } 197 | 198 | // 下单力度计算 199 | // 1. 小成交量的趋势成功率比较低,减小力度 200 | // 2. 过度频繁交易有害,减小力度 201 | // 3. 短时价格波动过大,减小力度 202 | // 4. 盘口价差过大,减少力度 203 | if (vol < cfg.burst.threshold.vol) tradeAmount *= vol / cfg.burst.threshold.vol 204 | if (numTick < 5) tradeAmount *= 0.80 205 | if (numTick < 10) tradeAmount *= 0.80 206 | if (bull && prices[-1] < prices[0 .. -1].max()) tradeAmount *= 0.90 207 | if (bear && prices[-1] > prices[0 .. -1].min()) tradeAmount *= 0.90 208 | if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 2) tradeAmount *= 0.90 209 | if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 3) tradeAmount *= 0.90 210 | if (Math.abs(prices[-1] - prices[-2]) > burstPrice * 4) tradeAmount *= 0.90 211 | if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 2) tradeAmount *= 0.90 212 | if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 3) tradeAmount *= 0.90 213 | if (orderBook.asks[0].limitPrice - orderBook.bids[0].limitPrice > burstPrice * 4) tradeAmount *= 0.90 214 | 215 | if (tradeAmount >= 0.1) { // 最后下单量小于0.1BTC的就不操作了 216 | def tradePrice = bull ? bidPrice : askPrice 217 | trading = true 218 | 219 | while (tradeAmount >= 0.1) { 220 | def orderId = bull // 提单 221 | ? trader1.trade("btc_cny", Type.BUY, bidPrice, tradeAmount).orderId 222 | : trader1.trade("btc_cny", Type.SELL, askPrice, tradeAmount).orderId 223 | 224 | ignoreException { // 等待200ms后取消挂单 225 | sleep 200 226 | trader1.cancelOrder("btc_cny", orderId) 227 | } 228 | 229 | // 获取订单状态 230 | def order 231 | while (order == null || order.status == Status.CANCEL_REQUEST_IN_PROCESS) { 232 | order = trader1.getOrder("btc_cny", orderId).orders[0] 233 | } 234 | logger.warn("TRADING: {} price: {}, amount: {}, dealAmount: {}", 235 | bull ? '++':'--', 236 | String.format("%.2f", bull ? bidPrice : askPrice), 237 | String.format("%.3f", tradeAmount), 238 | String.format("%.3f", order.dealAmount)) 239 | tradeAmount -= order.dealAmount 240 | tradeAmount -= 0.01 241 | tradeAmount *= 0.98 // 每轮循环都少量削减力度 242 | 243 | if (order.status == Status.CANCELLED) { 244 | updateOrderBook() // 更新盘口,更新后的价格高于提单价格也需要削减力度 245 | while (bull && bidPrice - tradePrice > +0.1) { 246 | tradeAmount *= 0.99 247 | tradePrice += 0.1 248 | } 249 | while (bear && askPrice - tradePrice < -0.1) { 250 | tradeAmount *= 0.99 251 | tradePrice -= 0.1 252 | } 253 | } 254 | } 255 | numTick = 0 256 | } 257 | } catch (InterruptedException e) { 258 | logger.error("interrupted: ", e) 259 | break 260 | 261 | } catch (all) { 262 | logger.error("unhandled exception: ", all) 263 | continue 264 | } 265 | } 266 | } 267 | } 268 | 269 | // configure logging 270 | _prop = new Properties() 271 | _prop.setProperty("log4j.rootLogger", "INFO, trading") 272 | _prop.setProperty("log4j.appender.trading", "org.apache.log4j.ConsoleAppender") 273 | _prop.setProperty("log4j.appender.trading.Target", "System.err") 274 | _prop.setProperty("log4j.appender.trading.layout", "org.apache.log4j.PatternLayout") 275 | _prop.setProperty("log4j.appender.trading.layout.ConversionPattern", "[%d{yyyy-MM-dd HH:mm:ss}] %p %m %n") 276 | PropertyConfigurator.configure(_prop) 277 | 278 | // start trading 279 | _trading = new Trading(new ConfigSlurper().parse(new File(System.getProperty("cfg")).text)) 280 | _trading.start() 281 | --------------------------------------------------------------------------------