├── LICENSE ├── README.md ├── engine ├── .gitignore ├── README.md ├── build.gradle ├── exchange │ ├── build.gradle │ └── src │ │ ├── main │ │ └── java │ │ │ └── cqt │ │ │ └── goai │ │ │ └── exchange │ │ │ ├── Action.java │ │ │ ├── BaseExchange.java │ │ │ ├── ExchangeError.java │ │ │ ├── ExchangeException.java │ │ │ ├── ExchangeInfo.java │ │ │ ├── ExchangeManager.java │ │ │ ├── ExchangeName.java │ │ │ ├── ExchangeUtil.java │ │ │ ├── http │ │ │ ├── HistoryOrdersByTimestamp.java │ │ │ ├── HistoryOrdersDetailsByTimestamp.java │ │ │ ├── HttpExchange.java │ │ │ ├── TradeByFillOrKill.java │ │ │ ├── binance │ │ │ │ └── BinanceExchange.java │ │ │ ├── bitfinex │ │ │ │ └── BitfinexExchange.java │ │ │ ├── huobi │ │ │ │ └── pro │ │ │ │ │ └── HuobiProExchange.java │ │ │ └── okexv3 │ │ │ │ └── Okexv3Exchange.java │ │ │ ├── util │ │ │ ├── CommonUtil.java │ │ │ ├── CompleteList.java │ │ │ ├── Md5Util.java │ │ │ ├── OkhttpWebSocket.java │ │ │ ├── RateLimit.java │ │ │ ├── Seal.java │ │ │ ├── bitfinex │ │ │ │ └── BitfinexUtil.java │ │ │ ├── huobi │ │ │ │ └── pro │ │ │ │ │ └── HoubiProUtil.java │ │ │ └── okexv3 │ │ │ │ └── Okexv3Util.java │ │ │ └── web │ │ │ └── socket │ │ │ ├── BaseWebSocketClient.java │ │ │ ├── WebSocketExchange.java │ │ │ ├── WebSocketInfo.java │ │ │ └── okexv3 │ │ │ ├── Okexv3WebSocketClient.java │ │ │ └── Okexv3WebSocketExchange.java │ │ └── test │ │ └── java │ │ └── test │ │ └── RateLimitTest.java ├── model │ ├── build.gradle │ └── src │ │ ├── main │ │ └── java │ │ │ └── cqt │ │ │ └── goai │ │ │ └── model │ │ │ ├── BaseModelList.java │ │ │ ├── To.java │ │ │ ├── Util.java │ │ │ ├── enums │ │ │ ├── Period.java │ │ │ ├── Side.java │ │ │ ├── State.java │ │ │ └── Type.java │ │ │ ├── market │ │ │ ├── Depth.java │ │ │ ├── Kline.java │ │ │ ├── Klines.java │ │ │ ├── Row.java │ │ │ ├── Rows.java │ │ │ ├── Ticker.java │ │ │ ├── Trade.java │ │ │ └── Trades.java │ │ │ ├── other │ │ │ ├── RunInfo.java │ │ │ └── RunState.java │ │ │ └── trade │ │ │ ├── Account.java │ │ │ ├── Balance.java │ │ │ ├── Balances.java │ │ │ ├── Order.java │ │ │ ├── OrderDetail.java │ │ │ ├── OrderDetails.java │ │ │ ├── Orders.java │ │ │ ├── Precision.java │ │ │ └── Precisions.java │ │ └── test │ │ └── java │ │ └── test │ │ ├── market │ │ ├── DepthTest.java │ │ ├── KlineTest.java │ │ ├── KlinesTest.java │ │ ├── RowTest.java │ │ ├── RowsTest.java │ │ ├── TickerTest.java │ │ ├── TradeTest.java │ │ └── TradesTest.java │ │ └── trade │ │ └── PrecisionTest.java ├── run │ ├── build.gradle │ ├── jar.bat │ └── src │ │ └── main │ │ ├── java │ │ └── cqt │ │ │ └── goai │ │ │ └── run │ │ │ ├── Application.java │ │ │ ├── annotation │ │ │ ├── Scheduled.java │ │ │ ├── ScheduledScope.java │ │ │ └── Schedules.java │ │ │ ├── exchange │ │ │ ├── BaseExchange.java │ │ │ ├── Exchange.java │ │ │ ├── FourFunction.java │ │ │ ├── LocalExchange.java │ │ │ ├── ProxyExchange.java │ │ │ ├── TrConsumer.java │ │ │ ├── factory │ │ │ │ ├── BaseExchangeFactory.java │ │ │ │ └── LocalExchangeFactory.java │ │ │ └── model │ │ │ │ ├── LogAction.java │ │ │ │ ├── ModelManager.java │ │ │ │ ├── ModelObserver.java │ │ │ │ └── TradesObserver.java │ │ │ ├── main │ │ │ ├── Const.java │ │ │ ├── Injectable.java │ │ │ ├── MethodInfo.java │ │ │ ├── MethodScope.java │ │ │ ├── Minister.java │ │ │ ├── Notice.java │ │ │ ├── RunTask.java │ │ │ ├── ScheduledInfo.java │ │ │ ├── ScheduledJob.java │ │ │ ├── ScheduledMethod.java │ │ │ ├── TaskManager.java │ │ │ ├── UserLogger.java │ │ │ └── Util.java │ │ │ └── notice │ │ │ ├── BaseNotice.java │ │ │ ├── EmailNotice.java │ │ │ ├── Notice.java │ │ │ ├── NoticeType.java │ │ │ └── TelegramNotice.java │ │ └── resources │ │ └── logback.xml ├── settings.gradle └── strategy │ ├── build.gradle │ ├── package.bat │ ├── package.sh │ └── src │ └── main │ ├── java │ ├── Demo.java │ └── Main.java │ └── resources │ ├── .gitignore │ ├── config.yml │ └── logback.xml ├── qrcode_for_gh_9666b42fb2ca_430.jpg └── strategy ├── .gitignore ├── application └── web.jar ├── build.gradle ├── libs └── goai-engine.jar ├── package.bat ├── package.sh ├── run_goai.bat ├── run_goai.sh └── src └── main ├── java ├── Demo.java ├── Main.java └── tutorial │ ├── Tutorial01.java │ ├── Tutorial02.java │ ├── Tutorial03.java │ └── Tutorial04.java └── resources └── config.yml /README.md: -------------------------------------------------------------------------------- 1 |

2 | Sym 3 |
4 | 新一代的量化系统,为交易而构建 5 |

6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | ## 简介 14 | GOAi 是一款基于JAVA的开源量化交易系统,起源于公司自研量化系统。目标从初期的交易所API接口封装,一步步成长为一套全功能量化交易平台。目前仅支持数字资产量化交易,未来会陆续支持A股、港股、美股、期货、期权等多种交易标的。 15 | ## 动机 16 | #### 商业闭源平台问题: 17 | * 不要把交易所密钥存储在第三方平台,重要的事情说三遍。 18 | * 策略的保密性,策略是自己辛辛苦苦研究成果。平台无法确保存储的绝对安全。 19 | * 如果代跑策略,用第三方平台并不方便。 20 | * 商业平台的闭源性质,你并不清楚框架做了什么。 21 | * 熊市本来就不赚钱,还要支出一笔额外费用。 22 | #### 其它开源平台: 23 | * 部分项目产品化做的不够好,偏技术,易用性较差。 24 | * 部分项目个人业余时间进行维护,文档、bug修复、新增功能不够及时。 25 | ## 我们的优势 26 | * 交易引擎100%开源 27 | * 公司持续化维护运营,配备专门的开发、设计、测试人员。 28 | * 产品优化会根据使用者的反馈第一时间做出调整。 29 | * 定期举行线上线下的交流会。 30 | ## 功能 31 | #### 现有功能: 32 | * 支持主流交易所现货http接口(okexv3、火币、币安、bitfinex)。 33 | * 支持交易所WebSocket连接方式(okexv3 ws接口)。 34 | * 支持电报发消息通知。 35 | * 接口的模板化封装。 36 | #### 准备实现功能: 37 | * 更多主流现货交易的支持。 38 | * okex合约的支持。 39 | * bitmex交易所支持。 40 | * 回测系统的实现。 41 | * 图表及收益曲线的支持。 42 | * 多语言开发的支持 如 python、javascript 等。 43 | ## 界面 44 | 45 | **登录页** 46 | 47 | ![index](https://images.gitee.com/uploads/images/2019/0308/174439_8af709ba_2076727.png) 48 | 49 | **首页** 50 | 51 | ![index](https://images.gitee.com/uploads/images/2019/0307/164231_1a8ea26c_2076727.png) 52 | 53 | **实例管理** 54 | 55 | > 在线配置参数实例 56 | 57 | ![index](https://images.gitee.com/uploads/images/2019/0307/180525_1ce47b16_2076727.png) 58 | 59 | > jar包本地获取配置参数实例 60 | 61 | ![index](https://images.gitee.com/uploads/images/2019/0307/164232_753a4090_2076727.png) 62 | 63 | **策略管理** 64 | 65 | ![index](https://images.gitee.com/uploads/images/2019/0307/164231_51d0a7f1_2076727.png) 66 | 67 | **交易所管理** 68 | 69 | ![index](https://images.gitee.com/uploads/images/2019/0307/164232_bc8a0ca2_2076727.png) 70 | 71 | **策略模板** 72 | 73 | ![index](https://raw.githubusercontent.com/zq33/TP/master/%E9%A1%B5%E9%9D%A2%E6%88%AA%E5%9B%BE/%E7%AD%96%E7%95%A5%E6%A8%A1%E6%9D%BF.png) 74 | 75 | 76 | ## 环境要求 77 | 78 | **GOAi运行环境基本要求** 79 | > 只需启动运行GOAi服务(策略实例、GOAi后台管理系统Web服务)情况 80 | + JDK(1.8及以上) 81 | + Linux(Centos7) 或 Windows 82 | 83 | **GOAi策略开发环境基本要求** 84 | > 策略开发调试基本环境,一般是在本地开发调试策略时所需要的环境要求 85 | + JDK(1.8及以上) 86 | + Gradle(4.8及以上) 87 | + Linux(Centos7) 或 Windows 88 | 89 | 90 | ## 一键启动方式 91 | 92 | **Windows** 93 | 94 | + 进入strategy目录,点击运行 `run_goai.bat` 脚本文件即可 95 | 96 | **MAC** 97 | 98 | + 进入strategy目录,运行 ./run_goai.sh 命令 99 | 100 | **Linux** 101 | 102 | + 进入strategy目录,点击运行 `run_goai.sh` 脚本文件即可 103 | 104 | 若是Centos7系统(更多系统支持敬请期待),则可下载[一键安装启动脚本](https://github.com/goaiquant/GOAi/releases/tag/v0.0.1),快速安装部署并启动: 105 | + 使用root身份登录Linux服务器 106 | + 切换到root目录:`cd /root` 107 | + 下载一键安装启动脚本:`wget https://github.com/goaiquant/GOAi/releases/download/v0.0.1/goai.sh` 108 | + 运行脚本进行安装或管理:`bash goai.sh` 109 | + 按照窗口提示输入对应命令序号进行操作即可 110 | 111 | ## 文档 112 | * [API 接口文档](https://github.com/goaiquant/GOAi/wiki/GOAi-API-接口文档) 113 | * [telegram 通知配置说明](https://github.com/goaiquant/GOAi/wiki/电报通知配置方法) 114 | 115 | ## 使用IDEA 打开工程写策略 116 | * [IDE打开工程并写策略方法](http://note.youdao.com/noteshare?id=cce22cf28f89ffcba66c758c38756b13) 117 | 118 | ## 社群 119 | * 公众号:了解最新产品动态,听策略大师、开发小哥、设计妹子吐槽吹水。 120 | > ![index](https://github.com/goaiquant/GOAi/blob/master/qrcode_for_gh_9666b42fb2ca_430.jpg?raw=true) 121 | * GOAi官方QQ交流群:689620375 (群附件有GOAi使用的视频教程) 122 | 123 | ## 授权 124 | 引擎使用 AGPLV3 开源,须严格遵守 AGPLV3 的相关条款。 125 | -------------------------------------------------------------------------------- /engine/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | .run_info 4 | .goai 5 | *.iml 6 | PID 7 | node 8 | build 9 | *.log 10 | logs 11 | *.csv 12 | *.xlsx 13 | .mime_cache 14 | .realtime 15 | .profit 16 | -------------------------------------------------------------------------------- /engine/README.md: -------------------------------------------------------------------------------- 1 | # GOAi量化交易系统 2 | 3 | ## 本地环境搭建 4 | + JDK1.8 5 | + Gradle 6 | 7 | ## 模块介绍 8 | + **exchange**: 封装各交易所接口 http 和 websocket 9 | + **model**: model bean 10 | + **run**: 结合model和exchange可以运行策略 11 | + **strategy**: 策略模块 可在引擎中的该目录下写策略 12 | 13 | 14 | -------------------------------------------------------------------------------- /engine/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 4 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter/' } 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.1.RELEASE") 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 15 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter/' } 16 | maven { url 'https://maven.yinian.tech/public/' } 17 | mavenCentral() 18 | } 19 | apply plugin: 'java' 20 | apply plugin: 'idea' 21 | 22 | idea { 23 | module { 24 | downloadSources = true 25 | inheritOutputDirs = false 26 | outputDir = file("build/classes/main/") 27 | testOutputDir = file("build/classes/test/") 28 | } 29 | } 30 | 31 | [compileJava,compileTestJava,javadoc]*.options*.encoding = 'UTF-8' 32 | 33 | sourceCompatibility = 1.8 34 | targetCompatibility = 1.8 35 | 36 | dependencies { 37 | annotationProcessor 'org.projectlombok:lombok:1.18.4' 38 | implementation 'org.projectlombok:lombok:1.18.4' 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /engine/exchange/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | enabled = true 3 | baseName = 'exchange' 4 | version = '1.0.0' 5 | } 6 | 7 | dependencies { 8 | testCompile 'junit:junit:4.12' 9 | 10 | // compile fileTree(dir: 'libs', includes: ['*.jar']) 11 | 12 | compile project(':model') 13 | 14 | compile 'dive:common:0.0.1' 15 | compile 'dive:cache-mime:0.0.2' 16 | compile 'dive:http-okhttp:0.0.1' 17 | compile 'com.squareup.okhttp3:okhttp:3.11.0' 18 | 19 | compile 'org.java-websocket:Java-WebSocket:1.3.9' 20 | compile 'org.apache.commons:commons-compress:1.18' 21 | } 22 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/Action.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | /** 4 | * 请求行为动作 5 | * @author GOAi 6 | 7 | */ 8 | public enum Action { 9 | // 市场接口 10 | TICKER, 11 | KLINES, 12 | DEPTH, 13 | TRADES, 14 | 15 | // 账户接口 16 | BALANCES, 17 | ACCOUNT, 18 | 19 | // 交易接口 20 | PRECISIONS, 21 | PRECISION, 22 | BUY_LIMIT, 23 | SELL_LIMIT, 24 | BUY_MARKET, 25 | SELL_MARKET, 26 | MULTI_BUY, 27 | MULTI_SELL, 28 | CANCEL_ORDER, 29 | CANCEL_ORDERS, 30 | 31 | // 订单接口 32 | ORDERS, 33 | HISTORY_ORDERS, 34 | ORDER, 35 | ORDER_DETAILS, 36 | 37 | // 推送接口 38 | ON_TICKER, 39 | ON_KLINES, 40 | ON_DEPTH, 41 | ON_TRADES, 42 | ON_ACCOUNT, 43 | ON_ORDERS 44 | } 45 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/BaseExchange.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | import org.slf4j.Logger; 4 | 5 | /** 6 | * 交易所基本信息 7 | * @author GOAi 8 | */ 9 | public class BaseExchange { 10 | 11 | /** 12 | * 日志 13 | */ 14 | protected final Logger log; 15 | 16 | /** 17 | * 交易所名称 18 | */ 19 | protected final ExchangeName name; 20 | 21 | public BaseExchange(ExchangeName name, Logger log) { 22 | this.name = name; 23 | this.log = log; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/ExchangeError.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | /** 4 | * 交易所相关错误码 5 | * 6 | * @author GOAi 7 | */ 8 | public enum ExchangeError { 9 | // 交易所不支持 10 | EXCHANGE(1001), 11 | // 币对不支持 12 | SYMBOL(1002), 13 | // K线周期不支持 14 | PERIOD(1003); 15 | // public static final Integer ERROR_TOKEN = 1001; // 授权错误 16 | 17 | private int code; 18 | 19 | ExchangeError(int code) { 20 | this.code = code; 21 | } 22 | 23 | public int getCode() { 24 | return code; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/ExchangeException.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | 4 | import dive.common.model.Message; 5 | 6 | /** 7 | * 交易所信息处理异常 8 | * @author GOAi 9 | */ 10 | public class ExchangeException extends RuntimeException { 11 | 12 | private static final long serialVersionUID = -8633608065854161508L; 13 | 14 | private Integer code = Message.CODE_FAILED; 15 | 16 | public ExchangeException(String message) { 17 | super(message); 18 | } 19 | 20 | public ExchangeException(ExchangeError error, String message) { 21 | super(message); 22 | this.code = error.getCode(); 23 | } 24 | 25 | public ExchangeException(String message, Throwable throwable) { 26 | super(message, throwable); 27 | } 28 | 29 | public ExchangeException(ExchangeError error, String message, Throwable throwable) { 30 | super(message, throwable); 31 | this.code = error.getCode(); 32 | } 33 | 34 | 35 | public Integer getCode() { 36 | return code; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/ExchangeManager.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | import cqt.goai.exchange.http.HttpExchange; 4 | import cqt.goai.exchange.http.binance.BinanceExchange; 5 | import cqt.goai.exchange.http.bitfinex.BitfinexExchange; 6 | import cqt.goai.exchange.http.huobi.pro.HuobiProExchange; 7 | import cqt.goai.exchange.http.okexv3.Okexv3Exchange; 8 | import cqt.goai.exchange.web.socket.WebSocketExchange; 9 | import cqt.goai.exchange.web.socket.okexv3.Okexv3WebSocketExchange; 10 | import org.slf4j.Logger; 11 | 12 | /** 13 | * 统一获取Exchange对象 14 | * @author GOAi 15 | */ 16 | public class ExchangeManager { 17 | 18 | /** 19 | * 获取HttpExchange 20 | * Simple Factory Pattern 简单工厂模式 21 | * 获取实例对象即可,没有太多的拓展性 22 | * @param name 交易所名 23 | * @param log 日志 24 | */ 25 | public static HttpExchange getHttpExchange(ExchangeName name, Logger log) { 26 | if (null != name) { 27 | switch (name) { 28 | case OKEXV3: return new Okexv3Exchange(log); 29 | case BITFINEX: return new BitfinexExchange(log); 30 | case HUOBIPRO: return new HuobiProExchange(log); 31 | case BINANCE: return new BinanceExchange(log); 32 | default: 33 | } 34 | } 35 | throw new ExchangeException(ExchangeError.EXCHANGE, "exchange name is not supported"); 36 | } 37 | 38 | /** 39 | * 获取WSExchange 40 | * 简单工厂模式 41 | * @param name 交易所名 42 | * @param log 日志 43 | */ 44 | public static WebSocketExchange getWebSocketExchange(ExchangeName name, Logger log) { 45 | if (null != name) { 46 | switch (name) { 47 | case OKEXV3: return new Okexv3WebSocketExchange(log); 48 | default: 49 | } 50 | } 51 | throw new ExchangeException(ExchangeError.EXCHANGE, "exchange name is not supported"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/ExchangeName.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | import cqt.goai.exchange.util.CompleteList; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import static cqt.goai.exchange.Action.*; 10 | import static cqt.goai.exchange.Action.ORDER; 11 | import static cqt.goai.exchange.util.CompleteList.*; 12 | 13 | /** 14 | * 交易所名称 15 | * 16 | * @author GOAi 17 | */ 18 | public enum ExchangeName { 19 | 20 | /** 21 | * OKEX V3 版本, ALL 22 | * https://www.okex.com/docs/zh/ 23 | * CompleteList.ALL 24 | */ 25 | OKEXV3("okexv3", CompleteList.ALL), 26 | 27 | /** 28 | * bitfinex 29 | * https://docs.bitfinex.com/v1/docs/rest-general 30 | * https://docs.bitfinex.com/v2/docs/rest-auth 31 | * CompleteList.MARKET 32 | * CompleteList.ACCOUNT 33 | * CompleteList.TRADE 34 | * CompleteList.ORDER 35 | */ 36 | BITFINEX("bitfinex", CompleteList.exclude(PUSH)), 37 | 38 | 39 | /** 40 | * huobipro 41 | * https://github.com/huobiapi/API_Docs 42 | */ 43 | HUOBIPRO("huobipro", CompleteList.exclude(PUSH)), 44 | 45 | 46 | 47 | /** 48 | * binance 49 | * https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md 50 | */ 51 | BINANCE("binance", CompleteList.exclude(PUSH)); 52 | 53 | 54 | /** 55 | * 交易所名称,唯一识别 56 | */ 57 | private String name; 58 | 59 | /** 60 | * 已完成的功能 61 | */ 62 | private List functions; 63 | 64 | ExchangeName(String name, Action... actions) { 65 | this.name = name; 66 | this.functions = null == actions ? new ArrayList<>() : Arrays.asList(actions); 67 | } 68 | 69 | public String getName() { 70 | return this.name; 71 | } 72 | 73 | public List getFunctions() { 74 | return this.functions; 75 | } 76 | 77 | /** 78 | * 根据名称获取 79 | */ 80 | public static ExchangeName getByName(String name) { 81 | for (ExchangeName en : ExchangeName.values()) { 82 | if (en.name.equals(name)) { 83 | return en; 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return this.name; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/ExchangeUtil.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange; 2 | 3 | import dive.http.okhttp.OkHttp; 4 | 5 | /** 6 | * 工具类 7 | * @author GOAi 8 | */ 9 | public class ExchangeUtil { 10 | 11 | /** 12 | * okhttp util 13 | */ 14 | public static final OkHttp OKHTTP = new OkHttp(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/http/HistoryOrdersByTimestamp.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.http; 2 | 3 | import cqt.goai.exchange.ExchangeInfo; 4 | import cqt.goai.exchange.ExchangeName; 5 | import cqt.goai.model.trade.Orders; 6 | import dive.http.common.MimeHttp; 7 | import dive.http.common.MimeRequest; 8 | import org.slf4j.Logger; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 根据时间戳获取历史订单 14 | * 15 | * @author GOAi 16 | */ 17 | public interface HistoryOrdersByTimestamp { 18 | 19 | /** 20 | * 获取请求工具 21 | * @return 请求工具 22 | */ 23 | MimeHttp getHttp(); 24 | 25 | /** 26 | * 获取日志输出 27 | * @return 日志 28 | */ 29 | Logger getLog(); 30 | 31 | /** 32 | * 获取交易所名称 33 | * @return 交易所名称 34 | */ 35 | ExchangeName getExchangeName(); 36 | 37 | /** 38 | * 获取历史订单 39 | * @param access access 40 | * @param secret secret 41 | * @param symbol 币对 42 | * @param start 开始时间 43 | * @param end 结束时间 44 | * @return Orders 45 | */ 46 | default Orders getHistoryOrders(String symbol, String access, String secret, Long start, Long end) { 47 | List mimeRequests = this.historyOrdersRequests(symbol, access, secret, start, end, 0); 48 | List results = HttpExchange.results(mimeRequests, this.getHttp(), this.getLog()); 49 | return this.parseHistoryOrders(results, symbol, access, secret); 50 | } 51 | 52 | /** 53 | * 获取历史订单请求 54 | * @param symbol 币对 55 | * @param access access 56 | * @param secret secret 57 | * @param start 开始时间戳 58 | * @param end 截止时间戳 59 | * @param delay 延时 60 | * @return 请求 61 | */ 62 | List historyOrdersRequests(String symbol, String access, String secret, 63 | Long start, Long end, long delay); 64 | 65 | /** 66 | * 解析历史订单 67 | * @param results 请结果 68 | * @param symbol 币对 69 | * @param access access 70 | * @param secret secret 71 | * @return 订单列表 72 | */ 73 | default Orders parseHistoryOrders(List results, String symbol, String access, String secret) { 74 | return HttpExchange.analyze(results, ExchangeInfo.historyOrders(symbol, access, secret), this::transformHistoryOrders, this.getExchangeName(), this.getLog()); 75 | } 76 | 77 | /** 78 | * 解析历史订单明细 79 | * @param results 请结果 80 | * @param info 请求信息 81 | * @return 结果 82 | */ 83 | Orders transformHistoryOrders(List results, ExchangeInfo info); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/http/HistoryOrdersDetailsByTimestamp.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.http; 2 | 3 | import cqt.goai.exchange.ExchangeInfo; 4 | import cqt.goai.exchange.ExchangeName; 5 | import cqt.goai.model.trade.OrderDetails; 6 | import dive.http.common.MimeHttp; 7 | import dive.http.common.MimeRequest; 8 | import org.slf4j.Logger; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 获取历史订单明细 14 | * 15 | * @author GOAi 16 | */ 17 | public interface HistoryOrdersDetailsByTimestamp { 18 | 19 | /** 20 | * 获取请求工具 21 | * @return 请求工具 22 | */ 23 | MimeHttp getHttp(); 24 | 25 | /** 26 | * 获取日志输出 27 | * @return 日志 28 | */ 29 | Logger getLog(); 30 | 31 | /** 32 | * 获取交易所名称 33 | * @return 交易所名称 34 | */ 35 | ExchangeName getExchangeName(); 36 | 37 | /** 38 | * 获取历史订单明细 39 | * @param symbol 币对 40 | * @param access access 41 | * @param secret secret 42 | * @param start 开始时间 43 | * @param end 结束时间 44 | * @return 订单明细 45 | */ 46 | default OrderDetails getHistoryOrdersDetails(String symbol, String access, String secret, Long start, Long end) { 47 | List mimeRequests = this.historyOrdersDetailsRequests(symbol, access, secret, start, end, 0); 48 | List results = HttpExchange.results(mimeRequests, this.getHttp(), this.getLog()); 49 | return this.parseHistoryOrdersDetails(results, symbol, access, secret); 50 | 51 | } 52 | 53 | /** 54 | * 获取历史订单明细请求 55 | * @param symbol 币对 56 | * @param access access 57 | * @param secret secret 58 | * @param start 开始时间戳 59 | * @param end 截止时间戳 60 | * @param delay 延时 61 | * @return 请求 62 | */ 63 | List historyOrdersDetailsRequests(String symbol, String access, String secret, 64 | Long start, Long end, long delay); 65 | 66 | /** 67 | * 解析历史订单明细 68 | * @param results 请求结果 69 | * @param symbol 币对 70 | * @param access access 71 | * @param secret secret 72 | * @return 订单明细 73 | */ 74 | default OrderDetails parseHistoryOrdersDetails(List results, String symbol, String access, String secret) { 75 | return HttpExchange.analyze(results, ExchangeInfo.historyOrders(symbol, access, secret), this::transformHistoryOrdersDetails, this.getExchangeName(), this.getLog()); 76 | } 77 | 78 | /** 79 | * 解析历史订单明细 80 | * @param results 请求结果 81 | * @param info 请求信息 82 | * @return 结果 83 | */ 84 | OrderDetails transformHistoryOrdersDetails(List results, ExchangeInfo info); 85 | 86 | } 87 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/http/TradeByFillOrKill.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.http; 2 | 3 | import cqt.goai.exchange.ExchangeInfo; 4 | import cqt.goai.exchange.ExchangeName; 5 | import dive.http.common.MimeHttp; 6 | import dive.http.common.MimeRequest; 7 | import org.slf4j.Logger; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * fok交易 13 | * 14 | * @author GOAi 15 | */ 16 | public interface TradeByFillOrKill { 17 | 18 | /** 19 | * 获取请求工具 20 | * @return 请求工具 21 | */ 22 | MimeHttp getHttp(); 23 | 24 | /** 25 | * 获取日志输出 26 | * @return 日志 27 | */ 28 | Logger getLog(); 29 | 30 | /** 31 | * 获取交易所名称 32 | * @return 交易所名称 33 | */ 34 | ExchangeName getExchangeName(); 35 | 36 | /** 37 | * 下fok买单 38 | * @param access access 39 | * @param secret secret 40 | * @param symbol 币对 41 | * @param price 价格 42 | * @param amount 数量 43 | * @return 订单id 44 | */ 45 | default String buyFillOrKill(String symbol, String access, String secret, String price, String amount) { 46 | List mimeRequests = this.buyFillOrKillRequests(symbol, access, secret, price, amount, 0); 47 | List results = HttpExchange.results(mimeRequests, this.getHttp(), this.getLog()); 48 | return this.parseBuyFillOrKill(results, symbol, access, secret, price, amount); 49 | } 50 | 51 | /** 52 | * 获取fok买单请求 53 | * @param symbol 币对 54 | * @param access access 55 | * @param secret secret 56 | * @param price 价格 57 | * @param amount 数量 58 | * @param delay 延时 59 | * @return 请求 60 | */ 61 | List buyFillOrKillRequests(String symbol, String access, String secret, String price, String amount, long delay); 62 | 63 | /** 64 | * 解析fok订单 65 | * @param results 请结果 66 | * @param symbol 币对 67 | * @param access access 68 | * @param secret secret 69 | * @param price 价格 70 | * @param amount 数量 71 | * @return 订单id 72 | */ 73 | default String parseBuyFillOrKill(List results, String symbol, String access, String secret, String price, String amount) { 74 | return HttpExchange.analyze(results, ExchangeInfo.buyLimit(symbol, access, secret, price, amount), this::transformBuyFillOrKill, this.getExchangeName(), this.getLog()); 75 | } 76 | 77 | /** 78 | * 解析订单id 79 | * @param results 请结果 80 | * @param info 请求信息 81 | * @return 订单id 82 | */ 83 | String transformBuyFillOrKill(List results, ExchangeInfo info); 84 | 85 | 86 | /** 87 | * 下fok卖单 88 | * @param access access 89 | * @param secret secret 90 | * @param symbol 币对 91 | * @param price 价格 92 | * @param amount 数量 93 | * @return 订单id 94 | */ 95 | default String sellFillOrKill(String symbol, String access, String secret, String price, String amount) { 96 | List mimeRequests = this.sellFillOrKillRequests(symbol, access, secret, price, amount, 0); 97 | List results = HttpExchange.results(mimeRequests, this.getHttp(), this.getLog()); 98 | return this.parseSellFillOrKill(results, symbol, access, secret, price, amount); 99 | } 100 | 101 | /** 102 | * 获取fok卖单请求 103 | * @param symbol 币对 104 | * @param access access 105 | * @param secret secret 106 | * @param price 价格 107 | * @param amount 数量 108 | * @param delay 延时 109 | * @return 请求 110 | */ 111 | List sellFillOrKillRequests(String symbol, String access, String secret, String price, String amount, long delay); 112 | 113 | /** 114 | * 解析订单id 115 | * @param results 请结果 116 | * @param symbol 币对 117 | * @param access access 118 | * @param secret secret 119 | * @param price 价格 120 | * @param amount 数量 121 | * @return 订单id 122 | */ 123 | default String parseSellFillOrKill(List results, String symbol, String access, String secret, String price, String amount) { 124 | return HttpExchange.analyze(results, ExchangeInfo.buyLimit(symbol, access, secret, price, amount), this::transformSellFillOrKill, this.getExchangeName(), this.getLog()); 125 | } 126 | 127 | /** 128 | * 解析订单id 129 | * @param results 请结果 130 | * @param info 请求信息 131 | * @return 订单id 132 | */ 133 | String transformSellFillOrKill(List results, ExchangeInfo info); 134 | } 135 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/CompleteList.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util; 2 | 3 | import cqt.goai.exchange.Action; 4 | 5 | import java.util.*; 6 | import java.util.stream.Stream; 7 | 8 | import static cqt.goai.exchange.Action.*; 9 | 10 | /** 11 | * 便捷标识交易所接口的完成情况 12 | * @author GOAi 13 | */ 14 | public class CompleteList { 15 | 16 | public static Action[] ALL; 17 | public static Action[] MARKET; 18 | public static Action[] ACCOUNT; 19 | public static Action[] TRADE; 20 | public static Action[] ORDER; 21 | public static Action[] PUSH; 22 | 23 | static { 24 | MARKET = new Action[]{TICKER, KLINES, DEPTH, TRADES}; 25 | ACCOUNT = new Action[]{BALANCES, Action.ACCOUNT}; 26 | TRADE = new Action[]{PRECISIONS, PRECISION, BUY_LIMIT, SELL_LIMIT, 27 | BUY_MARKET, SELL_MARKET, MULTI_BUY, MULTI_SELL, CANCEL_ORDER, CANCEL_ORDERS}; 28 | ORDER = new Action[]{ORDERS, HISTORY_ORDERS, Action.ORDER, ORDER_DETAILS}; 29 | PUSH = new Action[]{ON_TICKER, ON_KLINES, ON_DEPTH, ON_TRADES, ON_ACCOUNT, ON_ORDERS}; 30 | Set list = new HashSet<>(); 31 | list.addAll(Arrays.asList(MARKET)); 32 | list.addAll(Arrays.asList(ACCOUNT)); 33 | list.addAll(Arrays.asList(TRADE)); 34 | list.addAll(Arrays.asList(ORDER)); 35 | list.addAll(Arrays.asList(PUSH)); 36 | ALL = list.toArray(new Action[]{}); 37 | } 38 | 39 | 40 | public static Action[] include(Object... actions) { 41 | Set set = new HashSet<>(); 42 | for (Object o : actions) { 43 | if (o instanceof Action) { 44 | set.add((Action) o); 45 | } else if ("Action[]".equals(o.getClass().getSimpleName()) 46 | && o.getClass().getComponentType() == Action.class) { 47 | set.addAll(Arrays.asList((Action[]) o)); 48 | } 49 | } 50 | return set.toArray(new Action[]{}); 51 | } 52 | 53 | public static Action[] exclude(Action[] exclude) { 54 | Set all = new HashSet<>(Arrays.asList(ALL)); 55 | Stream.of(exclude).forEach(all::remove); 56 | return all.toArray(new Action[]{}); 57 | } 58 | 59 | public static Action[] exclude(Object... actions) { 60 | Action[] exclude = CompleteList.include(actions); 61 | Set all = new HashSet<>(Arrays.asList(ALL)); 62 | Stream.of(exclude).forEach(all::remove); 63 | return all.toArray(new Action[]{}); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/Md5Util.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | /** 7 | * @author GOAi 8 | * @description 9 | */ 10 | public class Md5Util { 11 | 12 | private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', 13 | '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; 14 | 15 | public static String md5(String str) { 16 | try { 17 | if (str == null || str.trim().length() == 0) { 18 | return ""; 19 | } 20 | byte[] bytes = str.getBytes(); 21 | MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 22 | messageDigest.update(bytes); 23 | bytes = messageDigest.digest(); 24 | StringBuilder sb = new StringBuilder(); 25 | for (byte b : bytes) { 26 | sb.append(HEX_DIGITS[(b & 0xf0) >> 4]).append(HEX_DIGITS[b & 0xf]); 27 | } 28 | return sb.toString(); 29 | } catch (NoSuchAlgorithmException e) { 30 | e.printStackTrace(); 31 | } 32 | return ""; 33 | } 34 | 35 | // public static String md5(String str) { 36 | // return md5(str).toLowerCase(); 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/OkhttpWebSocket.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util; 2 | 3 | import okhttp3.*; 4 | import okio.ByteString; 5 | import org.slf4j.Logger; 6 | 7 | import java.util.function.Consumer; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * 推送回调 12 | * @author GOAi 13 | */ 14 | public class OkhttpWebSocket extends WebSocketListener { 15 | 16 | private static final OkHttpClient OKHTTP_CLIENT = new OkHttpClient(); 17 | 18 | /** 19 | * 连接url 20 | */ 21 | private final String url; 22 | 23 | /** 24 | * 收到消息转码方式 25 | */ 26 | private final Function decode; 27 | 28 | /** 29 | * 连接开始 30 | */ 31 | private final Runnable open; 32 | 33 | /** 34 | * 接收消息,转码后,交给谁 35 | */ 36 | private final Consumer receive; 37 | 38 | /** 39 | * 断开后回调 40 | */ 41 | private final Runnable closed; 42 | 43 | /** 44 | * 日志 45 | */ 46 | private Logger log; 47 | 48 | /** 49 | * 连接对象 50 | */ 51 | private WebSocket webSocket; 52 | 53 | public OkhttpWebSocket(String url, Function decode, Runnable open, 54 | Consumer receive, Runnable closed, Logger log) { 55 | this.url = url; 56 | this.decode = decode; 57 | this.open = open; 58 | this.receive = receive; 59 | this.closed = closed; 60 | this.log = log; 61 | this.connect(); 62 | } 63 | 64 | /** 65 | * 连接 66 | */ 67 | public void connect() { 68 | if (null != this.webSocket) { 69 | this.webSocket.cancel(); 70 | this.webSocket = null; 71 | } 72 | OKHTTP_CLIENT.newWebSocket(new Request.Builder() 73 | .url(this.url) 74 | .build(), this); 75 | } 76 | 77 | /** 78 | * 打开连接回调 79 | */ 80 | @Override 81 | public void onOpen(WebSocket webSocket, Response response) { 82 | this.webSocket = webSocket; 83 | if (null != this.open) { 84 | this.open.run(); 85 | } 86 | } 87 | 88 | /** 89 | * 发送消息 90 | * @param message 消息 91 | */ 92 | public void send(String message) { 93 | if (null != this.webSocket) { 94 | this.webSocket.send(message); 95 | } 96 | } 97 | 98 | /** 99 | * 收到消息 100 | */ 101 | @Override 102 | public void onMessage(WebSocket webSocket, ByteString bytes) { 103 | if (null != this.receive) { 104 | String message = this.decode.apply(bytes); 105 | this.receive.accept(message); 106 | } 107 | } 108 | 109 | /** 110 | * 连接正在关闭 111 | */ 112 | @Override 113 | public void onClosing(WebSocket webSocket, int code, String reason) { 114 | super.onClosing(webSocket, code, reason); 115 | this.log.info("onClosing --> {} {} {}", webSocket, code, reason); 116 | if (null != this.closed) { 117 | this.closed.run(); 118 | } 119 | } 120 | 121 | /** 122 | * 连接关闭回调 123 | */ 124 | @Override 125 | public void onClosed(WebSocket webSocket, int code, String reason) { 126 | super.onClosed(webSocket, code, reason); 127 | this.log.info("onClosed --> {} {} {}", webSocket, code, reason); 128 | if (null != this.closed) { 129 | this.closed.run(); 130 | } 131 | } 132 | 133 | @Override 134 | public void onFailure(WebSocket webSocket, Throwable t, Response response) { 135 | super.onFailure(webSocket, t, response); 136 | t.printStackTrace(); 137 | this.log.info("onFailure --> {} {} {}", webSocket, t, response); 138 | if (null != this.closed) { 139 | this.closed.run(); 140 | } 141 | } 142 | 143 | /** 144 | * 主动断开 145 | * @param code code 146 | * @param reason reason 147 | */ 148 | public void close(int code, String reason) { 149 | if (null != this.webSocket) { 150 | this.webSocket.close(code, reason); 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/RateLimit.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util; 2 | 3 | /** 4 | * 频率限制 5 | * 2种限制 6 | * 1. 主动推送限制频率,limit毫秒内只有一次 7 | * 2. 超时限制,推送则更新,30s未推送需要重连 8 | * 9 | * @author GOAi 10 | */ 11 | public class RateLimit { 12 | 13 | /** 14 | * 无限制 15 | */ 16 | public static final RateLimit NO = new RateLimit(0); 17 | 18 | /** 19 | * 上次true时间 20 | * 这里判断时间应当非常快没有阻塞的,就不考虑并发问题了,反正限制的频率都是主动推送,不必那么精确的 21 | * AtomicLong 和 Lock 都可以实现并发安全,但是得不偿失,因为即使多推送一次,无伤大雅 22 | */ 23 | private long last; 24 | 25 | /** 26 | * 频率限制,若为5000,则在下个5000毫秒内,不会返回true 27 | */ 28 | private final long limit; 29 | 30 | private RateLimit(long limit) { 31 | this.limit = limit; 32 | this.last = 0; 33 | } 34 | 35 | /** 36 | * 获取实例 37 | * @param milliseconds 毫秒 38 | * @return 实例 39 | */ 40 | public static RateLimit limit(long milliseconds) { 41 | return new RateLimit(milliseconds); 42 | } 43 | 44 | public static RateLimit second() { return new RateLimit(1000); } 45 | public static RateLimit second5() { return new RateLimit(1000 * 5); } 46 | public static RateLimit second10() { return new RateLimit(1000 * 10); } 47 | public static RateLimit second15() { return new RateLimit(1000 * 15); } 48 | public static RateLimit second20() { return new RateLimit(1000 * 20); } 49 | public static RateLimit second30() { return new RateLimit(1000 * 30); } 50 | public static RateLimit second3() { return new RateLimit(1000 * 3); } 51 | public static RateLimit second13() { return new RateLimit(1000 * 13); } 52 | public static RateLimit second23() { return new RateLimit(1000 * 23); } 53 | 54 | /** 55 | * limit内不会再次返回true 56 | * @param update 是否更新 57 | * @return 是否到了下一次 58 | */ 59 | public boolean timeout(boolean update) { 60 | if (limit <= 0) { 61 | return true; 62 | } 63 | long now = System.currentTimeMillis(); 64 | boolean result = last + limit < now; 65 | if (update && result) { 66 | this.last = now; 67 | } 68 | return result; 69 | } 70 | 71 | /** 72 | * limit内不会再次返回true 73 | * @return 是否到了下一次 74 | */ 75 | public boolean timeout() { 76 | return this.timeout(true); 77 | } 78 | 79 | /** 80 | * 更新时间, 收到消息更新一下 81 | */ 82 | public void update() { 83 | this.last = System.currentTimeMillis(); 84 | } 85 | 86 | /** 87 | * 是否有限制 88 | * @return 是否有限制 89 | */ 90 | public boolean isLimit() { 91 | return 0 < this.limit; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "RateLimit{" + 97 | "limit=" + limit + 98 | '}'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/Seal.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util; 2 | 3 | 4 | import static dive.common.util.Util.useful; 5 | 6 | /** 7 | * 隐藏字符串 8 | * @author GOAi 9 | */ 10 | public class Seal { 11 | /** 12 | * 隐藏字符串 13 | */ 14 | public static String seal(String secret) { 15 | if (!useful(secret)) { 16 | return ""; 17 | } 18 | int length = secret.length(); 19 | switch (length) { 20 | case 1: return "*"; 21 | case 2: return "**"; 22 | default: 23 | } 24 | int margin = 5; 25 | int div = length / 3; 26 | if (margin <= div) { 27 | return secret.substring(0, margin) 28 | + getStar(length - margin * 2) 29 | + secret.substring(length - margin); 30 | } else { 31 | return secret.substring(0, div) 32 | + getStar(length - div * 2) 33 | + secret.substring(length - div); 34 | } 35 | } 36 | 37 | /** 38 | * 获取指定长度的* 39 | */ 40 | private static String getStar(int length) { 41 | StringBuilder sb = new StringBuilder(); 42 | while (sb.length() < length) { 43 | sb.append("*"); 44 | } 45 | return sb.toString(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/util/okexv3/Okexv3Util.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.util.okexv3; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import cqt.goai.exchange.util.CommonUtil; 6 | import cqt.goai.model.enums.Period; 7 | import cqt.goai.model.enums.Side; 8 | import cqt.goai.model.enums.State; 9 | import cqt.goai.model.enums.Type; 10 | import cqt.goai.model.market.*; 11 | import cqt.goai.model.trade.Balance; 12 | import cqt.goai.model.trade.Order; 13 | import cqt.goai.model.trade.Orders; 14 | import okio.ByteString; 15 | import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.math.BigDecimal; 21 | import java.math.RoundingMode; 22 | import java.util.ArrayList; 23 | import java.util.LinkedList; 24 | import java.util.List; 25 | import java.util.stream.Collectors; 26 | 27 | import static dive.common.math.BigDecimalUtil.div; 28 | import static dive.common.math.BigDecimalUtil.greater; 29 | import static java.math.BigDecimal.ZERO; 30 | 31 | /** 32 | * @author GOAi 33 | */ 34 | public class Okexv3Util { 35 | 36 | /** 37 | * okexv3 的周期换算 38 | * @param period 周期 39 | * @return 周期长度 秒 40 | */ 41 | public static Integer getPeriod(Period period) { 42 | switch (period) { 43 | case MIN1: 44 | case MIN3: 45 | case MIN5: 46 | case MIN15: 47 | case MIN30: 48 | case HOUR1: 49 | case HOUR2: 50 | // case HOUR3 : return 7200; 51 | case HOUR4: 52 | case HOUR6: 53 | case HOUR12: 54 | case DAY1: 55 | // case DAY3 : return 604800; 56 | case WEEK1: 57 | return period.getValue(); 58 | // case WEEK2 : return 604800; 59 | // case MONTH1 : return 604800; 60 | default: 61 | return null; 62 | } 63 | } 64 | 65 | 66 | /** 67 | * http 和 ws 解析方式相同 68 | * @param result 原始文件 69 | * @param r json 70 | * @return Ticker 71 | */ 72 | public static Ticker parseTicker(String result, JSONObject r) { 73 | /* { 74 | * "instrument_id":"ETH-USDT", 75 | * "last":"131.6299", 76 | * "best_bid":"131.6299", 77 | * "best_ask":"131.6857", 78 | * "open_24h":"149.8823", 79 | * "high_24h":"150.5", 80 | * "low_24h":"130.9743", 81 | * "base_volume_24h":"879069.0066515", 82 | * "quote_volume_24h":"124634731.57238156", 83 | * "timestamp":"2019-01-10T09:26:55.932Z" 84 | * } 85 | */ 86 | Long time = r.getDate("timestamp").getTime(); 87 | BigDecimal open = r.getBigDecimal("open_24h"); 88 | BigDecimal high = r.getBigDecimal("high_24h"); 89 | BigDecimal low = r.getBigDecimal("low_24h"); 90 | BigDecimal last = r.getBigDecimal("last"); 91 | BigDecimal volume = r.getBigDecimal("base_volume_24h"); 92 | 93 | return new Ticker(result, time, open, high, low, last, volume); 94 | } 95 | 96 | /** 97 | * ws 解析方式 98 | * @param result 原始文件 99 | * @param r json 100 | * @return Kline 101 | */ 102 | public static Kline parseKline(String result, JSONArray r) { 103 | /* {"candle": 104 | * ["2019-01-15T09:51:00.000Z", 105 | * "3594.0579", 106 | * "3595.5917", 107 | * "3593.9239", 108 | * "3595.5917", 109 | * "19.24375778"], 110 | * "instrument_id":"BTC-USDT"} 111 | */ 112 | Long time = r.getDate(0).getTime() / 1000; 113 | 114 | return CommonUtil.parseKlineByIndex(result, r, time, 1); 115 | } 116 | 117 | /** 118 | * 统一解析Depth 119 | */ 120 | public static Depth parseDepth(JSONObject r) { 121 | Long time = r.getDate("timestamp").getTime(); 122 | return CommonUtil.parseDepthByIndex(time, r.getJSONArray("asks"), r.getJSONArray("bids")); 123 | } 124 | 125 | /** 126 | * 统一解析Trades 127 | */ 128 | public static Trades parseTrades(JSONArray r) { 129 | List trades = new ArrayList<>(r.size()); 130 | for (int i = 0, l = r.size(); i < l; i++) { 131 | /* 132 | * [ 133 | * { 134 | * "time":"2019-01-11T14:02:02.536Z", 135 | * "timestamp":"2019-01-11T14:02:02.536Z", 136 | * "trade_id":"861471459", 137 | * "price":"3578.8482", 138 | * "size":"0.0394402", 139 | * "side":"sell" 140 | * }, 141 | */ 142 | JSONObject t = r.getJSONObject(i); 143 | Long time = t.getDate("timestamp").getTime(); 144 | String id = t.getString("trade_id"); 145 | Side side = Side.valueOf(t.getString("side").toUpperCase()); 146 | BigDecimal price = t.getBigDecimal("price"); 147 | BigDecimal amount = t.getBigDecimal("size"); 148 | Trade trade = new Trade(r.getString(i), time, id, side, price, amount); 149 | trades.add(trade); 150 | } 151 | return new Trades(trades.stream() 152 | .sorted((t1, t2) -> t2.getTime().compareTo(t1.getTime())) 153 | .collect(Collectors.toList())); 154 | } 155 | 156 | /** 157 | * 解析单个币种余额 158 | * 159 | * @param result 原始数据 160 | * @param r json 161 | * @return Balance 162 | */ 163 | public static Balance parseBalance(String result, JSONObject r) { 164 | /* 165 | * { 166 | * "frozen":"0", 167 | * "hold":"0", 168 | * "id":"6278097", 169 | * "currency":"USDT", 170 | * "balance":"127.613453580677953", 171 | * "available":"127.613453580677953", 172 | * "holds":"0" 173 | * } 174 | */ 175 | return new Balance(result, r.getString("currency"), 176 | r.getBigDecimal("available"), r.getBigDecimal("hold")); 177 | } 178 | 179 | /** 180 | * 解析Order 181 | * @param result 原始数据 182 | * @param t json 183 | * @return Order 184 | */ 185 | public static Order parseOrder(String result, JSONObject t) { 186 | /* 187 | * { 188 | * "order_id": "125678", 189 | * "notional": "12.4", 190 | * "price": "0.10000000", 191 | * "size": "0.01000000", 192 | * "instrument_id": "BTC-USDT", 193 | * "side": "buy", 194 | * "type": "limit", 195 | * "timestamp": "2016-12-08T20:02:28.538Z", 196 | * "filled_size": "0.00000000", 197 | * "filled_notional": "0.0000000000000000", 198 | * "status": "open" 199 | * } 200 | */ 201 | Long time = t.getDate("timestamp").getTime(); 202 | String id = t.getString("order_id"); 203 | Side side = Side.valueOf(t.getString("side").toUpperCase()); 204 | Type type = Type.valueOf(t.getString("type").toUpperCase()); 205 | State state = null; 206 | switch (t.getString("status")) { 207 | case "open": 208 | state = State.SUBMIT; 209 | break; 210 | case "part_filled": 211 | state = State.PARTIAL; 212 | break; 213 | case "canceling": 214 | state = State.CANCEL; 215 | break; 216 | case "filled": 217 | state = State.FILLED; 218 | break; 219 | case "cancelled": 220 | state = State.CANCEL; 221 | break; 222 | case "failure": 223 | state = State.CANCEL; 224 | break; 225 | case "ordering": 226 | state = State.SUBMIT; 227 | break; 228 | default: 229 | } 230 | BigDecimal price = t.getBigDecimal("price"); 231 | BigDecimal amount = t.getBigDecimal("size"); 232 | BigDecimal deal = t.getBigDecimal("filled_size"); 233 | BigDecimal average = ZERO; 234 | if (greater(deal, ZERO)) { 235 | average = div(t.getBigDecimal("filled_notional"), deal, 8, RoundingMode.HALF_UP); 236 | if (State.CANCEL.equals(state)) { 237 | state = State.UNDONE; 238 | } 239 | } 240 | return new Order(result, time, id, side, type, state, price, amount, deal, average); 241 | } 242 | 243 | /** 244 | * 统一解析orders 245 | * @param r json 246 | * @return orders 247 | */ 248 | public static Orders parseOrders(JSONArray r) { 249 | List orders = new LinkedList<>(); 250 | for (int i = 0; i< r.size(); i++) { 251 | orders.add(Okexv3Util.parseOrder(r.getString(i), r.getJSONObject(i))); 252 | } 253 | return new Orders(orders); 254 | } 255 | /** 256 | * WebSocket解压缩函数 257 | * @param bytes 压缩字符串 258 | * @return 解压缩后结果 259 | */ 260 | public static String uncompress(ByteString bytes) { 261 | try (final ByteArrayOutputStream out = new ByteArrayOutputStream(); 262 | final ByteArrayInputStream in = new ByteArrayInputStream(bytes.toByteArray()); 263 | final Deflate64CompressorInputStream zin = new Deflate64CompressorInputStream(in)) { 264 | final byte[] buffer = new byte[1024]; 265 | int offset; 266 | while (-1 != (offset = zin.read(buffer))) { 267 | out.write(buffer, 0, offset); 268 | } 269 | return out.toString(); 270 | } catch (final IOException e) { 271 | throw new RuntimeException(e); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/web/socket/BaseWebSocketClient.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.web.socket; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | import cqt.goai.model.enums.Period; 5 | import cqt.goai.model.market.Klines; 6 | import cqt.goai.model.market.Depth; 7 | import cqt.goai.model.market.Ticker; 8 | import cqt.goai.model.market.Trades; 9 | import cqt.goai.model.trade.Account; 10 | import cqt.goai.model.trade.Orders; 11 | import org.slf4j.Logger; 12 | 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.function.Consumer; 15 | 16 | import static dive.common.util.Util.exist; 17 | 18 | 19 | /** 20 | * WebSocket连接 21 | * @author GOAi 22 | */ 23 | public abstract class BaseWebSocketClient { 24 | 25 | /** 26 | * 日志 27 | */ 28 | protected final Logger log; 29 | 30 | /** 31 | * 币对 32 | */ 33 | protected final String symbol; 34 | 35 | /** 36 | * access 37 | */ 38 | protected final String access; 39 | 40 | /** 41 | * secret 42 | */ 43 | protected final String secret; 44 | 45 | /** 46 | * 连接ping的频率 本链接最近更新频率, 高于该频率不发送ping 47 | */ 48 | protected final RateLimit limit; 49 | 50 | /** 51 | * 消费函数 52 | */ 53 | private Consumer onTicker; 54 | 55 | private ConcurrentHashMap> onKlines = new ConcurrentHashMap<>(); 56 | 57 | private Consumer onDepth; 58 | 59 | private Consumer onTrades; 60 | 61 | private Consumer onAccount; 62 | 63 | private Consumer onOrders; 64 | 65 | public BaseWebSocketClient(String symbol, String access, String secret, RateLimit limit, Logger log) { 66 | this.symbol = symbol; 67 | this.access = access; 68 | this.secret = secret; 69 | this.limit = limit; 70 | this.log = log; 71 | } 72 | 73 | /** 74 | * 第一次连接需要做的 75 | */ 76 | public abstract void open(); 77 | 78 | /** 79 | * 连接断开 80 | */ 81 | public abstract void closed(); 82 | 83 | /** 84 | * 发送message 85 | * @param message message 86 | */ 87 | protected abstract void send(String message); 88 | 89 | /** 90 | * ping 91 | */ 92 | public void ping() { 93 | if (this.limit.timeout(true)) { 94 | this.send("ping"); 95 | } 96 | } 97 | 98 | /** 99 | * 处理收到的新信息 100 | * @param message 收到信息 101 | */ 102 | protected abstract void transform(String message); 103 | 104 | /** 105 | * 主动断开 106 | * @param code code 107 | * @param reason reason 108 | */ 109 | public abstract void close(int code, String reason); 110 | 111 | /** 112 | * 命令日志 113 | * @param command 命令 114 | * @param url url 115 | */ 116 | protected void commandLog(String command, String url) { 117 | this.log.info("websocket send: {} to {}", command, url); 118 | } 119 | // ===================== ticker ===================== 120 | 121 | /** 122 | * 注册推送Ticker 123 | * @param onTicker 推送函数 124 | */ 125 | public void onTicker(Consumer onTicker) { 126 | this.onTicker = onTicker; 127 | this.askTicker(); 128 | } 129 | 130 | /** 131 | * 发送订阅信息 132 | */ 133 | protected abstract void askTicker(); 134 | 135 | /** 136 | * 接收到Ticker后 137 | * @param ticker ticker 138 | */ 139 | protected void onTicker(Ticker ticker) { 140 | if (exist(this.onTicker)) { 141 | this.onTicker.accept(ticker); 142 | } 143 | } 144 | 145 | /** 146 | * 取消推送Ticker 147 | */ 148 | public abstract void noTicker(); 149 | 150 | // ===================== klines ===================== 151 | 152 | /** 153 | * 注册推送Klines 154 | * @param onKlines 推送函数 155 | * @param period 周期 156 | */ 157 | public void onKlines(Consumer onKlines, Period period) { 158 | this.onKlines.put(period, onKlines); 159 | this.askKlines(period); 160 | } 161 | 162 | /** 163 | * 发送订阅信息 164 | * @param period 周期 165 | */ 166 | protected abstract void askKlines(Period period); 167 | 168 | /** 169 | * 接收到Ticker后 170 | * @param klines klines 171 | * @param period 周期 172 | */ 173 | protected void onKlines(Klines klines, Period period) { 174 | if (this.onKlines.containsKey(period)) { 175 | this.onKlines.get(period).accept(klines); 176 | } 177 | } 178 | 179 | /** 180 | * 取消推送Klines 181 | * @param period 周期 182 | */ 183 | public abstract void noKlines(Period period); 184 | 185 | // ===================== depth ===================== 186 | 187 | /** 188 | * 注册推送Depth 189 | * @param onDepth 推送函数 190 | */ 191 | public void onDepth(Consumer onDepth) { 192 | this.onDepth = onDepth; 193 | this.askDepth(); 194 | } 195 | 196 | /** 197 | * 发送订阅信息 198 | */ 199 | protected abstract void askDepth(); 200 | 201 | /** 202 | * 接收到Depth后 203 | * @param depth depth 204 | */ 205 | protected void onDepth(Depth depth) { 206 | if (exist(this.onDepth)) { 207 | this.onDepth.accept(depth); 208 | } 209 | } 210 | 211 | /** 212 | * 取消推送Ticker 213 | */ 214 | public abstract void noDepth(); 215 | 216 | 217 | // ===================== trades ===================== 218 | 219 | /** 220 | * 注册推送Trades 221 | * @param onTrades 推送函数 222 | */ 223 | public void onTrades(Consumer onTrades) { 224 | this.onTrades = onTrades; 225 | this.askTrades(); 226 | } 227 | 228 | /** 229 | * 发送订阅信息 230 | */ 231 | protected abstract void askTrades(); 232 | 233 | /** 234 | * 接收到Trades后 235 | * @param trades Trades 236 | */ 237 | protected void onTrades(Trades trades) { 238 | if (exist(this.onTrades)) { 239 | this.onTrades.accept(trades); 240 | } 241 | } 242 | 243 | /** 244 | * 取消推送Trades 245 | */ 246 | public abstract void noTrades(); 247 | 248 | 249 | // ===================== account ===================== 250 | 251 | /** 252 | * 注册推送Account 253 | * @param onAccount 推送函数 254 | */ 255 | public void onAccount(Consumer onAccount) { 256 | this.onAccount = onAccount; 257 | this.askAccount(); 258 | } 259 | 260 | /** 261 | * 发送订阅信息 262 | */ 263 | protected abstract void askAccount(); 264 | 265 | /** 266 | * 接收到Account后 267 | * @param account account 268 | */ 269 | protected void onAccount(Account account) { 270 | if (exist(this.onAccount)) { 271 | this.onAccount.accept(account); 272 | } 273 | } 274 | 275 | /** 276 | * 取消推送Account 277 | */ 278 | public abstract void noAccount(); 279 | 280 | // ===================== orders ===================== 281 | 282 | /** 283 | * 注册推送Orders 284 | * @param onOrders 推送函数 285 | */ 286 | public void onOrders(Consumer onOrders) { 287 | this.onOrders = onOrders; 288 | this.askOrders(); 289 | } 290 | 291 | /** 292 | * 发送订阅信息 293 | */ 294 | protected abstract void askOrders(); 295 | 296 | /** 297 | * 接收到Orders后 298 | * @param orders orders 299 | */ 300 | protected void onOrders(Orders orders) { 301 | if (exist(this.onOrders)) { 302 | this.onOrders.accept(orders); 303 | } 304 | } 305 | 306 | /** 307 | * 取消推送Orders 308 | */ 309 | public abstract void noOrders(); 310 | 311 | } 312 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/web/socket/WebSocketExchange.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.web.socket; 2 | 3 | import cqt.goai.exchange.BaseExchange; 4 | import cqt.goai.exchange.ExchangeException; 5 | import cqt.goai.exchange.ExchangeInfo; 6 | import cqt.goai.exchange.ExchangeName; 7 | import cqt.goai.model.enums.Period; 8 | import cqt.goai.model.market.Klines; 9 | import cqt.goai.model.market.Depth; 10 | import cqt.goai.model.market.Ticker; 11 | import cqt.goai.model.market.Trades; 12 | import cqt.goai.model.trade.Account; 13 | import cqt.goai.model.trade.Orders; 14 | import dive.common.math.RandomUtil; 15 | import org.slf4j.Logger; 16 | 17 | import java.util.concurrent.*; 18 | import java.util.function.Consumer; 19 | 20 | /** 21 | * WebSocket请求,每个交易所的WebSocket请求方式都要继承该类 22 | * @author GOAi 23 | */ 24 | public class WebSocketExchange extends BaseExchange { 25 | 26 | /** 27 | * 每个连接加入ping的id,取消时用 28 | */ 29 | private String id; 30 | 31 | /** 32 | * 需要ping的连接 33 | */ 34 | private static final ConcurrentHashMap EXCHANGES = new ConcurrentHashMap<>(); 35 | 36 | static { 37 | ScheduledExecutorService timer = new ScheduledThreadPoolExecutor(1, new ThreadPoolExecutor.DiscardPolicy()); 38 | timer.scheduleAtFixedRate(() -> EXCHANGES.values().stream().parallel().forEach(WebSocketExchange::ping), 7, 7, TimeUnit.SECONDS); 39 | } 40 | 41 | public WebSocketExchange(ExchangeName name, Logger log) { 42 | super(name, log); 43 | this.ping(this); 44 | } 45 | 46 | /** 47 | * 定时某些交易所需要定时向服务器发送ping 48 | */ 49 | protected void ping() {} 50 | 51 | /** 52 | * 加入ping 53 | * @param e ping对象 54 | */ 55 | private void ping(WebSocketExchange e) { 56 | if (null != this.id && EXCHANGES.containsKey(this.id)) { 57 | return; 58 | } 59 | this.id = RandomUtil.token(); 60 | EXCHANGES.put(this.id, e); 61 | } 62 | 63 | /** 64 | * 取消ping 65 | */ 66 | protected void noPing() { 67 | EXCHANGES.remove(this.id); 68 | } 69 | 70 | // =============== ticker =============== 71 | 72 | /** 73 | * 获取Ticker 74 | * @param info 请求信息 75 | * @param onTicker 消费函数 76 | * @return 订阅id 77 | */ 78 | public boolean onTicker(ExchangeInfo info, Consumer onTicker) { 79 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onTicker is not supported."); 80 | } 81 | 82 | public void noTicker(String pushId) { 83 | throw new ExchangeException(super.name + " noTicker is not supported. pushId: " + pushId); 84 | } 85 | 86 | // =============== klines =============== 87 | 88 | /** 89 | * 获取Klines 90 | * @param info 请求信息 91 | * @param onKlines 消费函数 92 | * @return 订阅id 93 | */ 94 | public boolean onKlines(ExchangeInfo info, Consumer onKlines) { 95 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onKlines " + info.getPeriod() + " is not supported."); 96 | } 97 | 98 | public void noKlines(Period period, String pushId) { 99 | throw new ExchangeException(super.name + " noKlines " + period + " is not supported. pushId: " + pushId); 100 | } 101 | 102 | // =============== depth =============== 103 | 104 | /** 105 | * 获取Depth 106 | * @param info 请求信息 107 | * @param onDepth 消费函数 108 | * @return 订阅id 109 | */ 110 | public boolean onDepth(ExchangeInfo info, Consumer onDepth) { 111 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onDepth is not supported."); 112 | } 113 | 114 | public void noDepth(String pushId) { 115 | throw new ExchangeException(super.name + " noDepth is not supported. pushId: " + pushId); 116 | } 117 | 118 | // =============== trades =============== 119 | 120 | /** 121 | * 获取Trades 122 | * @param info 请求信息 123 | * @param onTrades 消费函数 124 | * @return 订阅id 125 | */ 126 | public boolean onTrades(ExchangeInfo info, Consumer onTrades) { 127 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onTrades is not supported."); 128 | } 129 | 130 | public void noTrades(String pushId) { 131 | throw new ExchangeException(super.name + " noTrades is not supported. pushId: " + pushId); 132 | } 133 | 134 | // =============== account =============== 135 | 136 | /** 137 | * 获取Account 138 | * @param info 请求信息 139 | * @param onAccount 消费函数 140 | * @return 订阅id 141 | */ 142 | public boolean onAccount(ExchangeInfo info, Consumer onAccount) { 143 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onAccount is not supported."); 144 | } 145 | 146 | public void noAccount(String pushId) { 147 | throw new ExchangeException(super.name + " noAccount is not supported. pushId: " + pushId); 148 | } 149 | 150 | // =============== orders =============== 151 | 152 | /** 153 | * 获取Orders 154 | * @param info 请求信息 155 | * @param onOrders 消费函数 156 | * @return 订阅id 157 | */ 158 | public boolean onOrders(ExchangeInfo info, Consumer onOrders) { 159 | throw new ExchangeException(super.name + " " + info.getSymbol() + " onOrders is not supported."); 160 | } 161 | 162 | public void noOrders(String pushId) { 163 | throw new ExchangeException(super.name + " noOrders is not supported. pushId: " + pushId); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /engine/exchange/src/main/java/cqt/goai/exchange/web/socket/WebSocketInfo.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.exchange.web.socket; 2 | 3 | import cqt.goai.exchange.Action; 4 | import cqt.goai.model.enums.Period; 5 | import lombok.Data; 6 | 7 | import java.util.Objects; 8 | import java.util.function.Consumer; 9 | 10 | /** 11 | * WebSocket推送封装的信息 12 | * @author GOAi 13 | */ 14 | @Data 15 | public class WebSocketInfo { 16 | 17 | /** 18 | * 推送id 19 | */ 20 | private String id; 21 | 22 | /** 23 | * 推送行为类型 24 | */ 25 | private Action action; 26 | 27 | /** 28 | * 推送币对 29 | */ 30 | private String symbol; 31 | 32 | /** 33 | * 推送用户 34 | */ 35 | private String access; 36 | 37 | /** 38 | * 消费函数 39 | */ 40 | private Consumer consumer; 41 | 42 | /** 43 | * 周期 44 | */ 45 | private Period period; 46 | 47 | public WebSocketInfo(String id, Action action, String symbol, String access, Consumer consumer) { 48 | this.id = id; 49 | this.action = action; 50 | this.symbol = symbol; 51 | this.access = access; 52 | this.consumer = consumer; 53 | } 54 | 55 | public WebSocketInfo(String id, Action action, String symbol, String access, Consumer consumer, Period period) { 56 | this.id = id; 57 | this.action = action; 58 | this.symbol = symbol; 59 | this.access = access; 60 | this.consumer = consumer; 61 | this.period = period; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) { 67 | return true; 68 | } 69 | if (o == null || getClass() != o.getClass()) { 70 | return false; 71 | } 72 | WebSocketInfo onInfo = (WebSocketInfo) o; 73 | return Objects.equals(id, onInfo.id); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return Objects.hash(id); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /engine/exchange/src/test/java/test/RateLimitTest.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | /** 8 | * @author GOAi 9 | */ 10 | public class RateLimitTest { 11 | 12 | @Test 13 | public void test() throws InterruptedException { 14 | RateLimit NO = RateLimit.NO; 15 | for (int i = 0; i < 10000; i++) { 16 | // 次次需要更新 17 | Assert.assertTrue(NO.timeout(false)); 18 | } 19 | 20 | RateLimit limit = RateLimit.limit(300); 21 | Assert.assertFalse(limit.timeout()); // 300毫秒之内,不需要推送 22 | Thread.sleep(200); 23 | Assert.assertFalse(limit.timeout()); // 300毫秒之内,不需要推送 24 | Thread.sleep(200); 25 | Assert.assertTrue(limit.timeout()); // 300毫秒之外,需要推送 26 | Assert.assertFalse(limit.timeout()); // 300毫秒之内,不需要推送 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /engine/model/build.gradle: -------------------------------------------------------------------------------- 1 | jar { 2 | enabled = true 3 | baseName = 'goai-model' 4 | version = '1.0.0' 5 | } 6 | 7 | dependencies { 8 | testCompile 'junit:junit:4.12' 9 | testCompile 'org.slf4j:slf4j-log4j12:1.7.25' 10 | 11 | compile 'org.slf4j:slf4j-api:1.7.25' 12 | compile 'com.alibaba:fastjson:1.2.49' 13 | } -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/BaseModelList.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | 6 | import java.io.Serializable; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * 处理list情况 14 | * 15 | * 封装基本list,额外添加 to 和 of 方法 16 | * Iterator Pattern 迭代器模式 17 | * 18 | * @author GOAi 19 | * @param list的对象 20 | */ 21 | @Getter 22 | @EqualsAndHashCode 23 | public abstract class BaseModelList implements To, Iterable, Serializable { 24 | private static final long serialVersionUID = 1L; 25 | /** 26 | * 底层数据 27 | */ 28 | private List list; 29 | 30 | /** 31 | * 构造器传入list对象,可选择ArrayList或LinkedList,看情况选择 32 | * @param list list 33 | */ 34 | public BaseModelList(List list) { 35 | this.list = list; 36 | } 37 | 38 | @Override 39 | public String to() { 40 | return Util.to(this.list); 41 | } 42 | 43 | 44 | public int size() { 45 | return this.list.size(); 46 | } 47 | 48 | public boolean isEmpty() { 49 | return this.list.isEmpty(); 50 | } 51 | 52 | public boolean contains(E e) { 53 | return this.list.contains(e); 54 | } 55 | 56 | @Override 57 | public Iterator iterator() { 58 | return this.list.iterator(); 59 | } 60 | 61 | public Object[] toArray() { 62 | return this.list.toArray(); 63 | } 64 | 65 | public E[] toArray(E[] a) { 66 | return this.list.toArray(a); 67 | } 68 | 69 | public E get(int index) { 70 | return this.list.get(index); 71 | } 72 | 73 | public E remove(int index) { 74 | return this.list.remove(index); 75 | } 76 | 77 | public int indexOf(E e) { 78 | return this.list.indexOf(e); 79 | } 80 | 81 | public int lastIndexOf(E e) { 82 | return this.list.lastIndexOf(e); 83 | } 84 | 85 | public Stream stream() { 86 | return this.list.stream(); 87 | } 88 | 89 | public Stream parallelStream() { 90 | return this.list.parallelStream(); 91 | } 92 | 93 | public E first() { 94 | return this.get(0); 95 | } 96 | 97 | public E last() { 98 | return this.get(this.size() - 1); 99 | } 100 | 101 | public E getFirst() { 102 | return this.first(); 103 | } 104 | 105 | public E getLast() { 106 | return this.last(); 107 | } 108 | 109 | public void add(E e) { 110 | this.list.add(e); 111 | } 112 | 113 | public void set(List eList) { 114 | this.list = eList; 115 | } 116 | 117 | @Override 118 | public String toString() { 119 | return list.toString(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/To.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model; 2 | 3 | /** 4 | * 对象转换 5 | * @author GOAi 6 | */ 7 | public interface To { 8 | 9 | /** 10 | * 转字符串 11 | * @return 字符串 12 | */ 13 | String to(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/Util.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONArray; 5 | import cqt.goai.model.trade.Balance; 6 | import org.slf4j.Logger; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.ArrayList; 10 | import java.util.Base64; 11 | import java.util.List; 12 | import java.util.function.Function; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * 工具类 17 | * @author GOAi 18 | */ 19 | public class Util { 20 | 21 | /** 22 | * 去除末尾的0, 保存时应该检查,有些交易所的数据末尾0乱七八糟的,不如都不要 23 | * @param number 待结尾数字 24 | * @return 结尾后数字 25 | */ 26 | public static BigDecimal strip(BigDecimal number) { 27 | return null != number ? 28 | new BigDecimal(number.stripTrailingZeros().toPlainString()) : null; 29 | } 30 | 31 | /** 32 | * 处理Long数据 33 | */ 34 | public static String to(Long time) { 35 | if (null == time) { 36 | return "null"; 37 | } 38 | return String.valueOf(time); 39 | } 40 | 41 | /** 42 | * 处理BigDecimal数据 43 | */ 44 | public static String to(BigDecimal number) { 45 | if (null == number) { 46 | return "null"; 47 | } 48 | return number.toPlainString(); 49 | } 50 | 51 | /** 52 | * 处理String数据 53 | */ 54 | public static String to(String content) { 55 | if (null == content) { 56 | return "null"; 57 | } 58 | return '\"' + content + '\"'; 59 | } 60 | 61 | /** 62 | * 处理ModelList数据 63 | */ 64 | public static String to(BaseModelList list) { 65 | if (null == list) { 66 | return "null"; 67 | } 68 | return list.to(); 69 | } 70 | 71 | /** 72 | * 处理枚举数据 73 | */ 74 | public static String to(Enum e) { 75 | if (null == e) { 76 | return "null"; 77 | } 78 | return '\"' + e.name() + '\"'; 79 | } 80 | 81 | /** 82 | * 处理整型数据 83 | */ 84 | public static String to(Integer number) { 85 | if (null == number) { 86 | return "null"; 87 | } 88 | return String.valueOf(number); 89 | } 90 | 91 | /** 92 | * 处理Balance数据 93 | */ 94 | public static String to(Balance balance) { 95 | if (null == balance) { 96 | return "null"; 97 | } 98 | return balance.to(); 99 | } 100 | 101 | /** 102 | * list 转换字符串数组 103 | * @param list list 104 | * @param 元素类型 105 | * @return 字符串数组 106 | */ 107 | public static String to(List list) { 108 | if (null == list) { 109 | return null; 110 | } 111 | if (0 == list.size()) { 112 | return "[]"; 113 | } 114 | return '[' + 115 | list.stream() 116 | .map(To::to) 117 | .collect(Collectors.joining(",")) + 118 | ']'; 119 | } 120 | 121 | /** 122 | * data数据传输时变成Base64编码 123 | * @param data 原始数据 124 | * @return Base64编码 125 | */ 126 | public static String encode(String data) { 127 | return null == data || "null".equals(data) ? 128 | "null" : ('\"' + Base64.getEncoder().encodeToString(data.getBytes()) + '\"'); 129 | } 130 | 131 | /** 132 | * data数据存入时解码 133 | * @param data Base64编码 134 | * @return 原始数据 135 | */ 136 | public static String decode(String data) { 137 | return null == data || "null".equals(data) ? 138 | null : new String(Base64.getDecoder().decode(data.getBytes())); 139 | } 140 | 141 | /** 142 | * 单个值还原 143 | * @param data 文本数据 144 | * @param of 转化方法 145 | * @param 结果对象 146 | * @return 转化结果 147 | */ 148 | public static E of(String data, Function of) { 149 | return null == data || "null".equals(data) ? null : of.apply(data); 150 | } 151 | 152 | /** 153 | * 可能组合成ModelList的元素,在上级就已经转变成JSONArray了 154 | * 本身的of(String data)不能使用,则重载个of(JSONArray r)方法 155 | * @param data 数组数据 156 | * @param of 解析JSONArray对象 157 | * @param log 错误输出 158 | * @return 解析结果 159 | */ 160 | public static T of(String data, Function of, Logger log) { 161 | try { 162 | return of.apply(JSON.parseArray(data)); 163 | } catch (Exception e) { 164 | e.printStackTrace(); 165 | log.error(e.getMessage()); 166 | log.error("of error -> {}", data); 167 | } 168 | return null; 169 | } 170 | 171 | /** 172 | * 解析list数据并添加到ModelList对象中 173 | * @param data 原始数据 174 | * @param fresh 构造新的实例 175 | * @param of JSONArray解析of 176 | * @param log 错误输出 177 | * @param 元素对象 178 | * @return 解析成功与否 179 | */ 180 | public static T of(String data, Function, T> fresh, 181 | Function of, Logger log) { 182 | try { 183 | JSONArray r = JSON.parseArray(data); 184 | return Util.of(r, fresh, of, log); 185 | } catch (Exception e) { 186 | e.printStackTrace(); 187 | log.error(e.getMessage()); 188 | log.error("of error -> {}", data); 189 | } 190 | return null; 191 | } 192 | 193 | /** 194 | * 解析JSONArray数据并添加到ModelList对象中 195 | * @param r JSONArray数据 196 | * @param fresh 构造新的实例 197 | * @param of JSONArray解析of 198 | * @param log 错误输出 199 | * @param 元素对象 200 | * @return 解析成功与否 201 | */ 202 | public static T of(JSONArray r, Function, T> fresh, 203 | Function of, Logger log) { 204 | if (null == r) { 205 | return null; 206 | } 207 | try { 208 | List list = new ArrayList<>(r.size()); 209 | for (int i = 0; i < r.size(); i++) { 210 | list.add(of.apply(r.getJSONArray(i))); 211 | } 212 | return fresh.apply(list); 213 | } catch (Exception e) { 214 | e.printStackTrace(); 215 | log.error(e.getMessage()); 216 | log.error("of error -> {}", r); 217 | } 218 | return null; 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/enums/Period.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.enums; 2 | 3 | /** 4 | * 周期,K线需要使用 5 | * @author GOAi 6 | */ 7 | public enum Period { 8 | // 1分钟 9 | MIN1 (60), 10 | // 3分钟 11 | MIN3 (60 * 3), 12 | // 5分钟 13 | MIN5 (60 * 5), 14 | // 15分钟 15 | MIN15 (60 * 15), 16 | // 30分钟 17 | MIN30 (60 * 30), 18 | // 1小时 19 | HOUR1 (60 * 60), 20 | // 2小时 21 | HOUR2 (60 * 60 * 2), 22 | // 3小时 23 | HOUR3 (60 * 60 * 3), 24 | // 4小时 25 | HOUR4 (60 * 60 * 4), 26 | // 6小时 27 | HOUR6 (60 * 60 * 6), 28 | // 12小时 29 | HOUR12 (60 * 60 * 12), 30 | // 1天 31 | DAY1 (60 * 60 * 24), 32 | // 3天 33 | DAY3 (60 * 60 * 24 * 3), 34 | // 1周 35 | WEEK1 (60 * 60 * 24 * 7), 36 | // 2周 37 | WEEK2 (60 * 60 * 24 * 14), 38 | // 1月 39 | MONTH1 (60 * 60 * 24 * 30); 40 | 41 | private int value; 42 | 43 | Period(int value) { 44 | this.value = value; 45 | } 46 | 47 | public int getValue() { 48 | return value; 49 | } 50 | 51 | /** 52 | * 根据值解析周期 53 | * @param value 秒 54 | * @return 周期 55 | */ 56 | public static Period parse(int value) { 57 | switch (value) { 58 | case 60: 59 | return MIN1; 60 | case 60 * 3: 61 | return MIN3; 62 | case 60 * 5: 63 | return MIN5; 64 | case 60 * 15: 65 | return MIN15; 66 | case 60 * 30: 67 | return MIN30; 68 | case 60 * 60: 69 | return HOUR1; 70 | case 60 * 60 * 2: 71 | return HOUR2; 72 | case 60 * 60 * 3: 73 | return HOUR3; 74 | case 60 * 60 * 4: 75 | return HOUR4; 76 | case 60 * 60 * 6: 77 | return HOUR6; 78 | case 60 * 60 * 12: 79 | return HOUR12; 80 | case 60 * 60 * 24: 81 | return DAY1; 82 | case 60 * 60 * 24 * 3: 83 | return DAY3; 84 | case 60 * 60 * 24 * 7: 85 | return WEEK1; 86 | case 60 * 60 * 24 * 14: 87 | return WEEK2; 88 | case 60 * 60 * 24 * 30: 89 | return MONTH1; 90 | default: 91 | return null; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/enums/Side.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.enums; 2 | 3 | /** 4 | * 订单方向 5 | * @author GOAi 6 | */ 7 | public enum Side { 8 | // 买 9 | BUY, 10 | // 卖 11 | SELL, 12 | } 13 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/enums/State.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.enums; 2 | 3 | /** 4 | * 订单状态 5 | * @author GOAi 6 | */ 7 | public enum State { 8 | // 已提交 9 | SUBMIT, 10 | // 已成交 11 | FILLED, 12 | // 已取消 13 | CANCEL, 14 | // 部分成交 15 | PARTIAL, 16 | // 部分成交后取消 17 | UNDONE, 18 | 19 | } -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/enums/Type.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.enums; 2 | 3 | /** 4 | * 订单类型 5 | * @author GOAi 6 | */ 7 | public enum Type { 8 | // 限价单 9 | LIMIT, 10 | // 市价单 11 | MARKET, 12 | // FOK 13 | FILL_OR_KILL, 14 | // IOC 15 | IMMEDIATE_OR_CANCEL, 16 | } 17 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Depth.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import cqt.goai.model.To; 6 | import cqt.goai.model.Util; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.slf4j.Logger; 11 | 12 | import java.io.Serializable; 13 | 14 | /** 15 | * 盘口深度 16 | * @author GOAi 17 | */ 18 | @Getter 19 | @ToString 20 | @EqualsAndHashCode 21 | public class Depth implements To, Serializable { 22 | 23 | private static final long serialVersionUID = -8133481259875889084L; 24 | 25 | /** 26 | * 时间戳 27 | */ 28 | private final Long time; 29 | 30 | /** 31 | * 卖盘数据,由低到高 32 | */ 33 | private final Rows asks; 34 | 35 | /** 36 | * 买盘数据,由高到低 37 | */ 38 | private final Rows bids; 39 | 40 | public Depth(Long time, Rows asks, Rows bids) { 41 | this.time = time; 42 | this.asks = asks; 43 | this.bids = bids; 44 | } 45 | 46 | @Override 47 | public String to() { 48 | return '{' + 49 | "\"time\":" + Util.to(this.time) + 50 | ",\"asks\":" + Util.to(this.asks) + 51 | ",\"bids\":" + Util.to(this.bids) + 52 | '}'; 53 | } 54 | 55 | public static Depth of(String data, Logger log) { 56 | try { 57 | JSONObject r = JSON.parseObject(data); 58 | return new Depth( 59 | r.getLong("time"), 60 | Rows.of(r.getJSONArray("asks"), log), 61 | Rows.of(r.getJSONArray("bids"), log)); 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | log.error(e.getMessage()); 65 | log.error("of error -> {}", data); 66 | } 67 | return null; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Kline.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.ToString; 9 | import org.slf4j.Logger; 10 | 11 | import java.io.Serializable; 12 | import java.math.BigDecimal; 13 | import java.util.Objects; 14 | 15 | /** 16 | * K线信息 17 | * @author GOAi 18 | */ 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | public class Kline implements To, Serializable { 23 | 24 | private static final long serialVersionUID = 3739670180018865992L; 25 | 26 | /** 27 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 28 | */ 29 | private final String data; 30 | 31 | /** 32 | * 时间, 精确到秒 33 | */ 34 | private final Long time; 35 | 36 | /** 37 | * 开盘价 38 | */ 39 | private final BigDecimal open; 40 | 41 | /** 42 | * 最高价 43 | */ 44 | private final BigDecimal high; 45 | 46 | /** 47 | * 最低价 48 | */ 49 | private final BigDecimal low; 50 | 51 | /** 52 | * 收盘价 53 | */ 54 | private final BigDecimal close; 55 | 56 | /** 57 | * 交易量 58 | */ 59 | private final BigDecimal volume; 60 | 61 | public Kline(String data, Long time, BigDecimal open, BigDecimal high, 62 | BigDecimal low, BigDecimal close, BigDecimal volume) { 63 | this.data = data; 64 | this.time = time; 65 | this.open = Util.strip(open); 66 | this.high = Util.strip(high); 67 | this.low = Util.strip(low); 68 | this.close = Util.strip(close); 69 | this.volume = Util.strip(volume); 70 | } 71 | 72 | @Override 73 | public String to() { 74 | return '[' + 75 | Util.encode(this.data) + 76 | ',' + Util.to(this.time) + 77 | ',' + Util.to(this.open) + 78 | ',' + Util.to(this.high) + 79 | ',' + Util.to(this.low) + 80 | ',' + Util.to(this.close) + 81 | ',' + Util.to(this.volume) + 82 | ']'; 83 | } 84 | 85 | public double open() { 86 | Objects.requireNonNull(this.open); 87 | return this.open.doubleValue(); 88 | } 89 | 90 | public double high() { 91 | Objects.requireNonNull(this.high); 92 | return this.high.doubleValue(); 93 | } 94 | 95 | public double low() { 96 | Objects.requireNonNull(this.low); 97 | return this.low.doubleValue(); 98 | } 99 | 100 | public double close() { 101 | Objects.requireNonNull(this.close); 102 | return this.close.doubleValue(); 103 | } 104 | 105 | public double volume() { 106 | Objects.requireNonNull(this.volume); 107 | return this.volume.doubleValue(); 108 | } 109 | 110 | public static Kline of(String data, Logger log) { 111 | return Util.of(data, Kline::of, log); 112 | } 113 | 114 | static Kline of(JSONArray r) { 115 | return new Kline( 116 | Util.decode(r.getString(0)), 117 | r.getLong(1), 118 | r.getBigDecimal(2), 119 | r.getBigDecimal(3), 120 | r.getBigDecimal(4), 121 | r.getBigDecimal(5), 122 | r.getBigDecimal(6)); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Klines.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 多根K线,按时间顺序由近到远 get(0)是最近的K线 12 | * @author GOAi 13 | */ 14 | public class Klines extends BaseModelList implements Serializable { 15 | 16 | private static final long serialVersionUID = 3954815101761999805L; 17 | 18 | public Klines(List list) { 19 | super(list); 20 | } 21 | 22 | public static Klines of(String data, Logger log) { 23 | return Util.of(data, Klines::new, Kline::of, log); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "Klines" + super.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Row.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.ToString; 9 | import org.slf4j.Logger; 10 | 11 | import java.io.Serializable; 12 | import java.math.BigDecimal; 13 | import java.util.Objects; 14 | 15 | /** 16 | * 档位 价格 和 数量 17 | * @author GOAi 18 | */ 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | public class Row implements To, Serializable { 23 | 24 | private static final long serialVersionUID = -5070260547441227713L; 25 | 26 | /** 27 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 28 | */ 29 | private final String data; 30 | 31 | /** 32 | * 价格 33 | */ 34 | private final BigDecimal price; 35 | 36 | /** 37 | * 数量 38 | */ 39 | private final BigDecimal amount; 40 | 41 | public Row(String data, BigDecimal price, BigDecimal amount) { 42 | this.data = data; 43 | this.price = Util.strip(price); 44 | this.amount = Util.strip(amount); 45 | } 46 | 47 | @Override 48 | public String to() { 49 | return '[' + 50 | Util.encode(this.data) + 51 | ',' + Util.to(this.price) + 52 | ',' + Util.to(this.amount) + 53 | ']'; 54 | } 55 | 56 | public double price() { 57 | Objects.requireNonNull(this.price); 58 | return this.price.doubleValue(); 59 | } 60 | 61 | public double amount() { 62 | Objects.requireNonNull(this.amount); 63 | return this.amount.doubleValue(); 64 | } 65 | 66 | public static Row of(String data, Logger log) { 67 | return Util.of(data, Row::of, log); 68 | } 69 | 70 | static Row of(JSONArray r) { 71 | return new Row( 72 | Util.decode(r.getString(0)), 73 | r.getBigDecimal(1), 74 | r.getBigDecimal(2)); 75 | } 76 | 77 | public static Row row(BigDecimal price, BigDecimal amount) { 78 | return new Row(null, price, amount); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Rows.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.BaseModelList; 5 | import cqt.goai.model.Util; 6 | import org.slf4j.Logger; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * 多个档位 13 | * @author GOAi 14 | */ 15 | public class Rows extends BaseModelList implements Serializable { 16 | 17 | private static final long serialVersionUID = -7432880663696816431L; 18 | 19 | public Rows(List list) { 20 | super(list); 21 | } 22 | 23 | public static Rows of(String data, Logger log) { 24 | return Util.of(data, Rows::new, Row::of, log); 25 | } 26 | 27 | static Rows of(JSONArray r, Logger log) { 28 | return Util.of(r, Rows::new, Row::of, log); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "Rows" + super.toString(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Ticker.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import cqt.goai.model.To; 6 | import cqt.goai.model.Util; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.slf4j.Logger; 11 | 12 | import java.io.Serializable; 13 | import java.math.BigDecimal; 14 | import java.util.Objects; 15 | 16 | /** 17 | * Ticker信息 18 | * 19 | * Prototype Pattern 原型模式 20 | * 本来想用原型模式,每个推送都clone一份 21 | * 最后决定还是用不可变类,这样并发更好 22 | * 23 | * @author GOAi 24 | */ 25 | @Getter 26 | @ToString 27 | @EqualsAndHashCode 28 | public class Ticker implements To, Serializable { 29 | 30 | private static final long serialVersionUID = 8856908960657104056L; 31 | 32 | /** 33 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 34 | */ 35 | private final String data; 36 | 37 | /** 38 | * 时间戳 39 | */ 40 | private final Long time; 41 | 42 | /** 43 | * 开盘价 44 | */ 45 | private final BigDecimal open; 46 | 47 | /** 48 | * 最高价 49 | */ 50 | private final BigDecimal high; 51 | 52 | /** 53 | * 最低价 54 | */ 55 | private final BigDecimal low; 56 | 57 | /** 58 | * 收盘价 59 | */ 60 | private final BigDecimal last; 61 | 62 | /** 63 | * 交易量 64 | */ 65 | private final BigDecimal volume; 66 | 67 | public Ticker(String data, Long time, BigDecimal open, BigDecimal high, 68 | BigDecimal low, BigDecimal last, BigDecimal volume) { 69 | this.data = data; 70 | this.time = time; 71 | this.open = Util.strip(open); 72 | this.high = Util.strip(high); 73 | this.low = Util.strip(low); 74 | this.last = Util.strip(last); 75 | this.volume = Util.strip(volume); 76 | } 77 | 78 | @Override 79 | public String to() { 80 | return '{' + 81 | "\"data\":" + Util.encode(this.data) + 82 | ",\"time\":" + Util.to(this.time) + 83 | ",\"open\":" + Util.to(this.open) + 84 | ",\"high\":" + Util.to(this.high) + 85 | ",\"low\":" + Util.to(this.low) + 86 | ",\"last\":" + Util.to(this.last) + 87 | ",\"volume\":" + Util.to(this.volume) + 88 | '}'; 89 | } 90 | 91 | public double open() { 92 | Objects.requireNonNull(this.open); 93 | return this.open.doubleValue(); 94 | } 95 | 96 | public double high() { 97 | Objects.requireNonNull(this.high); 98 | return this.high.doubleValue(); 99 | } 100 | 101 | public double low() { 102 | Objects.requireNonNull(this.low); 103 | return this.low.doubleValue(); 104 | } 105 | 106 | public double last() { 107 | Objects.requireNonNull(this.last); 108 | return this.last.doubleValue(); 109 | } 110 | 111 | public double volume() { 112 | Objects.requireNonNull(this.volume); 113 | return this.volume.doubleValue(); 114 | } 115 | 116 | public static Ticker of(String data, Logger log) { 117 | try { 118 | JSONObject r = JSON.parseObject(data); 119 | return new Ticker( 120 | Util.decode(r.getString("data")), 121 | r.getLong("time"), 122 | r.getBigDecimal("open"), 123 | r.getBigDecimal("high"), 124 | r.getBigDecimal("low"), 125 | r.getBigDecimal("last"), 126 | r.getBigDecimal("volume")); 127 | } catch (Exception e) { 128 | e.printStackTrace(); 129 | log.error(e.getMessage()); 130 | log.error("of error -> {}", data); 131 | } 132 | return null; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Trade.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import cqt.goai.model.enums.Side; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.slf4j.Logger; 11 | 12 | import java.io.Serializable; 13 | import java.math.BigDecimal; 14 | import java.util.Objects; 15 | 16 | /** 17 | * 成交信息 18 | * @author GOAi 19 | */ 20 | @Getter 21 | @ToString 22 | @EqualsAndHashCode 23 | public class Trade implements To, Serializable { 24 | 25 | private static final long serialVersionUID = 514750454318716315L; 26 | 27 | /** 28 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 29 | */ 30 | private final String data; 31 | 32 | /** 33 | * 时间戳 34 | */ 35 | private final Long time; 36 | 37 | /** 38 | * 交易id 39 | */ 40 | private final String id; 41 | 42 | /** 43 | * 方向 44 | */ 45 | private final Side side; 46 | 47 | /** 48 | * 价格 49 | */ 50 | private final BigDecimal price; 51 | 52 | /** 53 | * 数量 54 | */ 55 | private final BigDecimal amount; 56 | 57 | public Trade(String data, Long time, String id, Side side, BigDecimal price, BigDecimal amount) { 58 | this.data = data; 59 | this.time = time; 60 | this.id = id; 61 | this.side = side; 62 | this.price = Util.strip(price); 63 | this.amount = Util.strip(amount); 64 | } 65 | 66 | @Override 67 | public String to() { 68 | return '[' + 69 | Util.encode(this.data) + 70 | ',' + Util.to(this.time) + 71 | ',' + Util.to(this.id) + 72 | ',' + Util.to(this.side) + 73 | ',' + Util.to(this.price) + 74 | ',' + Util.to(this.amount) + 75 | ']'; 76 | } 77 | 78 | public double price() { 79 | Objects.requireNonNull(this.price); 80 | return this.price.doubleValue(); 81 | } 82 | 83 | public double amount() { 84 | Objects.requireNonNull(this.amount); 85 | return this.amount.doubleValue(); 86 | } 87 | 88 | public static Trade of(String data, Logger log) { 89 | return Util.of(data, Trade::of, log); 90 | } 91 | 92 | static Trade of(JSONArray r) { 93 | return new Trade( 94 | Util.decode(r.getString(0)), 95 | r.getLong(1), 96 | r.getString(2), 97 | Util.of(r.getString(3), Side::valueOf), 98 | r.getBigDecimal(4), 99 | r.getBigDecimal(5)); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/market/Trades.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.market; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 多个成交信息 12 | * @author GOAi 13 | */ 14 | public class Trades extends BaseModelList implements Serializable { 15 | 16 | private static final long serialVersionUID = -5998165404968198947L; 17 | 18 | public Trades(List list) { 19 | super(list); 20 | } 21 | 22 | public static Trades of(String data, Logger log) { 23 | return Util.of(data, Trades::new, Trade::of, log); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "Trades" + super.toString(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/other/RunInfo.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.other; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 标记程序运行信息 7 | * run 落盘 8 | * node 读取 9 | * @author GOAi 10 | */ 11 | @Data 12 | public class RunInfo { 13 | 14 | /** 15 | * 启动时间 16 | */ 17 | private Long startTime; 18 | 19 | /** 20 | * 结束时间 21 | */ 22 | private Long endTime; 23 | 24 | /** 25 | * 程序当前状态 26 | */ 27 | private RunState runState; 28 | 29 | /** 30 | * 程序pid 31 | */ 32 | private Integer pid; 33 | 34 | public RunInfo(Long startTime, RunState runState) { 35 | this.startTime = startTime; 36 | this.runState = runState; 37 | } 38 | 39 | public RunInfo setRunState(RunState runState) { 40 | this.runState = runState; 41 | return this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/other/RunState.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.other; 2 | 3 | /** 4 | * 运行状态 5 | * STOPPED -> STARTING -> STARTED -> STOPPING -> STOPPED 6 | * @author GOAi 7 | */ 8 | public enum RunState { 9 | /** 10 | * 程序停止运行 11 | */ 12 | STOPPED, 13 | /** 14 | * 启动中 15 | */ 16 | STARTING, 17 | /** 18 | * 启动完毕 19 | */ 20 | STARTED, 21 | /** 22 | * 停止中 23 | */ 24 | STOPPING, 25 | 26 | /** 27 | * 意外停止 28 | */ 29 | ERROR 30 | } 31 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Account.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import cqt.goai.model.To; 6 | import cqt.goai.model.Util; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.slf4j.Logger; 11 | 12 | import java.io.Serializable; 13 | import java.math.BigDecimal; 14 | import java.util.Objects; 15 | 16 | /** 17 | * 某个币对账户信息 18 | * @author GOAi 19 | */ 20 | @Getter 21 | @ToString 22 | @EqualsAndHashCode 23 | public class Account implements To, Serializable { 24 | 25 | private static final long serialVersionUID = -81334812598889084L; 26 | 27 | /** 28 | * 时间戳 29 | */ 30 | private final Long time; 31 | 32 | /** 33 | * 目标货币 BTC_USD 中的 BTC, 用USD买卖BTC 34 | */ 35 | private final Balance base; 36 | 37 | /** 38 | * 计价货币 BTC_USD 中的 USD 39 | */ 40 | private final Balance quote; 41 | 42 | public Account(Long time, Balance base, Balance quote) { 43 | this.time = time; 44 | this.base = base; 45 | this.quote = quote; 46 | } 47 | 48 | public BigDecimal getBaseFree() { 49 | Objects.requireNonNull(base); 50 | return this.base.getFree(); 51 | } 52 | 53 | public BigDecimal getBaseUsed() { 54 | Objects.requireNonNull(base); 55 | return this.base.getUsed(); 56 | } 57 | 58 | public BigDecimal getQuoteFree() { 59 | Objects.requireNonNull(quote); 60 | return this.quote.getFree(); 61 | } 62 | 63 | public BigDecimal getQuoteUsed() { 64 | Objects.requireNonNull(quote); 65 | return this.quote.getUsed(); 66 | } 67 | 68 | public double baseFree() { 69 | Objects.requireNonNull(base); 70 | return this.base.free(); 71 | } 72 | 73 | public double baseUsed() { 74 | Objects.requireNonNull(base); 75 | return this.base.used(); 76 | } 77 | 78 | public double quoteFree() { 79 | Objects.requireNonNull(quote); 80 | return this.quote.free(); 81 | } 82 | 83 | public double quoteUsed() { 84 | Objects.requireNonNull(quote); 85 | return this.quote.used(); 86 | } 87 | 88 | @Override 89 | public String to() { 90 | return '{' + 91 | "\"time\":" + Util.to(this.time) + 92 | ",\"base\":" + Util.to(this.base) + 93 | ",\"quote\":" + Util.to(this.quote) + 94 | '}'; 95 | } 96 | 97 | public static Account of(String data, Logger log) { 98 | try { 99 | JSONObject r = JSON.parseObject(data); 100 | return new Account( 101 | r.getLong("time"), 102 | Balance.of(r.getJSONArray("base")), 103 | Balance.of(r.getJSONArray("quote"))); 104 | } catch (Exception e) { 105 | e.printStackTrace(); 106 | log.error(e.getMessage()); 107 | log.error("of error -> {}", data); 108 | } 109 | return null; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Balance.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.ToString; 9 | import org.slf4j.Logger; 10 | 11 | import java.io.Serializable; 12 | import java.math.BigDecimal; 13 | import java.util.Objects; 14 | 15 | /** 16 | * 某个币的余额 17 | * @author GOAi 18 | */ 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | public class Balance implements To, Serializable { 23 | 24 | private static final long serialVersionUID = -8006911245239428396L; 25 | 26 | /** 27 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 28 | */ 29 | private final String data; 30 | 31 | /** 32 | * 币的名称 BTC USD 等 33 | */ 34 | private final String currency; 35 | 36 | /** 37 | * 可用数量 38 | */ 39 | private final BigDecimal free; 40 | 41 | /** 42 | * 冻结数量 43 | */ 44 | private final BigDecimal used; 45 | 46 | public Balance(String data, String currency, BigDecimal free, BigDecimal used) { 47 | this.data = data; 48 | this.currency = currency; 49 | this.free = Util.strip(free); 50 | this.used = Util.strip(used); 51 | } 52 | 53 | @Override 54 | public String to() { 55 | return '[' + 56 | Util.encode(data) + 57 | "," + Util.to(currency) + 58 | "," + Util.to(free) + 59 | "," + Util.to(used) + 60 | ']'; 61 | } 62 | 63 | public double free() { 64 | Objects.requireNonNull(this.free); 65 | return this.free.doubleValue(); 66 | } 67 | 68 | public double used() { 69 | Objects.requireNonNull(this.used); 70 | return this.used.doubleValue(); 71 | } 72 | 73 | public static Balance of(String data, Logger log) { 74 | return Util.of(data, Balance::of, log); 75 | } 76 | 77 | static Balance of(JSONArray r) { 78 | return new Balance( 79 | Util.decode(r.getString(0)), 80 | r.getString(1), 81 | r.getBigDecimal(2), 82 | r.getBigDecimal(3)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Balances.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 账户下所有币余额信息 12 | * @author GOAi 13 | */ 14 | public class Balances extends BaseModelList implements Serializable { 15 | 16 | private static final long serialVersionUID = -58830328674451561L; 17 | 18 | public Balances(List list) { 19 | super(list); 20 | } 21 | 22 | public static Balances of(String data, Logger log) { 23 | return Util.of(data, Balances::new, Balance::of, log); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "Balances" + super.toString(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Order.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import cqt.goai.model.enums.Side; 7 | import cqt.goai.model.enums.State; 8 | import cqt.goai.model.enums.Type; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.ToString; 12 | import org.slf4j.Logger; 13 | 14 | import java.io.Serializable; 15 | import java.math.BigDecimal; 16 | import java.time.Instant; 17 | import java.time.LocalDateTime; 18 | import java.time.ZoneId; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.Objects; 21 | 22 | import static java.math.BigDecimal.ZERO; 23 | 24 | /** 25 | * 订单 26 | * @author GOAi 27 | */ 28 | @Getter 29 | @ToString 30 | @EqualsAndHashCode 31 | public class Order implements To, Serializable { 32 | 33 | private static final long serialVersionUID = -298726083025069628L; 34 | 35 | /** 36 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 37 | */ 38 | private final String data; 39 | 40 | /** 41 | * 时间戳 42 | */ 43 | private final Long time; 44 | 45 | /** 46 | * 订单id 47 | */ 48 | private final String id; 49 | 50 | /** 51 | * 买卖方向 52 | */ 53 | private final Side side; 54 | 55 | /** 56 | * 订单类型 57 | */ 58 | private final Type type; 59 | 60 | /** 61 | * 订单状态 62 | */ 63 | private final State state; 64 | 65 | /** 66 | * 下单价格 67 | */ 68 | private final BigDecimal price; 69 | 70 | /** 71 | * 下单数量 72 | */ 73 | private final BigDecimal amount; 74 | 75 | /** 76 | * 成交数量 77 | */ 78 | private final BigDecimal deal; 79 | 80 | /** 81 | * 成交均价 82 | */ 83 | private BigDecimal average; 84 | 85 | public Order(String data, Long time, String id, Side side, Type type, State state, 86 | BigDecimal price, BigDecimal amount, BigDecimal deal, BigDecimal average) { 87 | this.data = data; 88 | this.time = time; 89 | this.id = id; 90 | this.side = side; 91 | this.type = type; 92 | this.state = state; 93 | this.price = Util.strip(price); 94 | this.amount = Util.strip(amount); 95 | this.deal = Util.strip(deal); 96 | this.average = Util.strip(average); 97 | } 98 | 99 | @Override 100 | public String to() { 101 | return '[' + 102 | Util.encode(data) + 103 | ',' + Util.to(time) + 104 | ',' + Util.to(id) + 105 | ',' + Util.to(side) + 106 | ',' + Util.to(type) + 107 | ',' + Util.to(state) + 108 | ',' + Util.to(price) + 109 | ',' + Util.to(amount) + 110 | ',' + Util.to(deal) + 111 | ',' + Util.to(average) + 112 | ']'; 113 | } 114 | 115 | public double price() { 116 | Objects.requireNonNull(this.price); 117 | return this.price.doubleValue(); 118 | } 119 | 120 | public double amount() { 121 | Objects.requireNonNull(this.amount); 122 | return this.amount.doubleValue(); 123 | } 124 | 125 | public double deal() { 126 | Objects.requireNonNull(this.deal); 127 | return this.deal.doubleValue(); 128 | } 129 | 130 | public double average() { 131 | Objects.requireNonNull(this.average); 132 | return this.average.doubleValue(); 133 | } 134 | 135 | public static Order of(String data, Logger log) { 136 | return Util.of(data, Order::of, log); 137 | } 138 | 139 | static Order of(JSONArray r) { 140 | return new Order( 141 | Util.decode(r.getString(0)), 142 | r.getLong(1), 143 | r.getString(2), 144 | Util.of(r.getString(3), Side::valueOf), 145 | Util.of(r.getString(4), Type::valueOf), 146 | Util.of(r.getString(5), State::valueOf), 147 | r.getBigDecimal(6), 148 | r.getBigDecimal(7), 149 | r.getBigDecimal(8), 150 | r.getBigDecimal(9)); 151 | } 152 | 153 | /** 154 | * 是否活跃订单 155 | * @return 是否活跃订单 156 | */ 157 | public boolean alive() { 158 | return this.state == State.SUBMIT || this.state == State.PARTIAL; 159 | } 160 | 161 | /** 162 | * 是否有成交 163 | * @return 是否有成交 164 | */ 165 | public boolean isDeal() { 166 | return this.state == State.FILLED || this.state == State.UNDONE; 167 | } 168 | 169 | /** 170 | * 成交总价 171 | * @return 成交总价 172 | */ 173 | public BigDecimal dealVolume() { 174 | return null == this.average ? ZERO : this.deal.multiply(this.average); 175 | } 176 | 177 | private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); 178 | 179 | public String simple() { 180 | return "Order{" + 181 | "time='" + LocalDateTime 182 | .ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) 183 | .format(DTF) + '\'' + 184 | ", id='" + id + '\'' + 185 | ", side=" + side + 186 | ", type=" + type + 187 | ", state=" + state + 188 | ", price=" + price + 189 | ", amount=" + amount + 190 | ", deal=" + deal + 191 | ", average=" + average + 192 | '}'; 193 | } 194 | 195 | public boolean isSell() { 196 | return this.side == Side.SELL; 197 | } 198 | 199 | public boolean isBuy() { 200 | return this.side == Side.BUY; 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/OrderDetail.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import cqt.goai.model.enums.Side; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.slf4j.Logger; 11 | 12 | import java.io.Serializable; 13 | import java.math.BigDecimal; 14 | import java.util.Objects; 15 | 16 | /** 17 | * 某笔订单的一条成交明细记录 18 | * @author GOAi 19 | */ 20 | @Getter 21 | @ToString 22 | @EqualsAndHashCode 23 | public class OrderDetail implements To, Serializable { 24 | 25 | private static final long serialVersionUID = 8135946558399552447L; 26 | 27 | /** 28 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 29 | */ 30 | private final String data; 31 | 32 | /** 33 | * 时间戳 34 | */ 35 | private final Long time; 36 | 37 | /** 38 | * 订单id 39 | */ 40 | private final String orderId; 41 | 42 | /** 43 | * 成交明细id 44 | */ 45 | private final String detailId; 46 | 47 | /** 48 | * 成交价格 49 | */ 50 | private final BigDecimal price; 51 | 52 | /** 53 | * 成交数量 54 | */ 55 | private final BigDecimal amount; 56 | 57 | /** 58 | * 手续费 59 | */ 60 | private final BigDecimal fee; 61 | 62 | /** 63 | * 手续费货币 64 | */ 65 | private final String feeCurrency; 66 | 67 | /** 68 | * 买卖方向 69 | */ 70 | private final Side side; 71 | 72 | public OrderDetail(String data, Long time, String orderId, String detailId, 73 | BigDecimal price, BigDecimal amount, BigDecimal fee, String feeCurrency) { 74 | this.data = data; 75 | this.time = time; 76 | this.orderId = orderId; 77 | this.detailId = detailId; 78 | this.price = Util.strip(price); 79 | this.amount = Util.strip(amount); 80 | this.fee = Util.strip(fee); 81 | this.feeCurrency = feeCurrency; 82 | this.side = null; 83 | } 84 | 85 | public OrderDetail(String data, Long time, String orderId, String detailId, 86 | BigDecimal price, BigDecimal amount, BigDecimal fee, String feeCurrency, Side side) { 87 | this.data = data; 88 | this.time = time; 89 | this.orderId = orderId; 90 | this.detailId = detailId; 91 | this.price = Util.strip(price); 92 | this.amount = Util.strip(amount); 93 | this.fee = Util.strip(fee); 94 | this.feeCurrency = feeCurrency; 95 | this.side = side; 96 | } 97 | 98 | @Override 99 | public String to() { 100 | return '[' + 101 | Util.encode(this.data) + 102 | "," + Util.to(this.time) + 103 | "," + Util.to(this.orderId) + 104 | "," + Util.to(this.detailId) + 105 | "," + Util.to(this.price) + 106 | "," + Util.to(this.amount) + 107 | "," + Util.to(this.fee) + 108 | "," + Util.to(this.feeCurrency) + 109 | "," + Util.to(this.side) + 110 | ']'; 111 | } 112 | 113 | 114 | public double price() { 115 | Objects.requireNonNull(this.price); 116 | return this.price.doubleValue(); 117 | } 118 | 119 | public double amount() { 120 | Objects.requireNonNull(this.amount); 121 | return this.amount.doubleValue(); 122 | } 123 | 124 | public double fee() { 125 | Objects.requireNonNull(this.fee); 126 | return this.fee.doubleValue(); 127 | } 128 | 129 | public static OrderDetail of(String data, Logger log) { 130 | return Util.of(data, OrderDetail::of, log); 131 | } 132 | 133 | static OrderDetail of(JSONArray r) { 134 | return new OrderDetail 135 | (Util.decode(r.getString(0)), 136 | r.getLong(1), 137 | r.getString(2), 138 | r.getString(3), 139 | r.getBigDecimal(4), 140 | r.getBigDecimal(5), 141 | r.getBigDecimal(6), 142 | r.getString(7), 143 | Util.of(r.getString(8), Side::valueOf)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/OrderDetails.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 多个订单成交明细记录 12 | * @author GOAi 13 | */ 14 | public class OrderDetails extends BaseModelList implements Serializable { 15 | 16 | private static final long serialVersionUID = -5883032867443051561L; 17 | 18 | public OrderDetails(List list) { 19 | super(list); 20 | } 21 | 22 | public static OrderDetails of(String data, Logger log) { 23 | return Util.of(data, OrderDetails::new, OrderDetail::of, log); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "OrderDetails" + super.toString(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Orders.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * 多笔订单 12 | * @author GOAi 13 | */ 14 | public class Orders extends BaseModelList implements Serializable { 15 | 16 | private static final long serialVersionUID = 3062332528451320335L; 17 | 18 | public Orders(List list) { 19 | super(list); 20 | } 21 | 22 | public static Orders of(String data, Logger log) { 23 | return Util.of(data, Orders::new, Order::of, log); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "Orders" + super.toString(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Precision.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import cqt.goai.model.To; 5 | import cqt.goai.model.Util; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.ToString; 9 | import org.slf4j.Logger; 10 | 11 | import java.io.Serializable; 12 | import java.math.BigDecimal; 13 | import java.util.Objects; 14 | 15 | /** 16 | * 某交易币对的精度 17 | * @author GOAi 18 | */ 19 | @Getter 20 | @ToString 21 | @EqualsAndHashCode 22 | public class Precision implements To, Serializable { 23 | 24 | private static final long serialVersionUID = -5822155062030969112L; 25 | 26 | /** 27 | * 交易所返回的原始数据, 防止有些用户需要一些特殊的数据 28 | */ 29 | private final String data; 30 | 31 | /** 32 | * 币对 33 | */ 34 | private final String symbol; 35 | 36 | /** 37 | * 交易时币精度,amount 38 | */ 39 | private final Integer base; 40 | 41 | /** 42 | * 计价货币精度,price BTC/USD 1btc->6000usd 所以数数数的是usd,也就是写price的位置的精度 43 | */ 44 | private final Integer quote; 45 | 46 | /** 47 | * 最小币步长 48 | */ 49 | private final BigDecimal baseStep; 50 | 51 | /** 52 | * 最小价格步长 53 | */ 54 | private final BigDecimal quoteStep; 55 | 56 | /** 57 | * 最小买卖数量 58 | */ 59 | private final BigDecimal minBase; 60 | 61 | /** 62 | * 最小买价格 63 | */ 64 | private final BigDecimal minQuote; 65 | 66 | /** 67 | * 最大买卖数量 68 | */ 69 | private final BigDecimal maxBase; 70 | 71 | /** 72 | * 最大买价格 73 | */ 74 | private final BigDecimal maxQuote; 75 | 76 | public Precision(String data, String symbol, 77 | Integer base, Integer quote, 78 | BigDecimal baseStep, BigDecimal quoteStep, 79 | BigDecimal minBase, BigDecimal minQuote, 80 | BigDecimal maxBase, BigDecimal maxQuote) { 81 | this.data = data; 82 | this.symbol = symbol; 83 | this.base = base; 84 | this.quote = quote; 85 | this.baseStep = Util.strip(baseStep); 86 | this.quoteStep = Util.strip(quoteStep); 87 | this.minBase = Util.strip(minBase); 88 | this.minQuote = Util.strip(minQuote); 89 | this.maxBase = Util.strip(maxBase); 90 | this.maxQuote = Util.strip(maxQuote); 91 | } 92 | 93 | @Override 94 | public String to() { 95 | return '[' + 96 | Util.encode(this.data) + 97 | "," + Util.to(this.symbol) + 98 | "," + Util.to(this.base) + 99 | "," + Util.to(this.quote) + 100 | "," + Util.to(this.baseStep) + 101 | "," + Util.to(this.quoteStep) + 102 | "," + Util.to(this.minBase) + 103 | "," + Util.to(this.minQuote) + 104 | "," + Util.to(this.maxBase) + 105 | "," + Util.to(this.maxQuote) + 106 | ']'; 107 | } 108 | 109 | public double baseStep() { 110 | Objects.requireNonNull(this.baseStep); 111 | return this.baseStep.doubleValue(); 112 | } 113 | 114 | public double quoteStep() { 115 | Objects.requireNonNull(this.quoteStep); 116 | return this.quoteStep.doubleValue(); 117 | } 118 | 119 | public double minBase() { 120 | Objects.requireNonNull(this.minBase); 121 | return this.minBase.doubleValue(); 122 | } 123 | 124 | public double minQuote() { 125 | Objects.requireNonNull(this.minQuote); 126 | return this.minQuote.doubleValue(); 127 | } 128 | 129 | public double maxBase() { 130 | Objects.requireNonNull(this.maxBase); 131 | return this.maxBase.doubleValue(); 132 | } 133 | 134 | public double maxQuote() { 135 | Objects.requireNonNull(this.maxQuote); 136 | return this.maxQuote.doubleValue(); 137 | } 138 | 139 | public static Precision of(String data, Logger log) { 140 | return Util.of(data, Precision::of, log); 141 | } 142 | 143 | static Precision of(JSONArray r) { 144 | return new Precision( 145 | Util.decode(r.getString(0)), 146 | r.getString(1), 147 | r.getInteger(2), 148 | r.getInteger(3), 149 | r.getBigDecimal(4), 150 | r.getBigDecimal(5), 151 | r.getBigDecimal(6), 152 | r.getBigDecimal(7), 153 | r.getBigDecimal(8), 154 | r.getBigDecimal(9)); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /engine/model/src/main/java/cqt/goai/model/trade/Precisions.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.model.trade; 2 | 3 | import cqt.goai.model.BaseModelList; 4 | import cqt.goai.model.Util; 5 | import org.slf4j.Logger; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * @author GOAi 12 | */ 13 | public class Precisions extends BaseModelList implements Serializable { 14 | 15 | private static final long serialVersionUID = 6672027458716855352L; 16 | 17 | public Precisions(List list) { 18 | super(list); 19 | } 20 | 21 | public static Precisions of(String data, Logger log) { 22 | return Util.of(data, Precisions::new, Precision::of, log); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "Precisions" + super.toString(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/DepthTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Depth; 4 | import cqt.goai.model.market.Row; 5 | import cqt.goai.model.market.Rows; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Arrays; 11 | 12 | public class DepthTest { 13 | 14 | @Test 15 | public void test() { 16 | Depth depth = new Depth( 17 | 1546426483495L, 18 | null, 19 | new Rows( Arrays.asList(new Row("231",new BigDecimal("1"), new BigDecimal("2") ), 20 | new Row(null, new BigDecimal("0.500"), new BigDecimal("1")))) 21 | ); 22 | String data = depth.to(); 23 | Assert.assertEquals("{\"time\":1546426483495,\"asks\":null,\"bids\":[[\"MjMx\",1,2],[null,0.5,1]]}", data); 24 | Depth t = Depth.of(data, null); 25 | Assert.assertEquals(depth, t); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/KlineTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Kline; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class KlineTest { 10 | 11 | @Test 12 | public void test() { 13 | Kline kline = new Kline( 14 | "[123,123]", 15 | 1546419093195L, 16 | new BigDecimal(1), 17 | new BigDecimal(2), 18 | null, 19 | new BigDecimal(4), 20 | new BigDecimal(5) 21 | ); 22 | String data = kline.to(); 23 | Assert.assertEquals("[\"WzEyMywxMjNd\",1546419093195,1,2,null,4,5]", data); 24 | Kline c = Kline.of(data, null); 25 | Assert.assertEquals(kline, c); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/KlinesTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Kline; 4 | import cqt.goai.model.market.Klines; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.Arrays; 10 | 11 | public class KlinesTest { 12 | 13 | @Test 14 | public void test() { 15 | Klines klines = new Klines(Arrays.asList(new Kline( 16 | "{123,\"123\"}", 17 | 1546422552967L, 18 | null, 19 | new BigDecimal("1"), 20 | new BigDecimal("2"), 21 | new BigDecimal("3"), 22 | new BigDecimal("0") 23 | ),new Kline( 24 | null, 25 | 1546422552968L, 26 | null, 27 | new BigDecimal("2"), 28 | new BigDecimal("3"), 29 | new BigDecimal("4"), 30 | new BigDecimal("5") 31 | ))); 32 | 33 | String data = klines.to(); 34 | Assert.assertEquals("[[\"ezEyMywiMTIzIn0=\",1546422552967,null,1,2,3,0],[null,1546422552968,null,2,3,4,5]]", data); 35 | 36 | Klines c = Klines.of(data, null); 37 | Assert.assertEquals(klines, c); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/RowTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Row; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class RowTest { 10 | 11 | @Test 12 | public void test() { 13 | Row row = new Row("12321,qwr42w3r238987c",new BigDecimal("1.12"),new BigDecimal("1.12")); 14 | String data = row.to(); 15 | Assert.assertEquals("[\"MTIzMjEscXdyNDJ3M3IyMzg5ODdj\",1.12,1.12]", data); 16 | Row r = Row.of(data, null); 17 | Assert.assertEquals(row, r); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/RowsTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Row; 4 | import cqt.goai.model.market.Rows; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.Arrays; 10 | 11 | public class RowsTest { 12 | 13 | @Test 14 | public void test() { 15 | Rows rows = new Rows(Arrays.asList(new Row( 16 | "{123,\"123\"}", 17 | new BigDecimal("3"), 18 | new BigDecimal("0") 19 | ),new Row( 20 | null, 21 | new BigDecimal("3"), 22 | new BigDecimal("0") 23 | ))); 24 | 25 | String data = rows.to(); 26 | Assert.assertEquals("[[\"ezEyMywiMTIzIn0=\",3,0],[null,3,0]]", data); 27 | 28 | Rows rs = Rows.of(data, null); 29 | Assert.assertEquals(rows, rs); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/TickerTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.market.Ticker; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class TickerTest { 10 | 11 | @Test 12 | public void test() { 13 | Ticker ticker = new Ticker( 14 | "{123,\"123\"}", 15 | 1546419093195L, 16 | new BigDecimal(1), 17 | new BigDecimal(2), 18 | null, 19 | new BigDecimal(4), 20 | new BigDecimal(5) 21 | ); 22 | String data = ticker.to(); 23 | Assert.assertEquals("{\"data\":\"ezEyMywiMTIzIn0=\",\"time\":1546419093195,\"open\":1,\"high\":2,\"low\":null,\"last\":4,\"volume\":5}", data); 24 | Ticker t = Ticker.of(data, null); 25 | Assert.assertEquals(ticker, t); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/TradeTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.enums.Side; 4 | import cqt.goai.model.market.Trade; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import java.math.BigDecimal; 9 | 10 | public class TradeTest { 11 | 12 | @Test 13 | public void test() { 14 | Trade trade = new Trade( 15 | "[123,123]", 16 | 1546419093195L, 17 | "123", 18 | Side.BUY, 19 | new BigDecimal(1), 20 | new BigDecimal(2) 21 | ); 22 | String data = trade.to(); 23 | Assert.assertEquals("[\"WzEyMywxMjNd\",1546419093195,\"123\",\"BUY\",1,2]", data); 24 | Trade t = Trade.of(data, null); 25 | Assert.assertEquals(trade, t); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/market/TradesTest.java: -------------------------------------------------------------------------------- 1 | package test.market; 2 | 3 | import cqt.goai.model.enums.Side; 4 | import cqt.goai.model.market.Trade; 5 | import cqt.goai.model.market.Trades; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Arrays; 11 | 12 | public class TradesTest { 13 | 14 | @Test 15 | public void test() { 16 | Trades trades = new Trades(Arrays.asList(new Trade( 17 | "asd{89as\"", 18 | 123L, 19 | "123", 20 | Side.BUY, 21 | new BigDecimal("1"), 22 | new BigDecimal("2") 23 | ),new Trade( 24 | null, 25 | 123L, 26 | "123", 27 | Side.SELL, 28 | new BigDecimal("1"), 29 | new BigDecimal("2") 30 | ))); 31 | 32 | String data = trades.to(); 33 | Assert.assertEquals("[[\"YXNkezg5YXMi\",123,\"123\",\"BUY\",1,2],[null,123,\"123\",\"SELL\",1,2]]", data); 34 | Trades ts = Trades.of(data, null); 35 | Assert.assertEquals(trades, ts); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /engine/model/src/test/java/test/trade/PrecisionTest.java: -------------------------------------------------------------------------------- 1 | package test.trade; 2 | 3 | import cqt.goai.model.trade.Precision; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class PrecisionTest { 10 | 11 | @Test 12 | public void test() { 13 | Precision precision = new Precision( 14 | "asda\"", 15 | "symbol", 16 | 2, 17 | 3, 18 | new BigDecimal("3"), 19 | new BigDecimal("5"), 20 | new BigDecimal("0.01"), 21 | new BigDecimal("0.001"), 22 | new BigDecimal("50").stripTrailingZeros(), 23 | new BigDecimal("0.01") 24 | ); 25 | String data = precision.to(); 26 | Assert.assertEquals("[\"YXNkYSI=\",\"symbol\",2,3,3,5,0.01,0.001,50,0.01]", data); 27 | Assert.assertEquals(precision, Precision.of(data, null)); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /engine/run/build.gradle: -------------------------------------------------------------------------------- 1 | sourceSets { 2 | main { 3 | java { 4 | srcDirs = ['src/main/java'] 5 | } 6 | resources { 7 | srcDirs = ['src/main/resources'] 8 | } 9 | } 10 | } 11 | 12 | //processResources { 13 | // include "**/logback.xml" 14 | // include "**/config**.yml" 15 | //} 16 | 17 | jar { 18 | from { configurations.runtime.collect{zipTree(it)} } 19 | baseName = 'goai-engine' 20 | manifest { 21 | attributes("Main-Class": "cqt.goai.run.Application") 22 | } 23 | } 24 | 25 | dependencies { 26 | testCompile 'junit:junit:4.12' 27 | 28 | // compile fileTree(dir: 'libs', includes: ['*.jar']) 29 | 30 | compile project(":exchange") 31 | 32 | compile 'ch.qos.logback:logback-classic:1.2.3' 33 | compile 'org.apache.logging.log4j:log4j-to-slf4j:2.11.1' 34 | compile 'org.yaml:snakeyaml:1.19' 35 | compile 'org.quartz-scheduler:quartz:2.3.0' 36 | 37 | compile 'javax.mail:mail:1.4.7' 38 | } 39 | 40 | task copyJar(type:Copy){ 41 | from ('build\\libs') 42 | into ('.') 43 | } -------------------------------------------------------------------------------- /engine/run/jar.bat: -------------------------------------------------------------------------------- 1 | gradle clean build -x test copyJar -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/Application.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run; 2 | 3 | import cqt.goai.run.main.Minister; 4 | 5 | /** 6 | * 启动类 7 | * 8 | * @author GOAi 9 | */ 10 | public class Application { 11 | 12 | /** 13 | * 主启动函数 14 | * @param args 启动参数 15 | */ 16 | public static void main(String[] args) { 17 | Minister.run(args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/annotation/Scheduled.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 定时任务 7 | * @author GOAi 8 | */ 9 | @Repeatable(Schedules.class) 10 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface Scheduled { 13 | /** 14 | * cron方式 优先 15 | */ 16 | String cron() default ""; 17 | 18 | /** 19 | * 固定频率,毫秒 20 | */ 21 | long fixedRate() default 0; 22 | 23 | /** 24 | * 延时,毫秒 25 | */ 26 | long delay() default 0; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/annotation/ScheduledScope.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.annotation; 2 | 3 | import cqt.goai.run.main.MethodScope; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * 方法范围 12 | * 13 | * @author GOAi 14 | */ 15 | @Target(ElementType.METHOD) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface ScheduledScope { 18 | /** 19 | * 默认实例范围 20 | * @return 方法运行范围 21 | */ 22 | MethodScope value() default MethodScope.INSTANCE; 23 | } 24 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/annotation/Schedules.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 单个方法允许设置多个定时任务 10 | * @author GOAi 11 | */ 12 | @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Schedules { 15 | Scheduled[] value(); 16 | } 17 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/Exchange.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange; 2 | 3 | import cqt.goai.exchange.ExchangeName; 4 | import cqt.goai.exchange.http.HttpExchange; 5 | import cqt.goai.exchange.util.RateLimit; 6 | import cqt.goai.model.enums.Period; 7 | import cqt.goai.model.market.*; 8 | import cqt.goai.model.trade.*; 9 | import org.slf4j.Logger; 10 | 11 | import java.math.BigDecimal; 12 | import java.util.List; 13 | import java.util.function.Consumer; 14 | 15 | /** 16 | * 交易所接口 17 | * @author GOAi 18 | */ 19 | public interface Exchange { 20 | 21 | /** 22 | * 所属交易所信息 23 | * @return 交易所信息 24 | */ 25 | ExchangeName getExchangeName(); 26 | 27 | /** 28 | * 交易所名 29 | * @return 交易所名 30 | */ 31 | String getName(); 32 | 33 | /** 34 | * 币对 35 | * @return 币对 36 | */ 37 | String getSymbol(); 38 | 39 | /** 40 | * access 41 | * @return access 42 | */ 43 | String getAccess(); 44 | 45 | /** 46 | * secret 47 | * @return secret 48 | */ 49 | String getSecret(); 50 | 51 | /** 52 | * 日志 53 | * @return 日志 54 | */ 55 | Logger getLog(); 56 | 57 | /** 58 | * 获取http请求对象 方便调用特殊api 59 | * @return HttpExchange 60 | */ 61 | HttpExchange getHttpExchange(); 62 | 63 | /** 64 | * 取消所有订阅 65 | */ 66 | void destroy(); 67 | 68 | /** 69 | * 检查价格 70 | * @param count 价格 71 | * @param ceil 取整方向 72 | * @return 调整后 73 | */ 74 | BigDecimal checkCount(BigDecimal count, boolean ceil); 75 | 76 | /** 77 | * 检查数量 78 | * @param base 数量 79 | * @return 调整后 80 | */ 81 | BigDecimal checkBase(BigDecimal base); 82 | 83 | /** 84 | * 检查多个精度 85 | * @param rows 多个 86 | * @param ceil 取整方向 87 | * @return 调整后 88 | */ 89 | List checkRows(List rows, boolean ceil); 90 | 91 | // =================== ticker ======================= 92 | 93 | /** 94 | * 获取Ticker 95 | * @return Ticker 96 | */ 97 | Ticker getTicker(); 98 | 99 | /** 100 | * 获取Ticker 101 | * @param latest http方式, false -> 允许不是最新的 true -> 必须最新的 102 | * @return Ticker 103 | */ 104 | Ticker getTicker(boolean latest); 105 | 106 | // =================== klines ======================= 107 | 108 | /** 109 | * 获取Klines 110 | * @return Klines 111 | */ 112 | Klines getKlines(); 113 | 114 | /** 115 | * 获取Klines 默认可以取不是最新的 116 | * @param period K线周期 117 | * @return Klines 118 | */ 119 | Klines getKlines(Period period); 120 | 121 | /** 122 | * 获取Klines 默认周期为MIN1 123 | * @param latest http方式, 允许不是最新的 124 | * @return Klines 125 | */ 126 | Klines getKlines(boolean latest); 127 | 128 | /** 129 | * 获取Klines 130 | * @param period K线周期 131 | * @param latest http方式, 允许不是最新的 132 | * @return Klines 133 | */ 134 | Klines getKlines(Period period, boolean latest); 135 | 136 | // =================== Depth ======================= 137 | 138 | /** 139 | * 获取Depth 140 | * @return Depth 141 | */ 142 | Depth getDepth(); 143 | 144 | /** 145 | * 获取Depth 146 | * @param latest http方式, 允许不是最新的 147 | * @return Depth 148 | */ 149 | Depth getDepth(boolean latest); 150 | 151 | // =================== trades ======================= 152 | 153 | /** 154 | * 获取Trades 155 | * @return Trades 156 | */ 157 | Trades getTrades(); 158 | 159 | /** 160 | * 获取Trades 161 | * @param latest http方式, 允许不是最新的 162 | * @return Trades 163 | */ 164 | Trades getTrades(boolean latest); 165 | 166 | // =================== account ======================= 167 | 168 | /** 169 | * 获取账户所有余额 170 | * @return Balances 171 | */ 172 | Balances getBalances(); 173 | 174 | /** 175 | * 获取Account 176 | * @return Account 177 | */ 178 | Account getAccount(); 179 | 180 | /** 181 | * 获取Account 182 | * @param latest http方式, 允许不是最新的 183 | * @return Account 184 | */ 185 | Account getAccount(boolean latest); 186 | 187 | // =================== trade ======================= 188 | 189 | /** 190 | * 获取所有币对精度信息 191 | * @return Precisions 192 | */ 193 | Precisions getPrecisions(); 194 | 195 | /** 196 | * 获取币对精度信息 197 | * @return Precision 198 | */ 199 | Precision getPrecision(); 200 | 201 | /** 202 | * 限价买 203 | * 自动结算精度,价格向下截断,数量向下截断 204 | * @param price 订单价格 205 | * @param amount 订单数量 206 | * @return 订单id 207 | */ 208 | String buyLimit(BigDecimal price, BigDecimal amount); 209 | 210 | /** 211 | * 限价买 212 | * 自动结算精度,价格向下截断,数量向下截断 213 | * @param price 订单价格 214 | * @param amount 订单数量 215 | * @return 订单id 216 | */ 217 | default String buyLimit(double price, double amount) { 218 | return this.buyLimit(new BigDecimal(price), new BigDecimal(amount)); 219 | } 220 | 221 | /** 222 | * 限价卖 223 | * 自动结算精度,价格向上截断,数量向上截断 224 | * @param price 订单价格 225 | * @param amount 订单数量 226 | * @return 订单id 227 | */ 228 | String sellLimit(BigDecimal price, BigDecimal amount); 229 | 230 | /** 231 | * 限价卖 232 | * 自动结算精度,价格向上截断,数量向上截断 233 | * @param price 订单价格 234 | * @param amount 订单数量 235 | * @return 订单id 236 | */ 237 | default String sellLimit(double price, double amount) { 238 | return this.sellLimit(new BigDecimal(price), new BigDecimal(amount)); 239 | } 240 | 241 | /** 242 | * 市价买 243 | * 自动结算精度,价格向下截断 244 | * @param count 订单总价 245 | * @return 订单id 246 | */ 247 | String buyMarket(BigDecimal count); 248 | 249 | /** 250 | * 市价卖 251 | * 自动结算精度,数量向上截断 252 | * @param base 订单数量 253 | * @return 订单id 254 | */ 255 | String sellMarket(BigDecimal base); 256 | 257 | /** 258 | * 多买 259 | * @param bids 多对买单 260 | * @return 订单id 261 | */ 262 | List multiBuy(List bids); 263 | 264 | /** 265 | * 多卖 266 | * @param asks 多对卖单 267 | * @return 订单id 268 | */ 269 | List multiSell(List asks); 270 | 271 | /** 272 | * 取消单个订单 273 | * @param id 订单id 274 | * @return 取消成功与否 275 | */ 276 | boolean cancelOrder(String id); 277 | 278 | /** 279 | * 取消多个订单 280 | * @param ids 多个订单id 281 | * @return 成功取消的订单id 282 | */ 283 | List cancelOrders(List ids); 284 | 285 | // =================== order ======================= 286 | 287 | /** 288 | * 获取活跃订单 289 | * @return Orders 290 | */ 291 | Orders getOrders(); 292 | 293 | /** 294 | * 获取历史订单 295 | * @return Orders 296 | */ 297 | Orders getHistoryOrders(); 298 | 299 | 300 | /** 301 | * 获取历史订单 302 | * @param id 订单id 303 | * @return Order 304 | */ 305 | Order getOrder(String id); 306 | 307 | /** 308 | * 获取订单成交明细 309 | * @param id 订单id 310 | * @return OrderDetails 311 | */ 312 | OrderDetails getOrderDetails(String id); 313 | 314 | /** 315 | * 获取所有订单成交明细 316 | * @return getOrderDetailAll 317 | */ 318 | OrderDetails getOrderDetailAll(); 319 | 320 | // =================== push ======================= 321 | 322 | /** 323 | * 设置onTicker, 默认无频率限制 324 | * @param onTicker 消费函数 325 | */ 326 | void setOnTicker(Consumer onTicker); 327 | 328 | /** 329 | * 设置onTicker 330 | * @param onTicker 消费函数 331 | * @param limit 推送频率限制 332 | */ 333 | void setOnTicker(Consumer onTicker, RateLimit limit); 334 | 335 | /** 336 | * 设置onTicker, 默认1MIN, 频率无频率限制 337 | * @param onKlines 消费函数 338 | */ 339 | void setOnKlines(Consumer onKlines); 340 | 341 | /** 342 | * 设置onTicker 343 | * @param onKlines 消费函数 344 | * @param period 推送K线周期 345 | * @param limit 频率限制 346 | */ 347 | void setOnKlines(Consumer onKlines, Period period, RateLimit limit); 348 | 349 | /** 350 | * 设置onDepth, 默认无频率限制 351 | * @param onDepth 消费函数 352 | */ 353 | void setOnDepth(Consumer onDepth); 354 | 355 | /** 356 | * 设置onDepth 357 | * @param onDepth 消费函数 358 | * @param limit 推送频率限制 359 | */ 360 | void setOnDepth(Consumer onDepth, RateLimit limit); 361 | 362 | /** 363 | * 设置onTrades, 默认无频率限制 364 | * @param onTrades 消费函数 365 | */ 366 | void setOnTrades(Consumer onTrades); 367 | 368 | /** 369 | * 设置onTrades 370 | * @param onTrades 消费函数 371 | * @param limit 推送频率限制 372 | */ 373 | void setOnTrades(Consumer onTrades, RateLimit limit); 374 | 375 | /** 376 | * 设置onAccount, 默认无频率限制 377 | * @param onAccount 消费函数 378 | */ 379 | void setOnAccount(Consumer onAccount); 380 | 381 | /** 382 | * 设置onOrders, 默认无频率限制 383 | * @param onOrders 消费函数 384 | */ 385 | void setOnOrders(Consumer onOrders); 386 | 387 | 388 | /** 389 | * 获取Exchange json 390 | * @param name 交易所名 391 | * @param symbol 币对 392 | * @param access access 393 | * @param secret secret 394 | * @return 交易所json 395 | */ 396 | static String getExchangeJson(String name, String symbol, String access, String secret) { 397 | return String.format("{\"name\":\"%s\",\"symbol\":\"%s\",\"access\":\"%s\",\"secret\":\"%s\"}", 398 | name, symbol, access, secret); 399 | } 400 | 401 | } 402 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/FourFunction.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange; 2 | 3 | /** 4 | * @author GOAi 5 | */ 6 | @FunctionalInterface 7 | public interface FourFunction { 8 | /** 9 | * 接受6个参数 10 | * @param t t 11 | * @param u u 12 | * @param k k 13 | * @param l l 14 | */ 15 | R apply(T t, U u, K k, L l); 16 | } 17 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/TrConsumer.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange; 2 | 3 | /** 4 | * @author GOAi 5 | */ 6 | @FunctionalInterface 7 | public interface TrConsumer { 8 | /** 9 | * 接受3个参数 10 | * @param t t 11 | * @param u u 12 | * @param k k 13 | */ 14 | void accept(T t, U u, K k); 15 | } 16 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/factory/BaseExchangeFactory.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.factory; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import cqt.goai.run.exchange.Exchange; 5 | import org.slf4j.Logger; 6 | 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * @author GOAi 11 | */ 12 | public abstract class BaseExchangeFactory { 13 | 14 | /** 15 | * Exchange工厂 16 | * Factory Method Pattern 工厂方法模式 17 | * 有多种Exchange类型,不同的工厂产生的Exchange底层实现不一样 18 | * @param proxy 是否包装代理 19 | * @param log 日志 20 | * @param config 配置信息 21 | * @param ready 是否准备好接收推送 22 | * @return Exchange 23 | */ 24 | public abstract Exchange getExchange(boolean proxy, Logger log, JSONObject config, Supplier ready); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/factory/LocalExchangeFactory.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.factory; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import cqt.goai.exchange.ExchangeName; 5 | import cqt.goai.run.exchange.Exchange; 6 | import cqt.goai.run.exchange.LocalExchange; 7 | import cqt.goai.run.exchange.ProxyExchange; 8 | import org.slf4j.Logger; 9 | 10 | import java.util.function.Supplier; 11 | 12 | /** 13 | * 本地实现Exchange 14 | * @author GOAi 15 | */ 16 | public class LocalExchangeFactory extends BaseExchangeFactory { 17 | 18 | @Override 19 | public Exchange getExchange(boolean proxy, Logger log, JSONObject config, Supplier ready) { 20 | Exchange exchange = new LocalExchange(log, 21 | ExchangeName.getByName(config.getString("name")), 22 | config.getString("symbol"), 23 | config.getString("access"), 24 | config.getString("secret"), 25 | ready); 26 | if (proxy) { 27 | exchange = new ProxyExchange(exchange); 28 | } 29 | return exchange; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/model/LogAction.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.model; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONArray; 5 | import com.alibaba.fastjson.JSONObject; 6 | import cqt.goai.exchange.Action; 7 | import cqt.goai.model.market.Row; 8 | import lombok.Data; 9 | 10 | import java.math.BigDecimal; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * @author GOAi 16 | */ 17 | @Data 18 | public class LogAction { 19 | 20 | private String name; 21 | private String symbol; 22 | private String access; 23 | private Action action; 24 | 25 | private BigDecimal price; 26 | private BigDecimal amount; 27 | 28 | private BigDecimal quote; 29 | private BigDecimal base; 30 | 31 | private List rows; 32 | 33 | private String id; 34 | private List ids; 35 | 36 | private LogAction(String name, String symbol, String access, Action action) { 37 | this.name = name; 38 | this.symbol = symbol; 39 | this.access = access; 40 | this.action = action; 41 | } 42 | 43 | private LogAction(String name, String symbol, String access, Action action, 44 | BigDecimal price, BigDecimal amount) { 45 | this(name, symbol, access, action); 46 | this.price = price; 47 | this.amount = amount; 48 | } 49 | 50 | public static LogAction buyLimit(String name, String symbol, String access, 51 | BigDecimal price, BigDecimal amount) { 52 | return new LogAction(name, symbol, access, Action.BUY_LIMIT, price, amount); 53 | } 54 | 55 | public static LogAction sellLimit(String name, String symbol, String access, 56 | BigDecimal price, BigDecimal amount) { 57 | return new LogAction(name, symbol, access, Action.SELL_LIMIT, price, amount); 58 | } 59 | 60 | public static LogAction buyMarket(String name, String symbol, String access, BigDecimal quote) { 61 | LogAction la = new LogAction(name, symbol, access, Action.BUY_MARKET); 62 | la.quote = quote; 63 | return la; 64 | } 65 | 66 | public static LogAction sellMarket(String name, String symbol, String access, BigDecimal base) { 67 | LogAction la = new LogAction(name, symbol, access, Action.BUY_MARKET); 68 | la.base = base; 69 | return la; 70 | } 71 | 72 | public static LogAction multiBuy(String name, String symbol, String access, List rows) { 73 | LogAction la = new LogAction(name, symbol, access, Action.MULTI_BUY); 74 | la.rows = rows; 75 | return la; 76 | } 77 | 78 | public static LogAction multiSell(String name, String symbol, String access, List rows) { 79 | LogAction la = new LogAction(name, symbol, access, Action.MULTI_SELL); 80 | la.rows = rows; 81 | return la; 82 | } 83 | 84 | private static final String NAME = "name"; 85 | private static final String SYMBOL = "symbol"; 86 | private static final String ACCESS = "access"; 87 | private static final String ACTION = "action"; 88 | private static final String PRICE = "price"; 89 | private static final String AMOUNT = "amount"; 90 | private static final String QUOTE = "quote"; 91 | private static final String BASE = "base"; 92 | private static final String ROWS = "rows"; 93 | private static final String ID = "id"; 94 | private static final String IDS = "ids"; 95 | 96 | public static LogAction of(String json) { 97 | JSONObject r = JSON.parseObject(json); 98 | String name = null; 99 | String symbol = null; 100 | String access = null; 101 | Action action = null; 102 | if (r.containsKey(NAME)) { 103 | name = r.getString(NAME); 104 | } 105 | if (r.containsKey(SYMBOL)) { 106 | symbol = r.getString(SYMBOL); 107 | } 108 | if (r.containsKey(ACCESS)) { 109 | access = r.getString(ACCESS); 110 | } 111 | if (r.containsKey(ACTION)) { 112 | action = Action.valueOf(r.getString(ACTION)); 113 | } 114 | LogAction la = new LogAction(name, symbol, access, action); 115 | if (r.containsKey(PRICE)) { 116 | la.price = r.getBigDecimal(PRICE); 117 | } 118 | if (r.containsKey(AMOUNT)) { 119 | la.amount = r.getBigDecimal(AMOUNT); 120 | } 121 | if (r.containsKey(QUOTE)) { 122 | la.quote = r.getBigDecimal(QUOTE); 123 | } 124 | if (r.containsKey(BASE)) { 125 | la.base = r.getBigDecimal(BASE); 126 | } 127 | if (r.containsKey(ROWS)) { 128 | JSONArray a = r.getJSONArray(ROWS); 129 | la.rows = new ArrayList<>(a.size()); 130 | for (int i = 0; i < a.size(); i++) { 131 | la.rows.add(Row.row(a.getJSONObject(i).getBigDecimal("price"), 132 | a.getJSONObject(i).getBigDecimal("amount"))); 133 | } 134 | } 135 | if (r.containsKey(ID)) { 136 | la.id = r.getString(ID); 137 | } 138 | if (r.containsKey(IDS)) { 139 | JSONArray array = r.getJSONArray(IDS); 140 | la.ids = new ArrayList<>(array.size()); 141 | for (int i = 0; i < array.size(); i++) { 142 | la.ids.add(array.getString(i)); 143 | } 144 | } 145 | 146 | return la; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/model/ModelManager.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.model; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | /** 8 | * 管理某一类型对象 9 | * @author GOAi 10 | */ 11 | public class ModelManager { 12 | /** 13 | * 缓存对象 14 | */ 15 | private T model; 16 | 17 | /** 18 | * model是否超时 19 | */ 20 | private RateLimit limit; 21 | 22 | /** 23 | * 管理所有需要推送的函数 24 | */ 25 | private ConcurrentHashMap> observers = new ConcurrentHashMap<>(); 26 | 27 | public ModelManager(RateLimit limit) { 28 | this.limit = limit; 29 | } 30 | 31 | /** 32 | * 推送model 33 | * @param ready 是否准备好接收 34 | * @param model model 35 | * @param id 推送id 36 | */ 37 | public void on(boolean ready, T model, String id) { 38 | this.update(model); 39 | if (ready && this.on() && this.observers.containsKey(id)) { 40 | this.observers.get(id).on(model); 41 | } 42 | } 43 | 44 | /** 45 | * 是否正在推送 46 | */ 47 | public boolean on() { 48 | return !observers.isEmpty(); 49 | } 50 | 51 | /** 52 | * 注册推送 53 | * @param observer 注册者 54 | */ 55 | public void observe(ModelObserver observer) { 56 | this.observers.put(observer.getId(), observer); 57 | } 58 | 59 | /** 60 | * http方式不用推送 61 | * @param model model 62 | */ 63 | public void update(T model) { 64 | this.model = model; 65 | this.limit.update(); 66 | } 67 | 68 | public T getModel() { 69 | return model; 70 | } 71 | 72 | public RateLimit getLimit() { 73 | return limit; 74 | } 75 | 76 | public ConcurrentHashMap> getObservers() { 77 | return this.observers; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/model/ModelObserver.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.model; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | 5 | import java.util.function.Consumer; 6 | 7 | /** 8 | * 某个具体订阅 9 | * @author GOAi 10 | */ 11 | public class ModelObserver { 12 | /** 13 | * 消费函数 14 | */ 15 | final Consumer consumer; 16 | /** 17 | * 频率限制 18 | */ 19 | final RateLimit limit; 20 | 21 | /** 22 | * 推送id 23 | */ 24 | private final String id; 25 | 26 | public ModelObserver(Consumer consumer, RateLimit limit, String id) { 27 | this.consumer = consumer; 28 | this.limit = limit; 29 | this.id = id; 30 | } 31 | 32 | /** 33 | * 接收model 34 | * @param model 新的对象 35 | */ 36 | void on(T model) { 37 | if (this.limit.timeout()) { 38 | this.consumer.accept(model); 39 | } 40 | } 41 | 42 | public String getId() { 43 | return id; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/exchange/model/TradesObserver.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.exchange.model; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | import cqt.goai.model.market.Trade; 5 | import cqt.goai.model.market.Trades; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.ConcurrentLinkedDeque; 10 | import java.util.concurrent.ConcurrentLinkedQueue; 11 | import java.util.function.Consumer; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * @author GOAi 16 | */ 17 | public class TradesObserver extends ModelObserver { 18 | /** 19 | * 最多缓存个数 20 | */ 21 | private int max; 22 | /** 23 | * 对于有频率限制的推送,不能就此丢弃Trades 24 | * 缓存起来,等到能推送的时候一起推 25 | */ 26 | private ConcurrentLinkedDeque trades; 27 | 28 | public TradesObserver(Consumer consumer, RateLimit limit, String id) { 29 | this(consumer, limit, id, 200); 30 | } 31 | 32 | public TradesObserver(Consumer consumer, RateLimit limit, String id, int max) { 33 | super(consumer, limit, id); 34 | if (limit.isLimit()) { 35 | trades = new ConcurrentLinkedDeque<>(); 36 | this.max = max; 37 | } 38 | } 39 | 40 | @Override 41 | void on(Trades model) { 42 | if (super.limit.timeout()) { 43 | if (!this.trades.isEmpty()) { 44 | this.addFirst(model); 45 | model = new Trades(new ArrayList<>(this.trades)); 46 | } 47 | this.trades.clear(); 48 | super.consumer.accept(model); 49 | } else { 50 | this.addFirst(model); 51 | } 52 | } 53 | 54 | /** 55 | * 从前面加入 56 | */ 57 | private void addFirst(Trades trades) { 58 | List ts = trades.getList(); 59 | for (int i = ts.size() - 1; 0 <= i; i--) { 60 | this.trades.addFirst(ts.get(i)); 61 | } 62 | // 最多保存200个 63 | while (this.max < this.trades.size()) { 64 | this.trades.removeLast(); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/Const.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | /** 4 | * 常量 5 | * @author GOAi 6 | */ 7 | class Const { 8 | 9 | /** 10 | * 获取配置的url 11 | */ 12 | static final String START_URL = "--url="; 13 | 14 | /** 15 | * 配置文件名称,可以相对路径或绝对路径 16 | */ 17 | static final String START_NAME = "--name="; 18 | 19 | 20 | /** 21 | * 策略名称 22 | */ 23 | static final String CONFIG_STRATEGY_NAME = "strategy_name"; 24 | /** 25 | * 启动类名,java的run需要 26 | */ 27 | static final String CONFIG_CLASS_NAME = "class_name"; 28 | /** 29 | * 每个实例的配置 30 | */ 31 | static final String CONFIG_CONFIGS = "configs"; 32 | /** 33 | * 运行模式 如果是debug,不使用ProxyExchange 34 | */ 35 | static final String CONFIG_RUN_MODE = "run_mode"; 36 | // static final String CONFIG_RUN_MODE_DEBUG = "DEBUG"; 37 | 38 | /** 39 | * 默认的定时方法 40 | */ 41 | static final String DEFAULT_SCHEDULED_METHOD = "loop"; 42 | 43 | // 魔法变量 44 | static final String LEFT_SQUARE_BRACKETS = "["; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/Injectable.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import cqt.goai.run.exchange.Exchange; 4 | 5 | import java.util.function.Function; 6 | 7 | /** 8 | * 自定义类型实现该接口,可实现自动注入 9 | * @author GOAi 10 | */ 11 | public interface Injectable { 12 | 13 | /** 14 | * 注入 15 | * 16 | * @param config 配置内容 17 | * @param getExchange 获取exchange 参数为json字符串 18 | * {"name":"xxx","symbol":"BTC_USD","access":"xxx","secret":"xxx"} 19 | */ 20 | void inject(String config, Function getExchange); 21 | 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import lombok.Getter; 4 | 5 | import java.lang.reflect.Method; 6 | import java.util.List; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | /** 10 | * 方法运行信息 11 | * 12 | * @author GOAi 13 | */ 14 | @Getter 15 | class MethodInfo { 16 | 17 | /** 18 | * 方法 19 | */ 20 | private final Method method; 21 | 22 | /** 23 | * 方法运行范围 24 | */ 25 | private final MethodScope methodScope; 26 | 27 | /** 28 | * 方法运行锁,重点是全局一个锁,不检查无锁,实例内只用一个,定时范围新生成 29 | */ 30 | private final ReentrantLock globalLock; 31 | 32 | /** 33 | * 实例锁 34 | */ 35 | private final ReentrantLock instanceLock; 36 | 37 | /** 38 | * 定时任务信息 39 | */ 40 | private final List schedules; 41 | 42 | MethodInfo(Method method, MethodScope methodScope, List schedules) { 43 | this.method = method; 44 | this.methodScope = methodScope; 45 | this.schedules = schedules; 46 | if (methodScope == MethodScope.GLOBAL) { 47 | this.globalLock = new ReentrantLock(); 48 | this.instanceLock = null; 49 | } else { 50 | this.globalLock = null; 51 | this.instanceLock = null; 52 | } 53 | } 54 | 55 | private MethodInfo(Method method, MethodScope methodScope, ReentrantLock globalLock, ReentrantLock instanceLock, List schedules) { 56 | this.method = method; 57 | this.methodScope = methodScope; 58 | this.globalLock = globalLock; 59 | this.instanceLock = instanceLock; 60 | this.schedules = schedules; 61 | } 62 | 63 | MethodInfo copy() { 64 | switch (methodScope) { 65 | case INSTANCE: return new MethodInfo(method, methodScope, null, new ReentrantLock(), schedules); 66 | case SCHEDULED: return new MethodInfo(method, methodScope, null, null, schedules); 67 | default: return this; 68 | } 69 | } 70 | 71 | ReentrantLock getLock() { 72 | switch (methodScope) { 73 | case GLOBAL: return this.globalLock; 74 | case INSTANCE: return this.instanceLock; 75 | case SCHEDULED: return new ReentrantLock(); 76 | default: return null; 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/MethodScope.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | /** 4 | * 定时任务方法运行范围 5 | * 6 | * @author GOAi 7 | */ 8 | public enum MethodScope { 9 | 10 | /** 11 | * 全局唯一 12 | */ 13 | GLOBAL, 14 | 15 | /** 16 | * 实例唯一 17 | */ 18 | INSTANCE, 19 | 20 | /** 21 | * 循环任务唯一 22 | */ 23 | SCHEDULED, 24 | 25 | /** 26 | * 不限制 27 | */ 28 | NONE 29 | 30 | } 31 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/Notice.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import cqt.goai.exchange.ExchangeUtil; 4 | import dive.http.common.MimeRequest; 5 | import org.slf4j.Logger; 6 | 7 | import static dive.common.util.Util.useful; 8 | 9 | 10 | /** 11 | * 通知对象 12 | * 13 | * @author GOAi 14 | */ 15 | public class Notice { 16 | 17 | private final Logger log; 18 | 19 | private final String telegramGroup; 20 | 21 | private final String token; 22 | 23 | public Notice(Logger log, String telegramGroup, String token) { 24 | this.log = log; 25 | this.telegramGroup = telegramGroup; 26 | this.token = token; 27 | } 28 | 29 | /** 30 | * 高级通知,发电报 31 | * @param message 消息 32 | */ 33 | public void noticeHigh(String message) { 34 | this.log.info("NOTICE_HIGH {}", message); 35 | 36 | // FIX ME 发电报通知 37 | if (useful(this.telegramGroup)) { 38 | new MimeRequest.Builder() 39 | .url("https://api.telegram.org/bot" + token + "/sendMessage") 40 | .post() 41 | .body("chat_id", this.telegramGroup) 42 | .body("text", message) 43 | .body("parse_mode", "HTML") 44 | .execute(ExchangeUtil.OKHTTP); 45 | } 46 | } 47 | 48 | /** 49 | * 低级通知,打日志 50 | * @param message 消息 51 | */ 52 | public void noticeLow(String message) { 53 | this.log.info("NOTICE_LOW {}", message); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/RunTask.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import cqt.goai.model.market.Depth; 6 | import cqt.goai.model.market.Klines; 7 | import cqt.goai.model.market.Ticker; 8 | import cqt.goai.model.market.Trades; 9 | import cqt.goai.model.trade.Account; 10 | import cqt.goai.model.trade.Orders; 11 | import cqt.goai.run.exchange.Exchange; 12 | import cqt.goai.run.notice.BaseNotice; 13 | import cqt.goai.run.notice.Notice; 14 | import dive.cache.mime.PersistCache; 15 | import dive.common.util.DateUtil; 16 | import dive.common.util.TryUtil; 17 | import org.slf4j.Logger; 18 | 19 | import java.io.*; 20 | import java.util.Date; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.function.Function; 25 | import java.util.function.Supplier; 26 | 27 | /** 28 | * 抽象任务类 29 | * 30 | * @author GOAi 31 | */ 32 | public class RunTask { 33 | /** 34 | * 是否准备好推送,init方法结束后设置为true 35 | */ 36 | private Boolean ready = false; 37 | 38 | /** 39 | * 准备好了才推送 40 | */ 41 | final Supplier isReady = () -> this.ready; 42 | 43 | /** 44 | * 原始日志工具 45 | */ 46 | private Logger originLog; 47 | 48 | /** 49 | * 用户日志 50 | */ 51 | protected Logger log; 52 | 53 | /** 54 | * 通知对象 55 | */ 56 | protected Notice notice; 57 | 58 | /** 59 | * 配置名称 60 | */ 61 | protected String strategyName; 62 | 63 | /** 64 | * 本实例id 65 | */ 66 | protected Integer id; 67 | 68 | /** 69 | * 本实例配置 70 | */ 71 | protected JSONObject config; 72 | 73 | /** 74 | * 默认e 75 | */ 76 | protected Exchange e; 77 | 78 | /** 79 | * 获取exchange 参数为json字符串 80 | * {"name":"xxx","symbol":"BTC_USD","access":"xxx","secret":"xxx"} 81 | */ 82 | protected Function getExchange; 83 | 84 | public RunTask() {} 85 | 86 | /** 87 | * 初始化设置 88 | * @param strategyName 配置名称 89 | * @param id 实例id 90 | * @param config 实例配置 91 | */ 92 | void init(String strategyName, Integer id, JSONObject config, Logger log, List notices) { 93 | this.originLog = log; 94 | this.log = new UserLogger(log); 95 | this.notice = new Notice(log, notices); 96 | this.strategyName = strategyName; 97 | this.id = id; 98 | this.config = config; 99 | } 100 | 101 | /** 102 | * 初始化方法 103 | */ 104 | protected void init() {} 105 | 106 | /** 107 | * 默认循环 108 | */ 109 | protected void loop() {} 110 | 111 | /** 112 | * 退出方法 113 | */ 114 | protected void destroy() {} 115 | 116 | 117 | /** 118 | * 重写该方法,可自动对e进行主动推送Ticker 119 | * @param ticker 主动推送的ticker 120 | */ 121 | protected void onTicker(Ticker ticker) { } 122 | 123 | /** 124 | * 重写该方法,可自动对e进行主动推送Klines 125 | * K线是1分钟K线,100个 126 | * @param klines K线 127 | */ 128 | protected void onKlines(Klines klines) { } 129 | 130 | /** 131 | * 重写该方法,可自动对e进行主动推送Depth 132 | * @param depth 盘口深度 133 | */ 134 | protected void onDepth(Depth depth) { } 135 | 136 | /** 137 | * 重写该方法,可自动对e进行主动推送Trades 138 | * @param trades 交易信息 139 | */ 140 | protected void onTrades(Trades trades) { } 141 | 142 | /** 143 | * 重写该方法,可自动对e进行主动推送币对余额 144 | * @param account 币对余额 145 | */ 146 | protected void onAccount(Account account) { } 147 | 148 | /** 149 | * 重写该方法,可自动对e进行主动推送Orders 150 | * @param orders 订单信息 151 | */ 152 | protected void onOrders(Orders orders) { } 153 | 154 | 155 | void setGetExchange(Function getExchange) { 156 | this.getExchange = getExchange; 157 | } 158 | 159 | /** 160 | * 睡眠一段时间 161 | * @param millis 毫秒 162 | */ 163 | protected void sleep(long millis) { 164 | try { 165 | Thread.sleep(millis); 166 | } catch (InterruptedException e) { 167 | e.printStackTrace(); 168 | } 169 | } 170 | 171 | /** 172 | * 重试获取 173 | * @param supplier 获取函数 174 | * @param times 次数 175 | * @param millis 失败暂停时间 176 | * @param 结果对象 177 | * @return 结果 178 | */ 179 | protected T retry(Supplier supplier, int times, long millis) { 180 | return TryUtil.retry(supplier, times, () -> this.sleep(millis)); 181 | } 182 | 183 | /** 184 | * 重试获取 185 | * @param supplier 获取函数 186 | * @param times 次数 187 | * @param 结果对象 188 | * @return 结果 189 | */ 190 | protected T retry(Supplier supplier, int times) { 191 | return retry(supplier, times, 3000); 192 | } 193 | 194 | /** 195 | * 重试获取 196 | * @param supplier 获取函数 197 | * @param 结果对象 198 | * @return 结果 199 | */ 200 | protected T retry(Supplier supplier) { 201 | return retry(supplier, 5, 1000); 202 | } 203 | 204 | 205 | // ============================ 206 | 207 | /** 208 | * 全局持久化 209 | */ 210 | private static final PersistCache PERSIST_CACHE = new PersistCache<>(".global"); 211 | 212 | /** 213 | * 全局保存 214 | * @param key 键 215 | * @param value 值 216 | */ 217 | protected void global(String key, Serializable value) { 218 | PERSIST_CACHE.set(key, value); 219 | } 220 | 221 | /** 222 | * 读取值 223 | * @param key 键 224 | * @param 值类型 225 | * @return 读取的值 226 | */ 227 | @SuppressWarnings("unchecked") 228 | protected T global(String key) { 229 | return (T) PERSIST_CACHE.get(key); 230 | } 231 | 232 | /** 233 | * 持久化缓存 234 | */ 235 | private PersistCache persistCache; 236 | 237 | /** 238 | * 初始化缓存 239 | */ 240 | private void initPersistCache() { 241 | if (null == this.persistCache) { 242 | synchronized (this) { 243 | if (null == this.persistCache) { 244 | this.persistCache = new PersistCache<>(".global_" + id); 245 | } 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * 实例内保存 252 | * @param key 键 253 | * @param value 值 254 | */ 255 | protected void store(String key, Serializable value) { 256 | this.initPersistCache(); 257 | this.persistCache.set(key, value); 258 | } 259 | 260 | /** 261 | * 读取值 262 | * @param key 键 263 | * @param 值类型 264 | * @return 读取的值 265 | */ 266 | @SuppressWarnings("unchecked") 267 | protected T store(String key) { 268 | this.initPersistCache(); 269 | return (T) this.persistCache.get(key); 270 | } 271 | 272 | /** 273 | * 实时信息保存路径 274 | */ 275 | private static final String PATH_REALTIME = ".realtime"; 276 | static { 277 | File file = new File(PATH_REALTIME); 278 | if (!file.exists()) { 279 | boolean result = file.mkdir(); 280 | if (!result) { 281 | throw new RuntimeException("can not create dir: " + PATH_REALTIME); 282 | } 283 | } 284 | } 285 | 286 | /** 287 | * 存储实时信息 288 | * @param key 键 289 | * @param value 值 290 | */ 291 | protected void show(String key, Object value) { 292 | String message = JSON.toJSONString(value); 293 | File file = new File(PATH_REALTIME + "/" + key); 294 | if (!file.exists()) { 295 | try { 296 | boolean result = file.createNewFile(); 297 | if (!result) { 298 | throw new RuntimeException("can not create file: " + file.getPath()); 299 | } 300 | } catch (IOException e1) { 301 | e1.printStackTrace(); 302 | } 303 | } 304 | FileOutputStream os = null; 305 | try { 306 | os = new FileOutputStream(file, false); 307 | os.write(message.getBytes()); 308 | } catch (IOException e1) { 309 | e1.printStackTrace(); 310 | } finally { 311 | if (null != os) { 312 | try { 313 | os.close(); 314 | } catch (IOException e1) { 315 | e1.printStackTrace(); 316 | } 317 | } 318 | } 319 | } 320 | 321 | /** 322 | * 默认值保存的实时信息 323 | * @param value 值 324 | */ 325 | protected void show(String value) { 326 | Map map = new HashMap<>(2); 327 | map.put("type", "string"); 328 | map.put("value", value); 329 | this.show(".default", map); 330 | } 331 | 332 | /** 333 | * 默认值保存的实时表格信息 334 | * @param comment 描述信息 335 | * @param headers 表头 336 | * @param lists 每一行 337 | */ 338 | protected void show(String comment, List headers, List... lists) { 339 | Map map = new HashMap<>(4); 340 | map.put("type", "table"); 341 | map.put("comment", comment); 342 | map.put("header", headers); 343 | map.put("value", lists); 344 | this.show(".default", map); 345 | } 346 | 347 | /** 348 | * 输出盈利的时间序列数据 349 | * @param profit 盈利 350 | */ 351 | protected void profit(double profit) { 352 | String date = DateUtil.formatISO8601(new Date()); 353 | String p = String.format("%8.8f", profit); 354 | String message = date + " " + p; 355 | this.originLog.info("PROFIT {}", message); 356 | File file = new File(".profit"); 357 | try (FileWriter fw = new FileWriter(file, true)){ 358 | fw.write(message + "\n"); 359 | } catch (IOException e) { 360 | e.printStackTrace(); 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/ScheduledInfo.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.quartz.Trigger; 6 | import org.quartz.impl.triggers.CronTriggerImpl; 7 | import org.quartz.impl.triggers.SimpleTriggerImpl; 8 | 9 | import java.text.ParseException; 10 | import java.util.Date; 11 | 12 | import static dive.common.util.Util.exist; 13 | import static dive.common.util.Util.useful; 14 | 15 | /** 16 | * 单个方法的定时信息 17 | * @author GOAi 18 | */ 19 | @Getter 20 | @Slf4j 21 | class ScheduledInfo { 22 | 23 | /** 24 | * cron定时 25 | */ 26 | private String cron; 27 | 28 | /** 29 | * 固定频率 30 | */ 31 | private long fixedRate; 32 | /** 33 | * 延时 34 | */ 35 | private long delay; 36 | 37 | /** 38 | * 触发器 39 | */ 40 | private Trigger trigger; 41 | 42 | /** 43 | * 提示信息 44 | */ 45 | private String tip; 46 | 47 | /** 48 | * 构造器 49 | */ 50 | ScheduledInfo(String cron, long fixedRate, long delay) { 51 | this.cron = cron; 52 | this.fixedRate = fixedRate; 53 | this.delay = delay; 54 | initTrigger(); 55 | } 56 | 57 | /** 58 | * 设置触发器 59 | */ 60 | private void initTrigger() { 61 | initCronTrigger(); 62 | if (exist(trigger)) { 63 | return; 64 | } 65 | initSimpleTrigger(); 66 | if (exist(trigger)) { 67 | return; 68 | } 69 | log.error("init trigger error --> cron: {}, fixedRate: {}, delay: {}", 70 | this.cron, this.fixedRate, this.delay); 71 | } 72 | 73 | /** 74 | * 重置触发器 75 | */ 76 | void setCron(String cron) { 77 | this.cron = cron; 78 | this.trigger = null; 79 | initCronTrigger(); 80 | } 81 | 82 | /** 83 | * 重置触发器 84 | */ 85 | void setRate(long fixedRate) { 86 | this.fixedRate = fixedRate; 87 | this.delay = 3000; 88 | initSimpleTrigger(); 89 | } 90 | 91 | /** 92 | * 使用前应该复制一份 93 | */ 94 | ScheduledInfo copy() { 95 | return new ScheduledInfo(this.cron, this.fixedRate, this.delay); 96 | } 97 | 98 | /** 99 | * 设置触发器 100 | */ 101 | private void initCronTrigger() { 102 | if (!useful(this.cron)) { 103 | return; 104 | } 105 | try { 106 | CronTriggerImpl cronTrigger = new CronTriggerImpl(); 107 | cronTrigger.setCronExpression(this.cron); 108 | this.tip = "cron: " + this.cron; 109 | this.trigger = cronTrigger; 110 | this.fixedRate = -1; 111 | this.delay = -1; 112 | } catch (ParseException e) { 113 | log.error("can not format {} to cron", this.cron); 114 | } 115 | } 116 | 117 | 118 | /** 119 | * 设置触发器 120 | */ 121 | private void initSimpleTrigger() { 122 | if (this.fixedRate <= 0 || this.delay < 0) { 123 | return; 124 | } 125 | SimpleTriggerImpl simpleTrigger = new SimpleTriggerImpl(); 126 | simpleTrigger.setRepeatInterval(this.fixedRate); 127 | this.tip = "fixedRate: " + this.fixedRate + " ms delay: " + this.delay + " ms"; 128 | // 无限次 129 | simpleTrigger.setRepeatCount(-1); 130 | simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + this.delay)); 131 | this.trigger = simpleTrigger; 132 | this.cron = ""; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/ScheduledJob.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.quartz.Job; 5 | import org.quartz.JobExecutionContext; 6 | 7 | import java.util.List; 8 | 9 | import static dive.common.util.Util.exist; 10 | 11 | 12 | /** 13 | * 从Configs中找到要执行的任务,执行 14 | * @author GOAi 15 | */ 16 | @Slf4j 17 | public class ScheduledJob implements Job { 18 | 19 | private static Minister minister = Minister.getInstance(); 20 | 21 | /** 22 | * Command Pattern 命令模式 23 | * 封装任务命令定时执行 24 | * @param context 任务内容 25 | */ 26 | @Override 27 | public void execute(JobExecutionContext context) { 28 | if (!ScheduledJob.minister.isReady()) { 29 | return; 30 | } 31 | /* 32 | * Interpreter Patter 解释器模式 33 | * 定义规则,不同解释器解释结果不同 34 | * 运算表达式 统一的解释方法,加减乘除不同的处理方式 35 | */ 36 | String description = context.getJobDetail().getDescription(); 37 | String[] split = description.split("-"); 38 | Integer id = Integer.valueOf(split[0]); 39 | Integer index = Integer.valueOf(split[2]); 40 | TaskManager manager = ScheduledJob.minister.getManagers().get(id); 41 | if (!exist(manager)) { 42 | log.error("can not find TaskManager: {}", description); 43 | return; 44 | } 45 | List methodTasks = manager.getMethodTasks(); 46 | if (methodTasks.size() <= index) { 47 | manager.log.error("can not find MethodTask: {}, methodTasks.size: {}", 48 | description, methodTasks.size()); 49 | return; 50 | } 51 | ScheduledMethod mt = methodTasks.get(index); 52 | mt.invoke(description); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/ScheduledMethod.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import lombok.Getter; 4 | import org.slf4j.Logger; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.Method; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.concurrent.locks.ReentrantLock; 10 | 11 | /** 12 | * 单个定时任务方法 13 | * 14 | * @author GOAi 15 | */ 16 | @Getter 17 | class ScheduledMethod { 18 | 19 | private static Minister minister = Minister.getInstance(); 20 | 21 | private final Logger log; 22 | 23 | private final Integer id; 24 | 25 | private final RunTask runTask; 26 | 27 | private final Method method; 28 | 29 | private final MethodScope methodScope; 30 | 31 | private final ReentrantLock lock; 32 | 33 | private final ScheduledInfo scheduledInfo; 34 | 35 | ScheduledMethod(Logger log, Integer id, RunTask runTask, Method method, ReentrantLock lock, ScheduledInfo scheduledInfo, MethodScope methodScope) { 36 | this.log = log; 37 | this.id = id; 38 | this.runTask = runTask; 39 | this.method = method; 40 | this.methodScope = methodScope; 41 | this.lock = lock; 42 | this.scheduledInfo = scheduledInfo; 43 | } 44 | 45 | void invoke(String description) { 46 | if (minister.isStopping()) { 47 | return; 48 | } 49 | try { 50 | if (null == lock || lock.tryLock(0, TimeUnit.MILLISECONDS)) { 51 | try { 52 | method.invoke(runTask); 53 | } catch (IllegalAccessException | InvocationTargetException e) { 54 | e.printStackTrace(); 55 | log.error("run method failed : {} --> {} exception: {}", 56 | description, method.getName(), e); 57 | } finally { 58 | if (null != lock && lock.isLocked()) { 59 | lock.unlock(); 60 | } 61 | } 62 | } else { 63 | if (minister.isDebug()) { 64 | log.error("{} lock is locked... {}", description, id); 65 | } 66 | } 67 | } catch (InterruptedException e) { 68 | e.printStackTrace(); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/UserLogger.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.Marker; 5 | 6 | /** 7 | * 用户的log 8 | * 9 | * @author GOAi 10 | */ 11 | public class UserLogger implements Logger { 12 | 13 | private final Logger log; 14 | 15 | public UserLogger(Logger log) { 16 | this.log = log; 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | return log.getName(); 22 | } 23 | 24 | @Override 25 | public boolean isTraceEnabled() { 26 | return log.isTraceEnabled(); 27 | } 28 | 29 | @Override 30 | public void trace(String msg) { 31 | log.trace("USER " + msg); 32 | } 33 | 34 | @Override 35 | public void trace(String format, Object arg) { 36 | log.trace("USER " + format, arg); 37 | } 38 | 39 | @Override 40 | public void trace(String format, Object arg1, Object arg2) { 41 | log.trace("USER " + format, arg1, arg2); 42 | } 43 | 44 | @Override 45 | public void trace(String format, Object... arguments) { 46 | log.trace("USER " + format, arguments); 47 | } 48 | 49 | @Override 50 | public void trace(String msg, Throwable t) { 51 | log.trace("USER " + msg, t); 52 | } 53 | 54 | @Override 55 | public boolean isTraceEnabled(Marker marker) { 56 | return log.isTraceEnabled(marker); 57 | } 58 | 59 | @Override 60 | public void trace(Marker marker, String msg) { 61 | log.trace(marker, "USER " + msg); 62 | } 63 | 64 | @Override 65 | public void trace(Marker marker, String format, Object arg) { 66 | log.trace(marker, "USER " + format, arg); 67 | } 68 | 69 | @Override 70 | public void trace(Marker marker, String format, Object arg1, Object arg2) { 71 | log.trace(marker, "USER " + format, arg1, arg2); 72 | } 73 | 74 | @Override 75 | public void trace(Marker marker, String format, Object... argArray) { 76 | log.trace(marker, "USER " + format, argArray); 77 | } 78 | 79 | @Override 80 | public void trace(Marker marker, String msg, Throwable t) { 81 | log.trace(marker, "USER " + msg, t); 82 | } 83 | 84 | @Override 85 | public boolean isDebugEnabled() { 86 | return log.isDebugEnabled(); 87 | } 88 | 89 | @Override 90 | public void debug(String msg) { 91 | log.debug("USER " + msg); 92 | } 93 | 94 | @Override 95 | public void debug(String format, Object arg) { 96 | log.debug("USER " + format, arg); 97 | } 98 | 99 | @Override 100 | public void debug(String format, Object arg1, Object arg2) { 101 | log.debug("USER " + format, arg1, arg2); 102 | } 103 | 104 | @Override 105 | public void debug(String format, Object... arguments) { 106 | log.debug("USER " + format, arguments); 107 | } 108 | 109 | @Override 110 | public void debug(String msg, Throwable t) { 111 | log.debug("USER " + msg, t); 112 | } 113 | 114 | @Override 115 | public boolean isDebugEnabled(Marker marker) { 116 | return log.isDebugEnabled(marker); 117 | } 118 | 119 | @Override 120 | public void debug(Marker marker, String msg) { 121 | log.debug(marker, "USER " + msg); 122 | } 123 | 124 | @Override 125 | public void debug(Marker marker, String format, Object arg) { 126 | log.debug(marker, "USER " + format, arg); 127 | } 128 | 129 | @Override 130 | public void debug(Marker marker, String format, Object arg1, Object arg2) { 131 | log.debug(marker, "USER " + format, arg1, arg2); 132 | } 133 | 134 | @Override 135 | public void debug(Marker marker, String format, Object... arguments) { 136 | log.debug(marker, "USER " + format, arguments); 137 | } 138 | 139 | @Override 140 | public void debug(Marker marker, String msg, Throwable t) { 141 | log.debug(marker, "USER " + msg, t); 142 | } 143 | 144 | @Override 145 | public boolean isInfoEnabled() { 146 | return log.isInfoEnabled(); 147 | } 148 | 149 | @Override 150 | public void info(String msg) { 151 | log.info("USER " + msg); 152 | } 153 | 154 | @Override 155 | public void info(String format, Object arg) { 156 | log.info("USER " + format, arg); 157 | } 158 | 159 | @Override 160 | public void info(String format, Object arg1, Object arg2) { 161 | log.info("USER " + format, arg1, arg2); 162 | } 163 | 164 | @Override 165 | public void info(String format, Object... arguments) { 166 | log.info("USER " + format, arguments); 167 | } 168 | 169 | @Override 170 | public void info(String msg, Throwable t) { 171 | log.info("USER " + msg, t); 172 | } 173 | 174 | @Override 175 | public boolean isInfoEnabled(Marker marker) { 176 | return log.isInfoEnabled(marker); 177 | } 178 | 179 | @Override 180 | public void info(Marker marker, String msg) { 181 | log.info(marker, "USER " + msg); 182 | } 183 | 184 | @Override 185 | public void info(Marker marker, String format, Object arg) { 186 | log.info(marker, "USER " + format, arg); 187 | } 188 | 189 | @Override 190 | public void info(Marker marker, String format, Object arg1, Object arg2) { 191 | log.info(marker, "USER " + format, arg1, arg2); 192 | } 193 | 194 | @Override 195 | public void info(Marker marker, String format, Object... arguments) { 196 | log.info(marker, "USER " + format, arguments); 197 | } 198 | 199 | @Override 200 | public void info(Marker marker, String msg, Throwable t) { 201 | log.info(marker, "USER " + msg, t); 202 | } 203 | 204 | @Override 205 | public boolean isWarnEnabled() { 206 | return log.isWarnEnabled(); 207 | } 208 | 209 | @Override 210 | public void warn(String msg) { 211 | log.warn("USER " + msg); 212 | } 213 | 214 | @Override 215 | public void warn(String format, Object arg) { 216 | log.warn("USER " + format, arg); 217 | } 218 | 219 | @Override 220 | public void warn(String format, Object... arguments) { 221 | log.warn("USER " + format, arguments); 222 | } 223 | 224 | @Override 225 | public void warn(String format, Object arg1, Object arg2) { 226 | log.warn("USER " + format, arg1, arg2); 227 | } 228 | 229 | @Override 230 | public void warn(String msg, Throwable t) { 231 | log.warn("USER " + msg, t); 232 | } 233 | 234 | @Override 235 | public boolean isWarnEnabled(Marker marker) { 236 | return log.isWarnEnabled(marker); 237 | } 238 | 239 | @Override 240 | public void warn(Marker marker, String msg) { 241 | log.warn(marker, "USER " + msg); 242 | } 243 | 244 | @Override 245 | public void warn(Marker marker, String format, Object arg) { 246 | log.warn(marker, "USER " + format, arg); 247 | } 248 | 249 | @Override 250 | public void warn(Marker marker, String format, Object arg1, Object arg2) { 251 | log.warn(marker, "USER " + format, arg1, arg2); 252 | } 253 | 254 | @Override 255 | public void warn(Marker marker, String format, Object... arguments) { 256 | log.warn(marker, "USER " + format, arguments); 257 | } 258 | 259 | @Override 260 | public void warn(Marker marker, String msg, Throwable t) { 261 | log.warn(marker, "USER " + msg, t); 262 | } 263 | 264 | @Override 265 | public boolean isErrorEnabled() { 266 | return log.isErrorEnabled(); 267 | } 268 | 269 | @Override 270 | public void error(String msg) { 271 | log.error("USER " + msg); 272 | } 273 | 274 | @Override 275 | public void error(String format, Object arg) { 276 | log.error("USER " + format, arg); 277 | } 278 | 279 | @Override 280 | public void error(String format, Object arg1, Object arg2) { 281 | log.error("USER " + format, arg1, arg2); 282 | } 283 | 284 | @Override 285 | public void error(String format, Object... arguments) { 286 | log.error("USER " + format, arguments); 287 | } 288 | 289 | @Override 290 | public void error(String msg, Throwable t) { 291 | log.error("USER " + msg, t); 292 | } 293 | 294 | @Override 295 | public boolean isErrorEnabled(Marker marker) { 296 | return log.isErrorEnabled(marker); 297 | } 298 | 299 | @Override 300 | public void error(Marker marker, String msg) { 301 | log.error(marker, "USER " + msg); 302 | } 303 | 304 | @Override 305 | public void error(Marker marker, String format, Object arg) { 306 | log.error(marker, "USER " + format, arg); 307 | } 308 | 309 | @Override 310 | public void error(Marker marker, String format, Object arg1, Object arg2) { 311 | log.error(marker, "USER " + format, arg1, arg2); 312 | } 313 | 314 | @Override 315 | public void error(Marker marker, String format, Object... arguments) { 316 | log.error(marker, "USER " + format, arguments); 317 | } 318 | 319 | @Override 320 | public void error(Marker marker, String msg, Throwable t) { 321 | log.error(marker, "USER " + msg, t); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/main/Util.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.main; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static dive.common.util.Util.exist; 10 | 11 | 12 | /** 13 | * 工具类 14 | * @author GOAi 15 | */ 16 | class Util { 17 | 18 | /** 19 | * 检查文件是否存在 20 | */ 21 | static boolean checkFile(File file) { 22 | if (null == file) { 23 | return false; 24 | } 25 | if (file.isDirectory()) { 26 | return false; 27 | } 28 | if (!file.exists()) { 29 | try { 30 | boolean result = file.createNewFile(); 31 | if (!result) { 32 | return false; 33 | } 34 | } catch (IOException e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | /** 42 | * 写文件 43 | * @param file 文件 44 | * @param message 写的内容 45 | */ 46 | static void writeFile(File file, String message) { 47 | try (FileOutputStream os = new FileOutputStream(file, false)){ 48 | os.write(message.getBytes()); 49 | } catch (IOException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | 54 | /** 55 | * 获取url参数 56 | * @param url url 57 | */ 58 | static Map getParamsByUrl(String url){ 59 | String params = url.substring(url.indexOf("?") + 1); 60 | if(!exist(params)){ 61 | return null; 62 | } 63 | 64 | String[] paramsArr = params.split("&"); 65 | Map map = new HashMap<>(); 66 | for(String param : paramsArr){ 67 | String[] keyValue = param.split("="); 68 | map.put(keyValue[0], keyValue[1]); 69 | } 70 | 71 | return map; 72 | } 73 | 74 | /** 75 | * 获取url指定参数 76 | * @param url url 77 | * @param name 参数名 78 | */ 79 | static String getParamByUrl(String url, String name){ 80 | Map map = getParamsByUrl(url); 81 | 82 | if(map.isEmpty() || !map.containsKey(name)){ 83 | return null; 84 | } 85 | 86 | return map.get(name); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/notice/BaseNotice.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.notice; 2 | 3 | import org.slf4j.Logger; 4 | 5 | 6 | /** 7 | * 通知对象 8 | * 9 | * @author GOAi 10 | */ 11 | public abstract class BaseNotice { 12 | 13 | protected final Logger log; 14 | 15 | final String strategyName; 16 | 17 | BaseNotice(Logger log, String strategyName) { 18 | this.log = log; 19 | this.strategyName = strategyName; 20 | } 21 | 22 | /** 23 | * 高级通知,发电报 24 | * @param message 消息 25 | */ 26 | public abstract void notice(String message); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/notice/EmailNotice.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.notice; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.slf4j.Logger; 5 | 6 | import javax.mail.MessagingException; 7 | import javax.mail.Session; 8 | import javax.mail.Transport; 9 | import javax.mail.internet.InternetAddress; 10 | import javax.mail.internet.MimeMessage; 11 | import java.io.UnsupportedEncodingException; 12 | import java.util.Date; 13 | import java.util.Properties; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * 邮箱通知 18 | * 19 | * @author goai 20 | */ 21 | public class EmailNotice extends BaseNotice { 22 | 23 | private String mailSmtpHost = "smtp.exmail.qq.com"; 24 | private String mailSmtpPort = "465"; 25 | private String mailSmtpSocketFactoryPort = "465"; 26 | private String username; 27 | private String password; 28 | private String sender = "goai"; 29 | private String[] to; 30 | 31 | public EmailNotice(Logger log, String strategyName, JSONObject config) { 32 | super(log, strategyName); 33 | mailSmtpHost = config.containsKey("mailSmtpHost") ? config.getString("mailSmtpHost") : mailSmtpHost; 34 | mailSmtpPort = config.containsKey("mailSmtpPort") ? config.getString("mailSmtpPort") : mailSmtpPort; 35 | mailSmtpSocketFactoryPort = config.containsKey("mailSmtpSocketFactoryPort") ? config.getString("mailSmtpSocketFactoryPort") : mailSmtpSocketFactoryPort; 36 | username = config.getString("username"); 37 | password = config.getString("password"); 38 | sender = config.containsKey("sender") ? config.getString("sender") : sender; 39 | String to = config.getString("to"); 40 | if (to.startsWith("[")) { 41 | this.to = config.getJSONArray("to").stream() 42 | .map(Object::toString) 43 | .collect(Collectors.toList()) 44 | .toArray(new String[]{}); 45 | } else { 46 | this.to = new String[]{to}; 47 | } 48 | } 49 | 50 | private void send(String content) { 51 | Properties props = new Properties(); 52 | props.setProperty("mail.transport.protocol", "smtp"); 53 | props.setProperty("mail.smtp.host", mailSmtpHost); 54 | props.setProperty("mail.smtp.auth", "true"); 55 | props.setProperty("mail.smtp.port", mailSmtpPort); 56 | props.setProperty("mail.smtp.timeout", "5000"); 57 | props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); 58 | props.setProperty("mail.smtp.socketFactory.fallback", "false"); 59 | props.setProperty("mail.smtp.socketFactory.port", mailSmtpSocketFactoryPort); 60 | props.setProperty("mail.smtp.starttls.enable", "true"); 61 | props.setProperty("mail.smtp.starttls.required", "true"); 62 | props.setProperty("mail.smtp.ssl.enable", "true"); 63 | Session session = Session.getInstance(props); 64 | Transport transport = null; 65 | try { 66 | MimeMessage message = new MimeMessage(session); 67 | message.setFrom(new InternetAddress(username, sender, "UTF-8")); 68 | InternetAddress[] internetAddressTo = new InternetAddress[to.length]; 69 | for (int i = 0; i < internetAddressTo.length; i++) { 70 | internetAddressTo[i] = new InternetAddress(to[i]); 71 | } 72 | message.setRecipients(MimeMessage.RecipientType.TO, internetAddressTo); 73 | message.setSubject(strategyName, "UTF-8"); 74 | message.setContent(content, "text/plain;charset=UTF-8"); 75 | message.setSentDate(new Date()); 76 | message.saveChanges(); 77 | transport = session.getTransport(); 78 | transport.connect(username, password); 79 | transport.sendMessage(message, message.getAllRecipients()); 80 | } catch (UnsupportedEncodingException | MessagingException e) { 81 | e.printStackTrace(); 82 | } finally { 83 | if (null != transport) { 84 | try { 85 | transport.close(); 86 | } catch (MessagingException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | } 92 | 93 | @Override 94 | public void notice(String message) { 95 | this.send(message); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/notice/Notice.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.notice; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | /** 9 | * 通知管理 10 | * 11 | * @author goai 12 | */ 13 | public class Notice { 14 | 15 | protected final Logger log; 16 | 17 | private final List list; 18 | 19 | public Notice(Logger log, List notices) { 20 | this.log = log; 21 | list = notices; 22 | } 23 | 24 | /** 25 | * 高级通知,发电报 26 | * @param message 消息 27 | */ 28 | public void noticeHigh(String message, NoticeType... noticeTypes) { 29 | this.log.info("NOTICE_HIGH {}", message); 30 | List types = Arrays.asList(noticeTypes); 31 | list.stream().filter(n -> { 32 | if (types.isEmpty()) { 33 | return true; 34 | } 35 | Class c = n.getClass(); 36 | return (c == EmailNotice.class && types.contains(NoticeType.Email)) 37 | || (c == TelegramNotice.class && types.contains(NoticeType.Telegram)); 38 | }).forEach(n -> n.notice(message)); 39 | } 40 | 41 | /** 42 | * 低级通知,打日志 43 | * @param message 消息 44 | */ 45 | public void noticeLow(String message) { 46 | this.log.info("NOTICE_LOW {}", message); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/notice/NoticeType.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.notice; 2 | 3 | /** 4 | * 通知类型 5 | * 6 | * @author goai 7 | */ 8 | public enum NoticeType { 9 | /** 10 | * 邮箱 11 | */ 12 | Email, 13 | /** 14 | * 电报 15 | */ 16 | Telegram 17 | } 18 | -------------------------------------------------------------------------------- /engine/run/src/main/java/cqt/goai/run/notice/TelegramNotice.java: -------------------------------------------------------------------------------- 1 | package cqt.goai.run.notice; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import cqt.goai.exchange.ExchangeUtil; 5 | import dive.http.common.MimeRequest; 6 | import org.slf4j.Logger; 7 | 8 | 9 | /** 10 | * 通知对象 11 | * 12 | * @author GOAi 13 | */ 14 | public class TelegramNotice extends BaseNotice { 15 | 16 | private final String token; 17 | 18 | private final String chatId; 19 | 20 | public TelegramNotice(Logger log, String strategyName, JSONObject config) { 21 | super(log, strategyName); 22 | this.token = config.getString("token"); 23 | this.chatId = config.getString("chatId"); 24 | } 25 | 26 | /** 27 | * 高级通知,发电报 28 | * @param message 消息 29 | */ 30 | @Override 31 | public void notice(String message) { 32 | new MimeRequest.Builder() 33 | .url("https://api.telegram.org/bot" + this.token + "/sendMessage") 34 | .post() 35 | .body("chat_id", this.chatId) 36 | .body("text", strategyName + "\n" + message) 37 | .body("parse_mode", "HTML") 38 | .execute(ExchangeUtil.OKHTTP); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /engine/run/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} %5level - [%thread] %18logger{18} : %msg%n 6 | 7 | logs/log.log 8 | 9 | logs/past/%d{yyyy-MM-dd}.log.gz 10 | 11 | 31 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%5level) - [%thread] %cyan(%18logger{18}) : %msg%n 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /engine/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'goai' 2 | 3 | include ':model' // model bean 4 | include ':exchange' // 封装各交易所接口 http 和 websocket 5 | 6 | include ':run' // 策略运行基础模块 7 | include ':strategy' // 策略程序,java版本,结合model和exchange可以运行策略 8 | -------------------------------------------------------------------------------- /engine/strategy/build.gradle: -------------------------------------------------------------------------------- 1 | sourceSets { 2 | main { 3 | java { 4 | srcDirs = ['src/main/java'] 5 | } 6 | resources { 7 | srcDirs = ['src/main/resources'] 8 | } 9 | } 10 | } 11 | 12 | //processResources { 13 | // include "**/logback.xml" 14 | // include "**/config**.yml" 15 | //} 16 | 17 | jar { 18 | from { configurations.runtime.collect{zipTree(it)} } 19 | baseName = 'goai' 20 | manifest { 21 | attributes("Main-Class": "cqt.goai.run.Application") 22 | } 23 | } 24 | 25 | dependencies { 26 | testCompile 'junit:junit:4.12' 27 | 28 | // compile fileTree(dir: 'libs', includes: ['*.jar']) 29 | 30 | compile project(":run") 31 | 32 | 33 | } 34 | task copyJar(type:Copy){ 35 | from ('build\\libs') 36 | into ('.') 37 | } 38 | -------------------------------------------------------------------------------- /engine/strategy/package.bat: -------------------------------------------------------------------------------- 1 | gradle clean build -x test copyJar -------------------------------------------------------------------------------- /engine/strategy/package.sh: -------------------------------------------------------------------------------- 1 | gradle clean build -x test -------------------------------------------------------------------------------- /engine/strategy/src/main/java/Demo.java: -------------------------------------------------------------------------------- 1 | import cqt.goai.model.market.Ticker; 2 | import cqt.goai.run.main.RunTask; 3 | import cqt.goai.run.notice.NoticeType; 4 | 5 | import static dive.common.util.Util.exist; 6 | 7 | /** 8 | * 演示类 演示HTTP调用的基础使用方法 9 | * @author GOAi 10 | */ 11 | public class Demo extends RunTask { 12 | 13 | /** 14 | * 以下配置对应 config.yml 中的配置名 如需使用请先定义好 引擎会自动赋值 15 | */ 16 | private String myString; 17 | 18 | /** 19 | * 在这里可以完成策略初始化的工作 20 | */ 21 | @Override 22 | protected void init() { 23 | 24 | log.info("myString --> {}",myString); 25 | 26 | } 27 | 28 | /** 29 | * 默认一秒执行一次该函数 可在配置表中配置loop字段 指定时间 30 | */ 31 | @Override 32 | protected void loop() { 33 | Ticker ticker = e.getTicker(true); 34 | log.info("exchange name --> {} ticker last: {}", e.getName(), exist(ticker) ? ticker.getLast() : null); 35 | } 36 | 37 | /** 38 | * 程序关闭的时候调用 39 | */ 40 | @Override 41 | protected void destroy() { 42 | log.info(id + " destroy"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /engine/strategy/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 启动类 3 | * 4 | * @author GOAi 5 | */ 6 | public class Main { 7 | 8 | public static void main(String[] args) { 9 | cqt.goai.run.Application.main(args); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /engine/strategy/src/main/resources/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goaiquant/GOAi/0ffec9b2252ca9d8162c8b989b1775a27144e3fe/engine/strategy/src/main/resources/.gitignore -------------------------------------------------------------------------------- /engine/strategy/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | strategy_name: DemoStrategy # 策略名称 2 | class_name: 'Demo' # 启动类名, 也就是策略任务类名,唯一 3 | notices: 4 | - type: email 5 | # mailSmtpHost: 'smtp.exmail.qq.com' # 如过是qq邮箱则这三行的内容无需填写 6 | # mailSmtpPort: '465' 7 | # mailSmtpSocketFactoryPort: '465' 8 | username: 'xxx@xxx.com' 9 | password: 'password' 10 | to: 'you@yourmail.com' 11 | - type: telegram 12 | token: '1234123:AAFs' # 如需发电报通知 则需Token 13 | chatId: '-1232145235' # 发电报通知用群GroupID 14 | configs: # 策略配置需要的参数 15 | loop: 0.5 # 多少秒定期执行 loop 函数 如不配置默认1秒。 16 | e: # 交易所的配置方式 如果配置名为e 则写程序的时候可以直接调用e 17 | name: okexv3 18 | symbol: BTC_USDT 19 | access: publicKey_Passphrase # OkexV3 需要在公钥后加 "_你的Passphrase" 20 | secret: privateKey 21 | huobiex: # 配置了一个名字为huobiex的交易所 22 | name: huobipro 23 | symbol: BTC_USDT 24 | access: publicKey 25 | secret: privateKey 26 | exchanges: # 交易所数组的配置方式 27 | - name: okexv3 28 | symbol: EOS_BTC 29 | access: publicKey_Passphrase 30 | secret: privateKey 31 | - name: huobipro 32 | symbol: XRP_BTC 33 | access: publicKey 34 | secret: privateKey 35 | myString: Hello GOAi # 自定义字符串 36 | -------------------------------------------------------------------------------- /engine/strategy/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} %5level - [%thread] %18logger{18} : %msg%n 6 | 7 | logs/log.log 8 | 9 | logs/past/%d{yyyy-MM-dd}.log.gz 10 | 11 | 31 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%5level) - [%thread] %cyan(%18logger{18}) : %msg%n 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /qrcode_for_gh_9666b42fb2ca_430.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goaiquant/GOAi/0ffec9b2252ca9d8162c8b989b1775a27144e3fe/qrcode_for_gh_9666b42fb2ca_430.jpg -------------------------------------------------------------------------------- /strategy/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | logs 3 | .gradle 4 | .goai 5 | .idea 6 | *.log 7 | *.iml 8 | PID 9 | .mime_cache 10 | .run_info 11 | -------------------------------------------------------------------------------- /strategy/application/web.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goaiquant/GOAi/0ffec9b2252ca9d8162c8b989b1775a27144e3fe/strategy/application/web.jar -------------------------------------------------------------------------------- /strategy/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 4 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter/' } 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.1.RELEASE") 9 | } 10 | } 11 | 12 | repositories { 13 | maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } 14 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter/' } 15 | maven { url 'https://maven.yinian.tech/public/' } 16 | mavenCentral() 17 | } 18 | 19 | apply plugin: 'java' 20 | apply plugin: 'idea' 21 | 22 | idea { 23 | module { 24 | downloadSources = true 25 | inheritOutputDirs = false 26 | outputDir = file("build/classes/main/") 27 | testOutputDir = file("build/classes/test/") 28 | } 29 | } 30 | 31 | [compileJava,compileTestJava,javadoc]*.options*.encoding = 'UTF-8' 32 | 33 | sourceCompatibility = 1.8 34 | targetCompatibility = 1.8 35 | 36 | sourceSets { 37 | main { 38 | java { 39 | srcDirs = ['src/main/java'] 40 | } 41 | resources { 42 | srcDirs = ['src/main/resources'] 43 | } 44 | } 45 | } 46 | 47 | jar { 48 | from { configurations.runtime.collect{zipTree(it)} } 49 | baseName = 'strategy' 50 | manifest { 51 | attributes("Main-Class": "cqt.goai.run.Application") 52 | } 53 | } 54 | 55 | dependencies { 56 | testCompile 'junit:junit:4.12' 57 | 58 | compile fileTree(dir: 'libs', includes: ['*.jar']) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /strategy/libs/goai-engine.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goaiquant/GOAi/0ffec9b2252ca9d8162c8b989b1775a27144e3fe/strategy/libs/goai-engine.jar -------------------------------------------------------------------------------- /strategy/package.bat: -------------------------------------------------------------------------------- 1 | gradle clean build -x test -------------------------------------------------------------------------------- /strategy/package.sh: -------------------------------------------------------------------------------- 1 | gradle clean build -x test -------------------------------------------------------------------------------- /strategy/run_goai.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | chcp 65001 3 | TITLE 运行GOAi服务 4 | 5 | SET JAR_PATH="%cd%\application\web.jar" 6 | SET GOAI_PORT=7758 7 | 8 | SET LOCAL_JRE="%cd%\libs\jre1.8.0_171\bin\java" 9 | 10 | REM 检测安装JAVA环境 11 | if "%JAVA_HOME%"=="" ( 12 | SET LOCAL_JRE="%cd%\libs\jre1.8.0_171\bin\java.exe" 13 | 14 | if not exist %LOCAL_JRE% ( 15 | ECHO 请先安装JDK环境 16 | PAUSE 17 | EXIT 18 | ) 19 | 20 | ECHO JAVA环境准备就绪,开始启动GOAi服务... 21 | START /B %LOCAL_JRE% -Xms64m -Xmx250m -jar %JAR_PATH% --server.port=%GOAI_PORT% 22 | ) else ( 23 | ECHO 准备启动GOAi服务... 24 | START /B java -Xms64m -Xmx250m -jar %JAR_PATH% --server.port=%GOAI_PORT% 25 | ) 26 | 27 | ECHO. 28 | ECHO ******************************************************************* 29 | ECHO ** ** 30 | ECHO ** 请用谷歌浏览器本地访问 http://127.0.0.1:%GOAI_PORT%(默认7758端口) ** 31 | ECHO ** ** 32 | ECHO ** 若Web服务启动较慢,打开浏览器后请刷新再试 ** 33 | ECHO ** ** 34 | ECHO ** 默认登录账号:goai 登录密码:goai123456 ** 35 | ECHO ** ** 36 | ECHO ******************************************************************* 37 | ECHO. 38 | 39 | CHOICE /t 5 /d y /n >nul 40 | START iexplore "http://127.0.0.1:%GOAI_PORT%" 41 | -------------------------------------------------------------------------------- /strategy/run_goai.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | ##################################################### 3 | # 运行GOAi Web服务 4 | ##################################################### 5 | 6 | exist_jar_home=$(cat /etc/profile|grep 'export JRE_HOME') 7 | if [[ -z "${exist_jar_home}" ]]; then 8 | source /etc/profile 9 | fi 10 | 11 | if type java > /dev/null 2>&1; then 12 | echo -e "检测JDK环境正常" 13 | else 14 | echo -e "JDK未安装,无法启动服务" && exit 1 15 | fi 16 | 17 | JAR_PATH="./application/web.jar" 18 | GOAI_PORT=7758 19 | 20 | COUNT=`ps -ef|grep -w java|grep -w "\-jar"|grep -w "${JAR_PATH}"|grep -v grep | wc -l` 21 | if [ $COUNT -gt 0 ]; then 22 | echo "GOAi服务正在运行中,无需再次启动" 23 | else 24 | nohup java -Xms64m -Xmx250m -jar ${JAR_PATH} --server.port=${GOAI_PORT} > /dev/null 2>&1 & 25 | 26 | echo "GOAi服务已开启,若需开启远程访问,请先确保开放对应端口及设置防火墙" 27 | fi 28 | -------------------------------------------------------------------------------- /strategy/src/main/java/Demo.java: -------------------------------------------------------------------------------- 1 | import cqt.goai.model.market.Ticker; 2 | import cqt.goai.run.main.RunTask; 3 | import static dive.common.util.Util.exist; 4 | 5 | /** 6 | * 演示类 演示HTTP调用的基础使用方法 7 | * @author GOAi 8 | */ 9 | public class Demo extends RunTask { 10 | 11 | /** 12 | * 以下配置对应 config.yml 中的配置名 如需使用请先定义好 引擎会自动赋值 13 | */ 14 | private String myString; 15 | 16 | /** 17 | * 在这里可以完成策略初始化的工作 18 | */ 19 | @Override 20 | protected void init() { 21 | log.info("myString --> {}",myString); 22 | } 23 | 24 | /** 25 | * 默认一秒执行一次该函数 可在配置表中配置loop字段 指定时间 26 | */ 27 | @Override 28 | protected void loop() { 29 | Ticker ticker = e.getTicker(true); 30 | log.info("exchange name --> {} ticker last: {}", e.getName(), exist(ticker) ? ticker.getLast() : null); 31 | } 32 | 33 | /** 34 | * 程序关闭的时候调用 35 | */ 36 | @Override 37 | protected void destroy() { 38 | log.info(id + " destroy"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /strategy/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 启动类 3 | * 4 | * @author GOAi 5 | */ 6 | public class Main { 7 | 8 | public static void main(String[] args) { 9 | cqt.goai.run.Application.main(args); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /strategy/src/main/java/tutorial/Tutorial01.java: -------------------------------------------------------------------------------- 1 | package tutorial; 2 | 3 | import cqt.goai.run.main.RunTask; 4 | 5 | /** 6 | * 教程1: 本教程你将了解 1、你的策略类需继承的父类。2、策略基础的三个方法 7 | * 1、GOAi引擎会封装好 运行策略的相关工作。你只需新建好你的策略类 并继承RunRask即可。 8 | * 2、有三个基础函数可能需要重写 init()、loop()、destroy() 。 9 | * init()顾名思义负责做初始化的工作。如果你的策略没有需要做的准备工作则无需重写该函数。 10 | * loop()你的策略逻辑在该函数中实现。函数默认1秒引擎会调用一次,如需更改请在配置中指定loop的值。 11 | * destroy() 策略关闭前会调用一次,如你的策略无需做收尾工作则不用重写该函数。 12 | */ 13 | 14 | public class Tutorial01 extends RunTask { 15 | 16 | /** 17 | * 在这里可以完成策略初始化的工作 18 | */ 19 | @Override 20 | protected void init() { 21 | log.info("Hello GOAi! 在这里可以完成你的策略的准备工作。"); 22 | } 23 | 24 | /** 25 | * 默认一秒执行一次该函数 可在配置表中配置loop字段 指定时间 例如0.5 为每半秒执行一次。 26 | */ 27 | @Override 28 | protected void loop() { 29 | log.info("该函数固定时间会调用一次。在这里写你的策略逻辑吧 就是这么简单!"); 30 | } 31 | 32 | /** 33 | * 程序关闭的时候调用 34 | */ 35 | @Override 36 | protected void destroy() { 37 | log.info("在这里处理你策略关闭的时候需要做的收尾工作,仅执行一次。"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /strategy/src/main/java/tutorial/Tutorial02.java: -------------------------------------------------------------------------------- 1 | package tutorial; 2 | 3 | import cqt.goai.model.market.Ticker; 4 | import cqt.goai.run.main.RunTask; 5 | 6 | import static dive.common.util.Util.exist; 7 | 8 | /** 9 | * 教程2:本教程你将了解 如何进行策略的配置。 10 | * 配置策略参数的两种方式:1、会优先在线获取配置:通过GOAi后台配置策略对应参数的值,并在策略中定义该变量名 11 | * 引擎启动后会自动把线上参数的值赋值给同名变量。 12 | * 2、如果线上配置获取不成功或本地调试的时候会从本地的resources目录下的 config.yml 中获取参数值并赋值给策略中 13 | * 定义好的变量。 14 | * config.yml 配置信息中 15 | * strategy_name:是该策略的名称 可以任意取。 16 | * class_name:是你策略的全类名。例如该教程 class_name: 'tutorial.Tutorial02' 17 | * configs: configs下定义好策略配置需要的参数。除 loop、e、telegramToken、telegramGroup 18 | * 外的变量名须在策略中定义好同名变量引擎才会自动赋值。 19 | * 例如:loop: 0.5 指定了loop 函数0.5秒执行一次 20 | * 交易所的配置方式可以参照yml 文件。因okexv3交易所密钥多了一个字段 需在公钥后加 下划线指定。 21 | */ 22 | 23 | public class Tutorial02 extends RunTask { 24 | 25 | /** 26 | * 以下配置对应 config.yml 中的配置名 如需使用请先定义好 引擎会自动赋值 27 | */ 28 | private String myString; 29 | 30 | @Override 31 | protected void init() { 32 | log.info("打印了config.yml 中配置的变量 myString:{}",myString); 33 | log.info("loop函数获取了交易所的Ticker 如网络无法连接 等待几秒后会有错误信息 connect timed out"); 34 | } 35 | 36 | @Override 37 | protected void loop() { 38 | Ticker ticker = e.getTicker(); 39 | log.info("exchange name --> {} ticker last: {}", e.getName(), exist(ticker) ? ticker.getLast() : null); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /strategy/src/main/java/tutorial/Tutorial03.java: -------------------------------------------------------------------------------- 1 | package tutorial; 2 | 3 | import cqt.goai.run.main.RunTask; 4 | import cqt.goai.run.notice.NoticeType; 5 | 6 | import java.util.Arrays; 7 | 8 | /** 9 | * 教程2:本教程你将了解 如何通知 和 持久化信息。 10 | */ 11 | 12 | public class Tutorial03 extends RunTask { 13 | 14 | @Override 15 | protected void init() { 16 | notice.noticeLow("low notice"); 17 | notice.noticeHigh("high notice"); 18 | notice.noticeHigh("high notice email", NoticeType.Email); 19 | notice.noticeHigh("high notice telegram", NoticeType.Telegram); 20 | 21 | // 1 全局存储和读取信息 22 | global("test1", 123); 23 | int value1 = global("test1"); 24 | 25 | // 2 实时显示信息 26 | // 显示字符串 27 | show("show string"); 28 | // 显示表格 29 | show("show table", Arrays.asList("标题1", "标题2"), 30 | Arrays.asList("行11", "行12"), 31 | Arrays.asList("行21", "行22")); 32 | 33 | // 3 存储收益,显示收益曲线 34 | profit(1234); 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /strategy/src/main/java/tutorial/Tutorial04.java: -------------------------------------------------------------------------------- 1 | package tutorial; 2 | 3 | import cqt.goai.exchange.util.RateLimit; 4 | import cqt.goai.model.market.Ticker; 5 | import cqt.goai.run.exchange.Exchange; 6 | import cqt.goai.run.main.RunTask; 7 | import cqt.goai.run.notice.NoticeType; 8 | 9 | import java.util.Arrays; 10 | 11 | /** 12 | * 教程2:本教程你将了解 如何接受主动推送信息。 13 | * onTicker 主动推送ticker 14 | * onKlines 主动推送klines 15 | * onDepth 主动推送depth 16 | * onTrades 主动推送trades 17 | * onAccount 主动推送account 18 | * onOrders 主动推送orders 19 | * 20 | * 以ticker为例,2种推送方式: 21 | * 1. 名为 属性名OnTicker(Ticker ticker) 的方法 22 | * 2. 调用对应Exchange的setOnTicker(Consumer consumer[, RateLimit limit])方法, 23 | * 该方法可限制推送频率不高于某个秒数, RateLimit.limit(mills) 或使用默认的 RateLimit.second3()等 24 | * 25 | */ 26 | 27 | public class Tutorial04 extends RunTask { 28 | 29 | /** 30 | * 有这个Exchange才会有推送 31 | */ 32 | private Exchange huobiE; 33 | 34 | @Override 35 | protected void init() { 36 | 37 | // 这种方式设置的主动推送,可以设置间隔时间,例:second3 表明两次推送的最小间隔不超过3秒 38 | huobiE.setOnTicker(ticker -> { 39 | log.info("huobi e 2 ticker -> {}", ticker); 40 | }, RateLimit.second3()); 41 | 42 | } 43 | 44 | /** 45 | * 属于参数 huobiE 的主动推送, 46 | * 以 huobiE 开头,并且只有一个Ticker参数的方法会默认设置推送 47 | * @param ticker 48 | */ 49 | protected void huobiEOnTicker(Ticker ticker) { 50 | log.info("huobi e 1 ticker -> {}", ticker); 51 | } 52 | 53 | /** 54 | * 默认为 e 的主动推送 55 | * @param ticker 56 | */ 57 | @Override 58 | protected void onTicker(Ticker ticker) { 59 | log.info("e ticker -> {}", ticker); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /strategy/src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | strategy_name: Tutorial01 # 策略名称 2 | class_name: 'tutorial.Tutorial02' # 启动类名, 也就是策略任务类名,唯一 3 | notices: 4 | - type: email 5 | # mailSmtpHost: 'smtp.exmail.qq.com' 6 | # mailSmtpPort: '465' 7 | # mailSmtpSocketFactoryPort: '465' 8 | username: 'xxx@xxx.com' 9 | password: 'password' 10 | to: 'you@yourmail.com' 11 | - type: telegram 12 | token: '1234123:AAFs' # 如需发电报通知 则需Token 13 | chatId: '-1232145235' # 发电报通知用群GroupID 14 | configs: # 策略配置需要的参数 15 | loop: 0.5 # 多少秒定期执行 loop 函数 如不配置默认1秒。 16 | e: # 交易所的配置方式 如果配置名为e 则写程序的时候可以直接调用e 17 | name: okexv3 18 | symbol: BTC_USDT 19 | access: publicKey_Passphrase # OkexV3 需要在公钥后加 "_你的Passphrase" 20 | secret: privateKey 21 | huobiex: # 配置了一个名字为huobiex的交易所 22 | name: huobipro 23 | symbol: BTC_USDT 24 | access: publicKey 25 | secret: privateKey 26 | exchanges: # 交易所数组的配置方式 27 | - name: okexv3 28 | symbol: EOS_BTC 29 | access: publicKey_Passphrase 30 | secret: privateKey 31 | - name: huobipro 32 | symbol: XRP_BTC 33 | access: publicKey 34 | secret: privateKey 35 | myString: Hello GOAi # 自定义字符串 36 | --------------------------------------------------------------------------------