├── .DS_Store ├── src ├── .DS_Store ├── main │ ├── resources │ │ ├── application.properties │ │ └── logback.xml │ └── java │ │ └── com │ │ └── aixi │ │ └── lv │ │ ├── config │ │ ├── BuyConfig.java │ │ ├── ApiKeyConfig.java │ │ ├── RestTemplateConfig.java │ │ ├── ExecutorConfig.java │ │ ├── ProfitRateConfig.java │ │ ├── MacdTradeConfig.java │ │ ├── BackTestConfig.java │ │ └── ExchangeInfoConfig.java │ │ ├── util │ │ ├── ApiUtil.java │ │ ├── RamUtil.java │ │ ├── SpringContextUtil.java │ │ ├── TimeUtil.java │ │ ├── EncryptUtil.java │ │ ├── CombineUtil.java │ │ └── NumUtil.java │ │ ├── LvApplication.java │ │ ├── model │ │ ├── constant │ │ │ ├── OrderSide.java │ │ │ ├── ContractSide.java │ │ │ ├── Interval.java │ │ │ ├── MacdOptType.java │ │ │ ├── OrderRespType.java │ │ │ ├── OrderTimeInForce.java │ │ │ ├── OrderType.java │ │ │ ├── OrderStatus.java │ │ │ ├── CurrencyType.java │ │ │ ├── TradePairStatus.java │ │ │ └── Symbol.java │ │ ├── domain │ │ │ ├── ExchangeInfoAmountFilter.java │ │ │ ├── Asset.java │ │ │ ├── Result.java │ │ │ ├── HighFrequency.java │ │ │ ├── ExchangeInfoQtyFilter.java │ │ │ ├── ExchangeInfoPriceFilter.java │ │ │ ├── BackTestData.java │ │ │ ├── Box.java │ │ │ ├── TradePair.java │ │ │ ├── ContractAccount.java │ │ │ ├── Account.java │ │ │ ├── MacdAccount.java │ │ │ └── KLine.java │ │ └── indicator │ │ │ ├── Indicator.java │ │ │ ├── EMA.java │ │ │ ├── SMA.java │ │ │ ├── BOLL.java │ │ │ ├── RSI.java │ │ │ └── MACD.java │ │ ├── service │ │ ├── BackTestCommonService.java │ │ ├── PriceFaceService.java │ │ ├── AccountService.java │ │ ├── HttpService.java │ │ ├── IndicatorService.java │ │ ├── PriceService.java │ │ ├── BackTestReadService.java │ │ └── BackTestAppendService.java │ │ ├── controller │ │ ├── TestController.java │ │ └── OrderController.java │ │ ├── schedule │ │ ├── BollTask.java │ │ └── offline │ │ │ ├── ReBuyTask.java │ │ │ ├── StopLossTask.java │ │ │ ├── ProfitTask.java │ │ │ └── BuyTask.java │ │ ├── strategy │ │ ├── indicator │ │ │ ├── MacdRateSellStrategy.java │ │ │ └── BollSellStrategy.java │ │ ├── profit │ │ │ ├── SecondProfitStrategy.java │ │ │ ├── FirstProfitStrategy.java │ │ │ └── ThirdProfitStrategy.java │ │ ├── contract │ │ │ └── PriceContractStrategy.java │ │ └── defense │ │ │ └── DefenseStrategy.java │ │ └── analysis │ │ ├── SymbolAlternativeAnalysis.java │ │ └── ContractBackTestAnalysis.java └── test │ └── java │ └── com │ └── aixi │ └── lv │ ├── BaseTest.java │ ├── 合约回测Test.java │ ├── 预警分析Test.java │ ├── 箱体Test.java │ ├── EasyTest.java │ ├── 价格Test.java │ ├── 近期回测Test.java │ └── 准备数据Test.java ├── .idea ├── vcs.xml ├── encodings.xml ├── misc.xml ├── compiler.xml ├── jarRepositories.xml └── workspace.xml ├── README.md └── pom.xml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/js3560750/Hermes/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/js3560750/Hermes/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8080 2 | spring.mail.port=465 3 | spring.mail.protocol=smtps 4 | 5 | ## 引入Swagger后需要设置这个,否则起不来 6 | spring.mvc.pathmatch.matching-strategy=ant_path_matcher -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/BuyConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.math.BigDecimal; 4 | 5 | /** 6 | * @author Js 7 | */ 8 | public class BuyConfig { 9 | 10 | /** 11 | * 单笔买入金额 USDT 12 | */ 13 | public static final BigDecimal BUY_AMOUNT = new BigDecimal("50"); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/ApiUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | /** 4 | * @author Js 5 | 6 | */ 7 | public class ApiUtil { 8 | 9 | public static final String URL_HOST = "https://api.binance.com"; 10 | 11 | public static String url(String url) { 12 | return URL_HOST + url; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/LvApplication.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class LvApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(LvApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/ApiKeyConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | /** 4 | * @author Js 5 | */ 6 | public class ApiKeyConfig { 7 | 8 | public static final String API_KEY = "*********************"; 9 | 10 | public static final String SECRET_KEY = "*********************"; 11 | 12 | public static final String WEB_PASSWORD = "*********************"; 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/BaseTest.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | class BaseTest { 11 | 12 | @Test 13 | void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/OrderSide.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 订单方向 9 | */ 10 | public enum OrderSide { 11 | 12 | /** 13 | * 买入 14 | */ 15 | BUY("BUY"), 16 | 17 | /** 18 | * 卖出 19 | */ 20 | SELL("SELL"), 21 | ; 22 | 23 | OrderSide(String code) { 24 | this.code = code; 25 | } 26 | 27 | @Getter 28 | private String code; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/ContractSide.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 合约方向 9 | */ 10 | public enum ContractSide { 11 | 12 | /** 13 | * 做多 14 | */ 15 | LONG("LONG"), 16 | 17 | /** 18 | * 做空 19 | */ 20 | SHORT("SHORT"), 21 | ; 22 | 23 | ContractSide(String code) { 24 | this.code = code; 25 | } 26 | 27 | @Getter 28 | private String code; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/RamUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import org.apache.lucene.util.RamUsageEstimator; 4 | 5 | /** 6 | * @author Js 7 | 8 | */ 9 | public class RamUtil { 10 | 11 | /** 12 | * 返回对象内存大小 13 | * 14 | * @param object 15 | * @return 16 | */ 17 | public static String getRamSize(Object object) { 18 | 19 | long l = RamUsageEstimator.sizeOfObject(object); 20 | String s = RamUsageEstimator.humanReadableUnits(l); 21 | 22 | return s; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/Interval.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * k线间隔 9 | */ 10 | public enum Interval { 11 | 12 | MINUTE_1("1m"), 13 | MINUTE_3("3m"), 14 | MINUTE_5("5m"), 15 | MINUTE_15("15m"), 16 | MINUTE_30("30m"), 17 | HOUR_1("1h"), 18 | HOUR_2("2h"), 19 | HOUR_4("4h"), 20 | DAY_1("1d"), 21 | ; 22 | 23 | Interval(String code) { 24 | this.code = code; 25 | } 26 | 27 | @Getter 28 | private String code; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | /** 9 | * @author Js 10 | */ 11 | @Configuration 12 | public class RestTemplateConfig { 13 | 14 | @Bean 15 | public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { 16 | return restTemplateBuilder.build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/MacdOptType.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * MACD策略的操作类型 9 | */ 10 | public enum MacdOptType { 11 | 12 | /** 13 | * 所有操作皆可 14 | */ 15 | ALL("ALL"), 16 | 17 | /** 18 | * 只买 19 | */ 20 | ONLY_BUY("ONLY_BUY"), 21 | 22 | /** 23 | * 只卖 24 | */ 25 | ONLY_SELL("ONLY_SELL"), 26 | 27 | 28 | ; 29 | 30 | MacdOptType(String code) { 31 | this.code = code; 32 | } 33 | 34 | @Getter 35 | private String code; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/OrderRespType.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 订单返回类型 9 | */ 10 | public enum OrderRespType { 11 | 12 | /** 13 | * 返回速度最快,不包含成交信息,信息量最少 14 | */ 15 | ACK("ACK"), 16 | 17 | /** 18 | * 返回速度居中,返回吃单成交的少量信息 19 | */ 20 | RESULT("RESULT"), 21 | 22 | /** 23 | * 返回速度最慢,返回吃单成交的详细信息 24 | */ 25 | FULL("FULL"), 26 | ; 27 | 28 | OrderRespType(String code) { 29 | this.code = code; 30 | } 31 | 32 | @Getter 33 | private String code; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/ExchangeInfoAmountFilter.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * @author Js 12 | 13 | * 14 | * 服务器交易信息-数量过滤器 15 | */ 16 | @Data 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class ExchangeInfoAmountFilter { 21 | 22 | /** 23 | * 过滤器类型 24 | */ 25 | private String filterType; 26 | 27 | /** 28 | * 一笔订单允许的最小金额 29 | */ 30 | private BigDecimal minNotional; 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/SpringContextUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author Js 10 | 11 | */ 12 | @Component 13 | public class SpringContextUtil implements ApplicationContextAware { 14 | 15 | public static ApplicationContext applicationContext; 16 | 17 | @Override 18 | public void setApplicationContext(ApplicationContext context) throws BeansException { 19 | SpringContextUtil.applicationContext = context; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/OrderTimeInForce.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 订单有效方式 9 | * 10 | * 这里定义了订单多久能够失效 11 | */ 12 | public enum OrderTimeInForce { 13 | 14 | /** 15 | * 成交为止 16 | * 订单会一直有效,直到被成交或者取消。 17 | */ 18 | GTC("GTC"), 19 | 20 | /** 21 | * 无法立即成交的部分就撤销 22 | * 订单在失效前会尽量多的成交。 23 | */ 24 | IOC("IOC"), 25 | 26 | /** 27 | * 无法全部立即成交就撤销 28 | * 如果无法全部成交,订单会失效。 29 | */ 30 | FOK("FOK"), 31 | ; 32 | 33 | OrderTimeInForce(String code) { 34 | this.code = code; 35 | } 36 | 37 | @Getter 38 | private String code; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/Asset.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import com.aixi.lv.model.constant.CurrencyType; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | /** 12 | * @author Js 13 | * 14 | * 货币资产 15 | * 16 | */ 17 | @Data 18 | @Builder 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class Asset { 22 | 23 | /** 24 | * 币种 25 | */ 26 | private CurrencyType currencyType; 27 | 28 | /** 29 | * 可自由交易数量 30 | */ 31 | private BigDecimal freeQty; 32 | 33 | /** 34 | * 锁定数量 35 | */ 36 | private BigDecimal lockedQty; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/Indicator.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.util.List; 4 | 5 | public interface Indicator { 6 | 7 | //Used to get the latest indicator value updated with closed candle 8 | double get(); 9 | 10 | //Used to get value of indicator simulated with the latest non-closed price 11 | double getTemp(double newPrice); 12 | 13 | //Used in constructor to set initial value 14 | void init(List closingPrices); 15 | 16 | //Used to update value with latest closed candle closing price 17 | void update(double newPrice); 18 | 19 | //Used to check for buy signal 20 | int check(double newPrice); 21 | 22 | String getExplanation(); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/Result.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Js 7 | */ 8 | @Data 9 | public class Result { 10 | 11 | private T data; 12 | 13 | private Boolean success; 14 | 15 | private String errorMsg; 16 | 17 | public Result(T data, Boolean success, String errorMsg) { 18 | this.data = data; 19 | this.success = success; 20 | this.errorMsg = errorMsg; 21 | } 22 | 23 | public static Result fail(String errorMsg) { 24 | return new Result(null, Boolean.FALSE, errorMsg); 25 | } 26 | 27 | public static Result success(T data) { 28 | return new Result(data, Boolean.TRUE, null); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/OrderType.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 订单类型 9 | */ 10 | public enum OrderType { 11 | 12 | /** 13 | * 限价单 14 | */ 15 | LIMIT("LIMIT"), 16 | 17 | /** 18 | * 市价单 19 | */ 20 | MARKET("MARKET"), 21 | 22 | /** 23 | * 止损单 24 | */ 25 | STOP_LOSS("STOP_LOSS"), 26 | 27 | /** 28 | * 限价止损单 29 | */ 30 | STOP_LOSS_LIMIT("STOP_LOSS_LIMIT"), 31 | 32 | /** 33 | * 止盈单 34 | */ 35 | TAKE_PROFIT("TAKE_PROFIT"), 36 | 37 | /** 38 | * 限价止盈单 39 | */ 40 | TAKE_PROFIT_LIMIT("TAKE_PROFIT_LIMIT"), 41 | ; 42 | 43 | OrderType(String code) { 44 | this.code = code; 45 | } 46 | 47 | @Getter 48 | private String code; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/BackTestCommonService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import com.aixi.lv.model.domain.MacdAccount; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | 7 | import static com.aixi.lv.config.BackTestConfig.OPEN; 8 | import static com.aixi.lv.config.MacdTradeConfig.THREAD_LOCAL_ACCOUNT; 9 | 10 | /** 11 | * @author Js 12 | * 13 | * 回测通用服务 14 | */ 15 | @Component 16 | @Slf4j 17 | public class BackTestCommonService { 18 | 19 | /** 20 | * 返回当前回测的自然时间 21 | * 22 | * 23 | * @return 24 | */ 25 | public String backTestNatureTime() { 26 | 27 | 28 | if (!OPEN) { 29 | return "非回测"; 30 | } 31 | 32 | MacdAccount account = THREAD_LOCAL_ACCOUNT.get(); 33 | 34 | return account.getCurBackTestComputeTime().toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/HighFrequency.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import com.aixi.lv.model.constant.Interval; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | /** 12 | * @author Js 13 | * 14 | */ 15 | @Data 16 | @Builder 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class HighFrequency { 20 | 21 | /** 22 | * 高频扫描标记 23 | * true 执行,false 不执行 24 | */ 25 | private Boolean scanFlag; 26 | 27 | /** 28 | * 扫描参考的箱底价格 29 | */ 30 | private BigDecimal bottomPrice; 31 | 32 | /** 33 | * 扫描参考的箱顶价格 34 | */ 35 | private BigDecimal topPrice; 36 | 37 | /** 38 | * k线间隔 39 | */ 40 | private Interval interval; 41 | 42 | /** 43 | * k线数量 44 | */ 45 | private Integer limit; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/ExchangeInfoQtyFilter.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * @author Js 12 | * @date 2022/1/2 11:14 上午 13 | * 14 | * 服务器交易信息-数量过滤器 15 | */ 16 | @Data 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class ExchangeInfoQtyFilter { 21 | 22 | /** 23 | * 过滤器类型 24 | */ 25 | private String filterType; 26 | 27 | /** 28 | * 允许的最大数量 29 | */ 30 | private BigDecimal maxQty; 31 | 32 | /** 33 | * 允许的最小数量 34 | */ 35 | private BigDecimal minQty; 36 | 37 | /** 38 | * 数量步长 39 | */ 40 | private BigDecimal stepSize; 41 | 42 | /** 43 | * 数量 BigDecimal的 scale 44 | */ 45 | private Integer qtyScale; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/ExchangeInfoPriceFilter.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * @author Js 12 | * @date 2022/1/2 11:14 上午 13 | * 14 | * 服务器交易信息-价格过滤器 15 | */ 16 | @Data 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class ExchangeInfoPriceFilter { 21 | 22 | /** 23 | * 过滤器类型 24 | */ 25 | private String filterType; 26 | 27 | /** 28 | * 允许的最大价格 29 | */ 30 | private BigDecimal maxPrice; 31 | 32 | /** 33 | * 允许的最小价格 34 | */ 35 | private BigDecimal minPrice; 36 | 37 | /** 38 | * 价格步长 39 | */ 40 | private BigDecimal tickSize; 41 | 42 | /** 43 | * 价格 BigDecimal的 scale 44 | */ 45 | private Integer priceScale; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.controller; 2 | 3 | import io.swagger.annotations.Api; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import static com.aixi.lv.config.ApiKeyConfig.WEB_PASSWORD; 11 | 12 | /** 13 | * @author Js 14 | */ 15 | @RestController 16 | @RequestMapping("/test") 17 | @Api(tags = "测试服务") 18 | @Slf4j 19 | public class TestController { 20 | 21 | @GetMapping("/hello") 22 | public String testHello(String js) { 23 | 24 | if (StringUtils.isEmpty(js)) { 25 | return "js"; 26 | } 27 | 28 | if (!js.equals(WEB_PASSWORD)) { 29 | return "js"; 30 | } 31 | 32 | return "hello 艾希"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/BackTestData.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | 6 | import com.alibaba.fastjson.annotation.JSONField; 7 | 8 | import com.aixi.lv.model.constant.Symbol; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Builder; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | 14 | /** 15 | * @author Js 16 | * 17 | * 回测数据 18 | */ 19 | @Data 20 | @Builder 21 | @AllArgsConstructor 22 | @NoArgsConstructor 23 | public class BackTestData { 24 | 25 | // 交易对(币种) 26 | @JSONField(ordinal = 1) 27 | private Symbol symbol; 28 | 29 | // 开盘时间 30 | @JSONField(ordinal = 2) 31 | private LocalDateTime openingTime; 32 | 33 | // 收盘价(当前K线未结束的即为最新价) 34 | @JSONField(ordinal = 3) 35 | private BigDecimal closingPrice; 36 | 37 | // 成交量 38 | @JSONField(ordinal = 4) 39 | private BigDecimal tradingVolume; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | * 8 | * 现货订单状态 9 | */ 10 | public enum OrderStatus { 11 | 12 | /** 13 | * 订单被交易引擎接受 14 | */ 15 | NEW("NEW"), 16 | 17 | /** 18 | * 部分订单被成交 19 | */ 20 | PARTIALLY_FILLED("PARTIALLY_FILLED"), 21 | 22 | /** 23 | * 订单完全成交 24 | */ 25 | FILLED("FILLED"), 26 | 27 | /** 28 | * 用户撤销了订单 29 | */ 30 | CANCELED("CANCELED"), 31 | 32 | /** 33 | * 订单没有被交易引擎接受,也没被处理 34 | */ 35 | REJECTED("REJECTED"), 36 | 37 | /** 38 | * 订单被交易引擎取消, 比如 39 | * LIMIT FOK 订单没有成交 40 | * 市价单没有完全成交 41 | * 强平期间被取消的订单 42 | * 交易所维护期间被取消的订单 43 | */ 44 | EXPIRED("EXPIRED"), 45 | ; 46 | 47 | OrderStatus(String code) { 48 | this.code = code; 49 | } 50 | 51 | @Getter 52 | private String code; 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/ExecutorConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.util.concurrent.LinkedBlockingQueue; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import com.google.common.util.concurrent.ListeningExecutorService; 8 | import com.google.common.util.concurrent.MoreExecutors; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | /** 13 | * @author Js 14 | */ 15 | @Configuration 16 | public class ExecutorConfig { 17 | 18 | @Bean("listeningExecutorService") 19 | public ListeningExecutorService executorService() { 20 | 21 | return MoreExecutors.listeningDecorator( 22 | new ThreadPoolExecutor(4, 23 | 8, 24 | 30, 25 | TimeUnit.SECONDS, 26 | // 有界阻塞队列 27 | new LinkedBlockingQueue<>(20000))); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/Box.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | 6 | import com.aixi.lv.model.constant.Interval; 7 | import com.aixi.lv.model.constant.Symbol; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | /** 14 | * @author Js 15 | */ 16 | @Data 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class Box { 21 | 22 | /** 23 | * 箱顶 24 | */ 25 | private BigDecimal topPrice; 26 | 27 | /** 28 | * 箱底 29 | */ 30 | private BigDecimal bottomPrice; 31 | 32 | /** 33 | * 箱体开始时间 34 | */ 35 | private LocalDateTime startTime; 36 | 37 | /** 38 | * 箱体结束时间 39 | */ 40 | private LocalDateTime endTime; 41 | 42 | /** 43 | * 时间间隔 44 | */ 45 | private Interval interval; 46 | 47 | /** 48 | * 交易对 49 | */ 50 | private Symbol symbol; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/CurrencyType.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import java.util.Optional; 4 | 5 | import lombok.Getter; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | /** 9 | * @author Js 10 | * 11 | * 币种 12 | */ 13 | public enum CurrencyType { 14 | 15 | BTC("BTC"), 16 | 17 | ETH("ETH"), 18 | 19 | DOGE("DOGE"), 20 | 21 | SHIB("SHIB"), 22 | 23 | BNB("BNB"), 24 | 25 | USDT("USDT"), 26 | ; 27 | 28 | CurrencyType(String code) { 29 | this.code = code; 30 | } 31 | 32 | @Getter 33 | private String code; 34 | 35 | /** 36 | * 找枚举 37 | * 38 | * @param code 39 | * @return 40 | */ 41 | public static Optional getByCode(String code) { 42 | 43 | for (CurrencyType item : CurrencyType.values()) { 44 | if (StringUtils.equals(item.getCode(), code)) { 45 | return Optional.of(item); 46 | } 47 | } 48 | 49 | return Optional.empty(); 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/ProfitRateConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.math.BigDecimal; 4 | 5 | /** 6 | * @author Js 7 | */ 8 | public class ProfitRateConfig { 9 | 10 | /** 11 | * 一阶段止盈【数量】比例 12 | */ 13 | public static final BigDecimal FIRST_QTY_RATE = new BigDecimal("0.6"); 14 | 15 | /** 16 | * 二阶段止盈【数量】比例 17 | */ 18 | public static final BigDecimal SECOND_QTY_RATE = new BigDecimal("0.3"); 19 | 20 | /** 21 | * 三阶段止盈【数量】比例 22 | */ 23 | public static final BigDecimal THIRD_QTY_RATE = new BigDecimal("0.1"); 24 | 25 | /** 26 | * * 27 | * * 28 | * * 29 | * * 30 | * * 31 | * ************************** 分界线 ********************* 32 | * * 33 | * * 34 | * * 35 | * * 36 | * * 37 | */ 38 | 39 | 40 | /** 41 | * 二阶段止盈【价格】比例 42 | */ 43 | public static final BigDecimal SECOND_PRICE_RATE = new BigDecimal("1.01"); 44 | 45 | /** 46 | * 三阶段止盈【价格】比例 47 | */ 48 | public static final BigDecimal THIRD_PRICE_RATE = new BigDecimal("1.03"); 49 | } 50 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/合约回测Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.analysis.ContractBackTestAnalysis; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.ContractAccount; 12 | import com.aixi.lv.model.domain.MacdAccount; 13 | import com.google.common.collect.Lists; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.junit.Test; 16 | 17 | import static com.aixi.lv.analysis.ContractBackTestAnalysis.THREAD_LOCAL_CONTRACT_ACCOUNT; 18 | import static com.aixi.lv.config.MacdTradeConfig.THREAD_LOCAL_ACCOUNT; 19 | 20 | /** 21 | * @author Js 22 | */ 23 | @Slf4j 24 | public class 合约回测Test extends BaseTest { 25 | 26 | @Resource 27 | ContractBackTestAnalysis contractBackTestAnalysis; 28 | 29 | @Test 30 | public void 简单回测() { 31 | 32 | LocalDateTime startTime = LocalDateTime.of(2022, 3, 19, 0, 0); 33 | LocalDateTime endTime = startTime.plusDays(45); 34 | 35 | 36 | contractBackTestAnalysis.doAnalysis(startTime, endTime); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/预警分析Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.analysis.BackTestAnalysis; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.MacdAccount; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.lang3.math.NumberUtils; 14 | import org.junit.Test; 15 | 16 | import static com.aixi.lv.config.BackTestConfig.INIT_BACK_TEST_AMOUNT; 17 | import static com.aixi.lv.config.MacdTradeConfig.THREAD_LOCAL_ACCOUNT; 18 | 19 | /** 20 | * @author Js 21 | */ 22 | @Slf4j 23 | public class 预警分析Test extends BaseTest { 24 | 25 | @Resource 26 | BackTestAnalysis backTestAnalysis; 27 | 28 | @Test 29 | public void 底部回升预警() { 30 | 31 | MacdAccount account = new MacdAccount(); 32 | THREAD_LOCAL_ACCOUNT.set(account); 33 | 34 | LocalDateTime endTime = LocalDateTime.of(2022, 3, 19, 23, 0); 35 | 36 | LocalDateTime startTime = endTime.minusDays(19); 37 | 38 | System.out.println(startTime); 39 | 40 | backTestAnalysis.doWarning(startTime, endTime); 41 | 42 | } 43 | 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/TradePair.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | 4 | import java.time.LocalDateTime; 5 | 6 | import com.aixi.lv.model.constant.TradePairStatus; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | /** 13 | * @author Js 14 | * 15 | * 交易对 16 | */ 17 | @Data 18 | @Builder 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class TradePair { 22 | 23 | /** 24 | * 交易对状态 25 | */ 26 | private TradePairStatus status; 27 | 28 | /** 29 | * 买入订单 30 | */ 31 | private OrderLife buyOrder; 32 | 33 | /** 34 | * 止损卖单 35 | */ 36 | private OrderLife lossOrder; 37 | 38 | /** 39 | * 强制止盈单 40 | */ 41 | private OrderLife forceProfitOrder; 42 | 43 | /** 44 | * 一阶段止盈卖单 45 | */ 46 | private OrderLife firstProfitOrder; 47 | 48 | /** 49 | * 二阶段止盈卖单 50 | */ 51 | private OrderLife secondProfitOrder; 52 | 53 | /** 54 | * 三阶段止盈卖单 55 | */ 56 | private OrderLife thirdProfitOrder; 57 | 58 | /** 59 | * 再次购买次数 60 | */ 61 | private Integer reBuyTimes; 62 | 63 | /** 64 | * 止损成交的时间 65 | */ 66 | private LocalDateTime lossTime; 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/TimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneOffset; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | 8 | import static com.aixi.lv.config.BackTestConfig.OPEN; 9 | import static com.aixi.lv.config.MacdTradeConfig.THREAD_LOCAL_ACCOUNT; 10 | 11 | /** 12 | * @author Js 13 | 14 | */ 15 | public class TimeUtil { 16 | 17 | public static LocalDateTime now() { 18 | 19 | LocalDateTime local; 20 | if (OPEN) { 21 | local = THREAD_LOCAL_ACCOUNT.get().getCurBackTestComputeTime(); 22 | } else { 23 | local = LocalDateTime.now(); 24 | } 25 | 26 | return local; 27 | } 28 | 29 | public static String getCurrentTime() { 30 | 31 | LocalDateTime local; 32 | if (OPEN) { 33 | local = THREAD_LOCAL_ACCOUNT.get().getCurBackTestComputeTime(); 34 | } else { 35 | local = LocalDateTime.now(); 36 | } 37 | 38 | return local.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); 39 | } 40 | 41 | public static String getTime(LocalDateTime localDateTime) { 42 | 43 | return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); 44 | } 45 | 46 | public static Long localToLong(LocalDateTime localDateTime) { 47 | 48 | return localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/schedule/BollTask.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.schedule; 2 | 3 | 4 | import javax.annotation.Resource; 5 | 6 | 7 | import com.aixi.lv.service.MailService; 8 | import com.aixi.lv.strategy.indicator.BollSellStrategy; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.scheduling.annotation.Async; 12 | import org.springframework.scheduling.annotation.EnableAsync; 13 | import org.springframework.scheduling.annotation.EnableScheduling; 14 | import org.springframework.scheduling.annotation.Scheduled; 15 | import org.springframework.stereotype.Component; 16 | 17 | 18 | /** 19 | * @author Js 20 | */ 21 | //@Component 22 | //@EnableScheduling // 1.开启定时任务 23 | //@EnableAsync // 2.开启多线程 24 | //@Profile({"lv","hermes"}) 25 | @Slf4j 26 | public class BollTask { 27 | 28 | @Resource 29 | BollSellStrategy bollSellStrategy; 30 | 31 | @Resource 32 | MailService mailService; 33 | 34 | //@Async 35 | //@Scheduled(cron = "10 5/5 0/1 * * ? ") // 每小时 第5分钟10秒开始执行 ,每隔5分钟执行一次 36 | public void bollSellTask() { 37 | 38 | try { 39 | 40 | bollSellStrategy.detectSell(); 41 | 42 | } catch (Exception e) { 43 | log.error("MacdTask异常 " + e.getMessage(), e); 44 | mailService.sendEmail("MacdTask异常", e.getMessage()); 45 | // 终止程序运行 46 | System.exit(0); 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/TradePairStatus.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * @author Js 7 | */ 8 | public enum TradePairStatus { 9 | 10 | /** 11 | * 新下的买单 12 | */ 13 | NEW("NEW"), 14 | 15 | /** 16 | * 买单已完成 17 | */ 18 | ALREADY("ALREADY"), 19 | 20 | /** 21 | * 买单已取消 22 | */ 23 | CANCEL("CANCEL"), 24 | 25 | /** 26 | * 止损单生效中 27 | */ 28 | LOSS("LOSS"), 29 | 30 | /** 31 | * 止损单已成交 32 | */ 33 | LOSS_DONE("LOSS_DONE"), 34 | 35 | /** 36 | * 一阶段止盈单生效中 37 | */ 38 | FIRST_PROFIT("FIRST_PROFIT"), 39 | 40 | /** 41 | * 一阶段止盈单已成交 42 | */ 43 | FIRST_DONE("FIRST_DONE"), 44 | 45 | /** 46 | * 二阶段止盈单生效中 47 | */ 48 | SECOND_PROFIT("SECOND_PROFIT"), 49 | 50 | /** 51 | * 二阶段止盈单已成交 52 | */ 53 | SECOND_DONE("SECOND_DONE"), 54 | 55 | /** 56 | * 三阶段止盈单生效中 57 | */ 58 | THIRD_PROFIT("THIRD_PROFIT"), 59 | 60 | /** 61 | * 三阶段止盈单已成交 62 | */ 63 | THIRD_DONE("THIRD_DONE"), 64 | 65 | /** 66 | * 强制止盈 67 | */ 68 | FORCE_PROFIT("FORCE_PROFIT"), 69 | 70 | 71 | /** 72 | * 强制止盈单被取消,这是异常case,得人工处理 73 | */ 74 | FORCE_PROFIT_CANCEL("FORCE_PROFIT_CANCEL"), 75 | 76 | /** 77 | * 已全部卖出 78 | */ 79 | FINISH("FINISH"), 80 | 81 | ; 82 | 83 | TradePairStatus(String code) { 84 | this.code = code; 85 | } 86 | 87 | @Getter 88 | private String code; 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/EncryptUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.Map; 5 | 6 | import javax.crypto.Mac; 7 | import javax.crypto.spec.SecretKeySpec; 8 | 9 | import com.alibaba.fastjson.JSON; 10 | import com.alibaba.fastjson.JSONObject; 11 | 12 | import com.aixi.lv.config.ApiKeyConfig; 13 | import lombok.extern.slf4j.Slf4j; 14 | 15 | /** 16 | * @author Js 17 | 18 | */ 19 | @Slf4j 20 | public class EncryptUtil { 21 | 22 | /** 23 | * @param data 要加密的数据,字符串 24 | * @return 25 | * @throws Exception 26 | */ 27 | public static String getSignature(String data) { 28 | 29 | // 密钥 30 | String secretKey = ApiKeyConfig.SECRET_KEY; 31 | 32 | try { 33 | Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); 34 | SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 35 | sha256_HMAC.init(secret_key); 36 | byte[] array = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8)); 37 | StringBuilder sb = new StringBuilder(); 38 | for (byte item : array) { 39 | sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); 40 | } 41 | 42 | return sb.toString().toUpperCase(); 43 | } catch (Exception e) { 44 | log.error(" hmacSHA256 | exception | data=" + JSON.toJSONString(data), e); 45 | throw new RuntimeException(e); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 13 | utf-8 14 | 15 | 16 | 17 | ./logs/output.log 18 | 19 | 23 | 24 | ./logs/output.%d{yyyy-MM-dd}.log 25 | 30 26 | 27 | 28 | 29 | 30 | 5MB 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/schedule/offline/ReBuyTask.java: -------------------------------------------------------------------------------- 1 | //package com.aixi.lv.schedule; 2 | // 3 | //import javax.annotation.Resource; 4 | // 5 | //import com.aixi.lv.model.constant.Symbol; 6 | //import com.aixi.lv.service.MailService; 7 | //import com.aixi.lv.strategy.buy.ReBuyStrategy; 8 | //import lombok.extern.slf4j.Slf4j; 9 | //import org.springframework.scheduling.annotation.Async; 10 | //import org.springframework.scheduling.annotation.EnableAsync; 11 | //import org.springframework.scheduling.annotation.EnableScheduling; 12 | //import org.springframework.scheduling.annotation.Scheduled; 13 | //import org.springframework.stereotype.Component; 14 | // 15 | ///** 16 | // * @author Js 17 | // * 18 | // * 复购任务 19 | // */ 20 | ////@Component 21 | ////@EnableScheduling // 1.开启定时任务 22 | ////@EnableAsync // 2.开启多线程 23 | //@Slf4j 24 | //public class ReBuyTask { 25 | // 26 | // @Resource 27 | // ReBuyStrategy reBuyStrategy; 28 | // 29 | // @Resource 30 | // MailService mailService; 31 | // 32 | // /** 33 | // * 轮询止盈策略 34 | // */ 35 | // @Async 36 | // @Scheduled(cron = "52 0/1 * * * ? ") // 每分钟第52秒 37 | // public void firstProfit() { 38 | // 39 | // try { 40 | // 41 | // reBuyStrategy.reBuy(Symbol.ETHUSDT); 42 | // reBuyStrategy.reBuy(Symbol.DOGEUSDT); 43 | // reBuyStrategy.reBuy(Symbol.BTCUSDT); 44 | // 45 | // } catch (Exception e) { 46 | // log.error("ReBuyTask异常 " + e.getMessage(), e); 47 | // mailService.sendEmail("ReBuyTask异常", e.getMessage()); 48 | // // 终止程序运行 49 | // System.exit(0); 50 | // } 51 | // 52 | // } 53 | //} 54 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/schedule/offline/StopLossTask.java: -------------------------------------------------------------------------------- 1 | //package com.aixi.lv.schedule; 2 | // 3 | //import javax.annotation.Resource; 4 | // 5 | //import com.aixi.lv.model.constant.Symbol; 6 | //import com.aixi.lv.service.MailService; 7 | //import com.aixi.lv.strategy.loss.StopLossStrategy; 8 | //import lombok.extern.slf4j.Slf4j; 9 | //import org.springframework.scheduling.annotation.Async; 10 | //import org.springframework.scheduling.annotation.EnableAsync; 11 | //import org.springframework.scheduling.annotation.EnableScheduling; 12 | //import org.springframework.scheduling.annotation.Scheduled; 13 | //import org.springframework.stereotype.Component; 14 | // 15 | ///** 16 | // * @author Js 17 | // * @date 2022/1/3 12:49 下午 18 | // */ 19 | ////@Component 20 | ////@EnableScheduling // 1.开启定时任务 21 | ////@EnableAsync // 2.开启多线程 22 | //@Slf4j 23 | //public class StopLossTask { 24 | // 25 | // @Resource 26 | // StopLossStrategy stopLossStrategy; 27 | // 28 | // @Resource 29 | // MailService mailService; 30 | // 31 | // /** 32 | // * 轮询止损策略 33 | // */ 34 | // @Async 35 | // @Scheduled(cron = "55 0/1 * * * ? ") // 每分钟第55秒 36 | // public void stopLoss() { 37 | // 38 | // try { 39 | // 40 | // stopLossStrategy.stopLoss(Symbol.ETHUSDT); 41 | // stopLossStrategy.stopLoss(Symbol.DOGEUSDT); 42 | // stopLossStrategy.stopLoss(Symbol.BTCUSDT); 43 | // 44 | // } catch (Exception e) { 45 | // log.error("StopLossTask异常 " + e.getMessage(), e); 46 | // mailService.sendEmail("StopLossTask异常", e.getMessage()); 47 | // // 终止程序运行 48 | // System.exit(0); 49 | // } 50 | // 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/ContractAccount.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | 6 | import com.aixi.lv.model.constant.ContractSide; 7 | import com.aixi.lv.model.constant.Symbol; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | /** 14 | * @author Js 15 | 16 | */ 17 | @Data 18 | @Builder 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class ContractAccount { 22 | 23 | /** 24 | * 账户名称 25 | */ 26 | private String name; 27 | 28 | /** 29 | * 账户涵盖的symbol 30 | */ 31 | private Symbol symbol; 32 | 33 | /** 34 | * 当前持有 USDT 金额 35 | */ 36 | private BigDecimal holdAmount; 37 | 38 | /** 39 | * 当前持有币数量 40 | */ 41 | private BigDecimal holdQty; 42 | 43 | /** 44 | * 是否持有单子 45 | */ 46 | private Boolean holdFlag; 47 | 48 | 49 | /** 50 | * 单子方向 51 | */ 52 | private ContractSide contractSide; 53 | 54 | /** 55 | * 下单价格 56 | */ 57 | private BigDecimal buyPrice; 58 | 59 | /** 60 | * 止盈价格 61 | */ 62 | private BigDecimal profitPrice; 63 | 64 | /** 65 | * 止损价格 66 | */ 67 | private BigDecimal lossPrice; 68 | 69 | /** 70 | * 回测时的当前自然时间 71 | */ 72 | private LocalDateTime curBackTestComputeTime; 73 | 74 | /** 75 | * 回测时盈利累计总和 76 | */ 77 | private BigDecimal backTestTotalProfit; 78 | 79 | /** 80 | * 回测时亏损累计总和 81 | */ 82 | private BigDecimal backTestTotalLoss; 83 | 84 | /** 85 | * 回测时盈利次数 86 | */ 87 | private int backTestProfitTimes; 88 | 89 | /** 90 | * 回测时亏损次数 91 | */ 92 | private int backTestLossTimes; 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/PriceFaceService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.model.constant.Interval; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.KLine; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Component; 14 | 15 | import static com.aixi.lv.config.BackTestConfig.OPEN; 16 | 17 | /** 18 | * @author Js 19 | * 20 | * 兼容回测的价格服务 21 | */ 22 | @Component 23 | @Slf4j 24 | public class PriceFaceService extends PriceService { 25 | 26 | @Resource 27 | private BackTestPriceService backTestPriceService; 28 | 29 | @Override 30 | public List queryKLine(Symbol symbol, Interval interval, Integer limit) { 31 | 32 | if (OPEN) { 33 | return backTestPriceService.queryKLine(symbol, interval, limit); 34 | } else { 35 | return super.queryKLine(symbol, interval, limit); 36 | } 37 | 38 | } 39 | 40 | @Override 41 | public List queryKLineByTime(Symbol symbol, Interval interval, Integer limit, LocalDateTime startTime, 42 | LocalDateTime endTime) { 43 | 44 | if (OPEN) { 45 | return backTestPriceService.queryKLineByTime(symbol, interval, limit, startTime, endTime); 46 | } else { 47 | return super.queryKLineByTime(symbol, interval, limit, startTime, endTime); 48 | } 49 | 50 | } 51 | 52 | @Override 53 | public BigDecimal queryNewPrice(Symbol symbol) { 54 | 55 | if (OPEN) { 56 | return backTestPriceService.queryNewPrice(symbol); 57 | } else { 58 | return super.queryNewPrice(symbol); 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.alibaba.fastjson.JSON; 9 | import com.alibaba.fastjson.JSONObject; 10 | 11 | import com.aixi.lv.model.constant.CurrencyType; 12 | import com.aixi.lv.model.domain.Account; 13 | import com.aixi.lv.model.domain.Asset; 14 | import com.aixi.lv.util.ApiUtil; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.stereotype.Component; 17 | 18 | /** 19 | * @author Js 20 | */ 21 | @Component 22 | @Slf4j 23 | public class AccountService { 24 | 25 | @Resource 26 | EncryptHttpService encryptHttpService; 27 | 28 | /** 29 | * 获取账户信息 30 | * 31 | * @return 32 | */ 33 | public Account queryAccountInfo() { 34 | 35 | try { 36 | 37 | String url = ApiUtil.url("/api/v3/account"); 38 | 39 | JSONObject params = new JSONObject(); 40 | long timeStamp = System.currentTimeMillis(); 41 | params.put("timestamp", timeStamp); 42 | 43 | JSONObject object = encryptHttpService.getObject(url, params); 44 | 45 | Account account = Account.parseObject(object); 46 | 47 | return account; 48 | 49 | } catch (Exception e) { 50 | log.error(String.format(" AccountService | queryAccountInfo_fail "), e); 51 | throw e; 52 | } 53 | } 54 | 55 | /** 56 | * 查询BNB余额 57 | * 58 | * @return 59 | */ 60 | public BigDecimal queryBNBFreeQty() { 61 | 62 | try { 63 | 64 | Account account = this.queryAccountInfo(); 65 | 66 | List assetList = account.getAssetList(); 67 | 68 | for (Asset asset : assetList) { 69 | if (CurrencyType.BNB == asset.getCurrencyType()) { 70 | return asset.getFreeQty(); 71 | } 72 | } 73 | 74 | return BigDecimal.ZERO; 75 | 76 | } catch (Exception e) { 77 | log.error(String.format(" AccountService | queryBNBFreeQty_fail "), e); 78 | throw e; 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import com.alibaba.fastjson.JSONArray; 7 | import com.alibaba.fastjson.JSONObject; 8 | 9 | import com.aixi.lv.model.constant.CurrencyType; 10 | import com.google.common.collect.Lists; 11 | import lombok.AllArgsConstructor; 12 | import lombok.Builder; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | /** 17 | * @author Js 18 | * 19 | * 账户 20 | */ 21 | @Data 22 | @Builder 23 | @AllArgsConstructor 24 | @NoArgsConstructor 25 | public class Account { 26 | 27 | /** 28 | * 货币资产 29 | */ 30 | private List assetList; 31 | 32 | /** 33 | * SPOT 现货账户 34 | * MARGIN 杠杆账户 35 | */ 36 | private String accountType; 37 | 38 | /** 39 | * 是否可以交易 40 | */ 41 | private Boolean canTrade; 42 | 43 | public static Account parseObject(JSONObject jo) { 44 | 45 | if (jo == null) { 46 | return null; 47 | } 48 | 49 | Account account = new Account(); 50 | 51 | Boolean canTrade = jo.getBoolean("canTrade"); 52 | account.setCanTrade(canTrade); 53 | 54 | String accountType = jo.getString("accountType"); 55 | account.setAccountType(accountType); 56 | 57 | JSONArray jsonArray = jo.getJSONArray("balances"); 58 | 59 | List balances = jsonArray.toJavaList(JSONObject.class); 60 | 61 | List assetList = Lists.newArrayList(); 62 | 63 | for (JSONObject balance : balances) { 64 | String asset = balance.getString("asset"); 65 | Optional optional = CurrencyType.getByCode(asset); 66 | if (optional.isPresent()) { 67 | Asset currencyAsset = new Asset(); 68 | currencyAsset.setCurrencyType(optional.get()); 69 | currencyAsset.setFreeQty(balance.getBigDecimal("free")); 70 | currencyAsset.setLockedQty(balance.getBigDecimal("locked")); 71 | assetList.add(currencyAsset); 72 | } 73 | } 74 | 75 | account.setAssetList(assetList); 76 | 77 | return account; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/EMA.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @author Js 8 | */ 9 | public class EMA implements Indicator { 10 | 11 | private double currentEMA; 12 | private final int period; 13 | private final double multiplier; 14 | private final List EMAhistory; 15 | private final boolean historyNeeded; 16 | private String fileName; 17 | 18 | public EMA(List closingPrices, int period, boolean historyNeeded) { 19 | currentEMA = 0; 20 | this.period = period; 21 | this.historyNeeded = historyNeeded; 22 | this.multiplier = 2.0 / (double)(period + 1); 23 | this.EMAhistory = new ArrayList<>(); 24 | init(closingPrices); 25 | } 26 | 27 | @Override 28 | public double get() { 29 | return currentEMA; 30 | } 31 | 32 | @Override 33 | public double getTemp(double newPrice) { 34 | return (newPrice - currentEMA) * multiplier + currentEMA; 35 | } 36 | 37 | @Override 38 | public void init(List closingPrices) { 39 | if (period > closingPrices.size()) {return;} 40 | 41 | //Initial SMA 42 | for (int i = 0; i < period; i++) { 43 | currentEMA += closingPrices.get(i); 44 | } 45 | 46 | currentEMA = currentEMA / (double)period; 47 | if (historyNeeded) {EMAhistory.add(currentEMA);} 48 | //Dont use latest unclosed candle; 49 | for (int i = period; i < closingPrices.size() - 1; i++) { 50 | update(closingPrices.get(i)); 51 | } 52 | } 53 | 54 | @Override 55 | public void update(double newPrice) { 56 | // EMA = (Close - EMA(previousBar)) * multiplier + EMA(previousBar) 57 | currentEMA = (newPrice - currentEMA) * multiplier + currentEMA; 58 | 59 | if (historyNeeded) {EMAhistory.add(currentEMA);} 60 | } 61 | 62 | @Override 63 | public int check(double newPrice) { 64 | return 0; 65 | } 66 | 67 | @Override 68 | public String getExplanation() { 69 | return null; 70 | } 71 | 72 | public List getEMAhistory() { 73 | return EMAhistory; 74 | } 75 | 76 | public int getPeriod() { 77 | return period; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/MacdAccount.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | 7 | import com.aixi.lv.model.constant.Symbol; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Data; 11 | import lombok.NoArgsConstructor; 12 | 13 | /** 14 | * @author Js 15 | */ 16 | @Data 17 | @Builder 18 | @AllArgsConstructor 19 | @NoArgsConstructor 20 | public class MacdAccount { 21 | 22 | /** 23 | * 账户名称 24 | */ 25 | private String name; 26 | 27 | /** 28 | * 账户涵盖的symbol 29 | */ 30 | private List symbolList; 31 | 32 | /** 33 | * 当前持有标记 34 | */ 35 | private Symbol curHoldSymbol; 36 | 37 | /** 38 | * 当前持有 USDT 金额 39 | */ 40 | private BigDecimal curHoldAmount; 41 | 42 | /** 43 | * 当前持有币数量 44 | */ 45 | private BigDecimal curHoldQty; 46 | 47 | /** 48 | * 当前交易对 49 | */ 50 | private TradePair curPair; 51 | 52 | /** 53 | * 购买的价格 54 | */ 55 | private BigDecimal lastBuyPrice; 56 | 57 | /** 58 | * 归属账户,多币种组合时,方便统计 59 | */ 60 | private String belongAccount; 61 | 62 | /** 63 | * 上一次换仓时间 64 | */ 65 | private LocalDateTime lastSwitchTime; 66 | 67 | /** 68 | * 上一次卖出时间 69 | */ 70 | private LocalDateTime lastSellTime; 71 | 72 | /** 73 | * 上一次卖出的币种 74 | */ 75 | private Symbol lastSellSymbol; 76 | 77 | /** 78 | * 准备卖出标记 79 | */ 80 | private Boolean readySellFlag; 81 | 82 | /** 83 | * 准备卖出标记的时间 84 | */ 85 | private LocalDateTime readySellTime; 86 | 87 | /** 88 | * 回测时的当前自然时间 89 | */ 90 | private LocalDateTime curBackTestComputeTime; 91 | 92 | /** 93 | * 回测时的当前增长率 94 | */ 95 | private Double curBackTestRate; 96 | 97 | /** 98 | * 回测时,账户涵盖的币种的平均增长率 99 | */ 100 | private Double symbolNatureRate; 101 | 102 | /** 103 | * 回测时盈利累计总和 104 | */ 105 | private BigDecimal backTestTotalProfit; 106 | 107 | /** 108 | * 回测时亏损累计总和 109 | */ 110 | private BigDecimal backTestTotalLoss; 111 | 112 | /** 113 | * 回测时盈利次数 114 | */ 115 | private Integer backTestProfitTimes; 116 | 117 | /** 118 | * 回测时亏损次数 119 | */ 120 | private Integer backTestLossTimes; 121 | 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hermes 2 | 币安数字货币量化交易机器人 3 | 4 | # 简介 5 | 可根据实时行情自动在币安上交易现货的量化交易机器人,支持秒级行情解析、策略分析、交易动作,已经经过数十万USDT的真实金额交易的验证。 6 | 在牛市,是可以赚钱的。 7 | 但在熊市,该交易机器人也一定是会亏钱的!!! 8 | 9 | # 重要 10 | 开源只是提供机会给程序员们一个更广阔的视野,交易有风险,盈利无需打赏,亏损也不负责哈~~ 11 | 12 | # 功能 13 | 1. 毫秒级现货行情获取 14 | 2. 实时指标分析计算,目前支持MACD\RSI\BOLL三种 15 | 3. 提供多种交易策略,如基于MACD涨跌的交易策略,基于实时价格突变的交易策略等 16 | 4. 完备的交易订单管理体系,支持跟踪止盈止损、分段止盈止损、时间止盈止损等 17 | 5. 支持过去2年时间行情回测分析,可用于快速回测交易策略,获取策略盈亏结果 18 | 6. 交易动作及行情邮件提醒 19 | 7. 可用Swagger来查询所有账户和交易单信息 20 | 8. 完整的交易动作日志记录 21 | 22 | # 如何使用 23 | ## 配置 24 | 1. 在 ApiKeyConfig 里配置自己的币安交易API KEY 25 | 2. 在 ExchangeInfoConfig 里配置交易账户的金额和交易的币种 26 | 3. 在 MailService 里配置邮箱信息用来接受交易结果通知 27 | 4. 在 MacdTask 里根据需要选择交易策略 28 | 29 | ## 启动 30 | 1. 服务器运行:用mvn命令打包,并上传jar包到服务器,在服务器启动jar包程序,交易机器人就开始运行啦。 31 | 2. 本地运行:本地LvApplication启动 32 | 33 | 34 | ---- 35 | 36 | # Hermes 37 | Binance Quantitative Trading Robot 38 | 39 | # Introduction 40 | A quantitative trading robot that can automatically trade spot on Binance according to real-time market conditions. It supports second-level market analysis, strategy analysis, and trading actions. It has been verified by hundreds of thousands of USDT transactions. 41 | In a bull market, you can make money, but in a bear market, the trading robot will definitely lose money. 42 | 43 | Open source only provides opportunities for ordinary programmers to have a broader vision. Transactions are risky, no rewards are required for profits, and no losses are responsible. 44 | 45 | # Features 46 | 1. Millisecond-level spot market acquisition 47 | 2. Real-time indicator analysis and calculation, currently supports three types of MACD\RSI\BOLL 48 | 3. Provide a variety of trading strategies, such as trading strategies based on MACD fluctuations, trading strategies based on real-time price changes, etc. 49 | 4. Complete transaction order management system, support tracking stop profit and stop loss, segment stop profit and stop loss, time stop profit and stop loss, etc. 50 | 5. Supports market backtest analysis in the past 2 years, which can be used to quickly backtest trading strategies and obtain strategy profit and loss results 51 | 6. Transaction action and market email reminder 52 | 7. Swagger can be used to query all account and transaction information 53 | 8. Complete transaction action logging 54 | 55 | # How 56 | 1. Configure your own Binance Trading API KEY in ApiKeyConfig 57 | 2. Configure the amount of the trading account and the currency of the transaction in ExchangeInfoConfig 58 | 3. Configure mailbox information in MailService to receive transaction result notifications 59 | 4. Select the trading strategy as needed in MacdTask 60 | 5. Use the mvn command to package and upload the jar package to the server. Or start the local LvApplication 61 | 6. Start the jar package program on the server, and the trading robot will start running 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.controller; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.List; 7 | 8 | import javax.annotation.Resource; 9 | 10 | import com.aixi.lv.manage.OrderLifeManage; 11 | import com.aixi.lv.model.constant.Symbol; 12 | import com.aixi.lv.model.domain.OrderLife; 13 | import com.aixi.lv.model.domain.Result; 14 | import com.aixi.lv.model.domain.TradePair; 15 | import com.aixi.lv.service.OrderService; 16 | import com.google.common.base.Preconditions; 17 | import io.swagger.annotations.Api; 18 | import io.swagger.annotations.ApiOperation; 19 | import lombok.extern.slf4j.Slf4j; 20 | import org.apache.commons.lang3.StringUtils; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | import static com.aixi.lv.config.ApiKeyConfig.WEB_PASSWORD; 26 | 27 | /** 28 | * @author Js 29 | */ 30 | @RestController 31 | @RequestMapping("/order") 32 | @Api(tags = "订单服务") 33 | @Slf4j 34 | public class OrderController { 35 | 36 | @Resource 37 | OrderLifeManage orderLifeManage; 38 | 39 | @Resource 40 | OrderService orderService; 41 | 42 | /** 43 | * @return 44 | */ 45 | @GetMapping("/info") 46 | @ApiOperation("获取当前所有交易对信息") 47 | public Result allPairInfo(String js) { 48 | 49 | if (StringUtils.isEmpty(js)) { 50 | return Result.success("js"); 51 | } 52 | 53 | if (!js.equals(WEB_PASSWORD)) { 54 | return Result.success("js"); 55 | } 56 | 57 | List allPair = orderLifeManage.getAllPair(); 58 | 59 | return Result.success(allPair); 60 | } 61 | 62 | /** 63 | * @param symbolCode 64 | * @param startTime 65 | * @param endTime 66 | * @return 67 | */ 68 | @GetMapping("/info/time") 69 | @ApiOperation("根据时间查询订单信息") 70 | public Result queryOrderByTime(String symbolCode, String startTime, String endTime, String js) { 71 | 72 | if (StringUtils.isEmpty(js)) { 73 | return Result.success("js"); 74 | } 75 | 76 | if (!js.equals(WEB_PASSWORD)) { 77 | return Result.success("js"); 78 | } 79 | 80 | Preconditions.checkArgument(StringUtils.isNotEmpty(symbolCode)); 81 | Preconditions.checkArgument(StringUtils.isNotEmpty(startTime)); 82 | Preconditions.checkArgument(StringUtils.isNotEmpty(endTime)); 83 | 84 | DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 85 | 86 | LocalDateTime start = LocalDateTime.parse(startTime, dtf); 87 | LocalDateTime end = LocalDateTime.parse(endTime, dtf); 88 | 89 | List orderLifeList = orderService.queryAllOrder(Symbol.getByCode(symbolCode), start, end); 90 | 91 | return Result.success(orderLifeList); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/indicator/MacdRateSellStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.indicator; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import javax.annotation.Resource; 6 | 7 | import com.aixi.lv.model.constant.Interval; 8 | import com.aixi.lv.model.constant.Symbol; 9 | import com.aixi.lv.model.domain.MacdAccount; 10 | import com.aixi.lv.model.indicator.MACD; 11 | import com.aixi.lv.service.BackTestCommonService; 12 | import com.aixi.lv.service.IndicatorService; 13 | import com.aixi.lv.util.NumUtil; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.springframework.stereotype.Component; 17 | 18 | /** 19 | * @author Js 20 | */ 21 | @Component 22 | @Slf4j 23 | public class MacdRateSellStrategy { 24 | 25 | @Resource 26 | MacdBuySellStrategy macdStrategy; 27 | 28 | @Resource 29 | IndicatorService indicatorService; 30 | 31 | @Resource 32 | BackTestCommonService backTestCommonService; 33 | 34 | /** 35 | * 卖出探测 36 | * 37 | * @param account 38 | */ 39 | public void sellDetect(MacdAccount account) { 40 | 41 | if (macdStrategy.isEmptyAccount(account)) { 42 | return; 43 | } 44 | 45 | if (account.getReadySellFlag()) { 46 | return; 47 | } 48 | 49 | Symbol curHoldSymbol = account.getCurHoldSymbol(); 50 | String name = account.getName(); 51 | 52 | if (curHoldSymbol == null) { 53 | return; 54 | } 55 | 56 | // 当前MACD是否处于高位下降趋势 57 | if (isCurLowerThanLast(curHoldSymbol, name)) { 58 | macdStrategy.sellAction(curHoldSymbol, account, "MACD高位下降卖出", new BigDecimal("0.1")); 59 | } 60 | 61 | } 62 | 63 | /** 64 | * 当前MACD是否处于高位下降趋势 65 | * 66 | * @param symbol 67 | * @param name 68 | * @return 69 | */ 70 | private Boolean isCurLowerThanLast(Symbol symbol, String name) { 71 | 72 | Double cur = macdStrategy.getCurHourMacd(symbol); 73 | Double last1 = macdStrategy.getLast1HourMacd(symbol); 74 | Double last2 = macdStrategy.getLast2HourMacd(symbol); 75 | Double last3 = macdStrategy.getLast3HourMacd(symbol); 76 | Double last4 = macdStrategy.getLast4HourMacd(symbol); 77 | 78 | double changeRate = (last1 - last2) / Math.abs(last2); 79 | 80 | if (last4 > 0 && last4 < last3 && last3 < last2 && last2 < last1) { 81 | 82 | log.info(" MACD 计算 | {} | {} | 当前MACD处于高位下降趋势 | last1 = {} | last2 = {} | last3 = {} | 回测自然时间 = {}", 83 | StringUtils.rightPad(name, 10), 84 | StringUtils.rightPad(symbol.getCode(), 10), 85 | NumUtil.showDouble(Math.abs(last1)), 86 | NumUtil.showDouble(Math.abs(last2)), 87 | NumUtil.showDouble(Math.abs(last3)), 88 | backTestCommonService.backTestNatureTime() 89 | ); 90 | 91 | return true; 92 | } else { 93 | return false; 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/analysis/SymbolAlternativeAnalysis.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.analysis; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.Comparator; 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | import java.util.stream.Collectors; 10 | 11 | import javax.annotation.Resource; 12 | 13 | import com.alibaba.fastjson.JSON; 14 | 15 | import com.aixi.lv.model.constant.Symbol; 16 | import com.aixi.lv.util.CombineUtil; 17 | import com.aixi.lv.util.NumUtil; 18 | import com.google.common.collect.Lists; 19 | import lombok.extern.slf4j.Slf4j; 20 | import org.springframework.stereotype.Component; 21 | 22 | import static com.aixi.lv.analysis.SymbolChoiceAnalysis.CHOICE_RATE_MAP; 23 | 24 | /** 25 | * @author Js 26 | 27 | * 28 | * 可供选择的币种分析 29 | */ 30 | @Component 31 | @Slf4j 32 | public class SymbolAlternativeAnalysis { 33 | 34 | @Resource 35 | SymbolChoiceAnalysis symbolChoiceAnalysis; 36 | 37 | /** 38 | * 到底选哪7个币种 39 | */ 40 | public void alternativeAnalysis() { 41 | 42 | // 13选7 ,有1716种组合 43 | // 12选7 ,有792种组合 44 | // 11选7 ,有330种组合 45 | List list = Lists.newArrayList(); 46 | list.add(Symbol.BTCUSDT); 47 | list.add(Symbol.ETHUSDT); 48 | list.add(Symbol.BNBUSDT); 49 | list.add(Symbol.XRPUSDT); 50 | list.add(Symbol.LUNAUSDT); 51 | list.add(Symbol.ADAUSDT); 52 | list.add(Symbol.SOLUSDT); 53 | list.add(Symbol.AVAXUSDT); 54 | list.add(Symbol.DOTUSDT); 55 | list.add(Symbol.SHIBUSDT); 56 | list.add(Symbol.MATICUSDT); 57 | list.add(Symbol.NEARUSDT); 58 | 59 | List> lists = CombineUtil.assignSymbolCombine(list, 7); 60 | 61 | for (int i = 0; i < lists.size(); i++) { 62 | List choiceSymbolList = lists.get(i); 63 | this.标准_精选账户回测_短期初测Action(choiceSymbolList); 64 | log.warn(" 可供选择的币种分析_当前已完成 = " + i); 65 | } 66 | 67 | Map sortedMap = new LinkedHashMap<>(); 68 | 69 | // Map 按Value 排序 70 | CHOICE_RATE_MAP.entrySet() 71 | .stream() 72 | .sorted(Comparator.comparing(Entry::getValue)) // 默认升序排列,小的放前面 73 | .collect(Collectors.toList()) 74 | .forEach(element -> sortedMap.put(element.getKey(), element.getValue())); 75 | 76 | // 信息打印 77 | for (Entry entry : sortedMap.entrySet()) { 78 | log.warn(" 分析币种增长率 = {} | 币种组合 = {} ", NumUtil.percent(entry.getValue() / 2), entry.getKey()); 79 | } 80 | 81 | } 82 | 83 | private void 标准_精选账户回测_短期初测Action(List choiceSymbolList) { 84 | 85 | // 组合账户币种数量 86 | Integer combineSize = 5; 87 | 88 | // 回测截止日期 89 | LocalDateTime endTime = LocalDateTime.of(2022, 3, 16, 23, 0); 90 | 91 | // 分析逻辑 92 | symbolChoiceAnalysis.币种组合分析(endTime, 30, null, choiceSymbolList, combineSize, null, null); 93 | 94 | symbolChoiceAnalysis.币种组合分析(endTime, 15, null, choiceSymbolList, combineSize, null, null); 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/箱体Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.alibaba.fastjson.JSON; 9 | 10 | import com.aixi.lv.model.constant.Interval; 11 | import com.aixi.lv.model.constant.Symbol; 12 | import com.aixi.lv.model.domain.Box; 13 | import com.aixi.lv.model.domain.KLine; 14 | import com.aixi.lv.model.domain.Result; 15 | import com.aixi.lv.service.AccountService; 16 | import com.aixi.lv.service.BoxService; 17 | import com.aixi.lv.service.EncryptHttpService; 18 | import com.aixi.lv.service.HttpService; 19 | import com.aixi.lv.service.MailService; 20 | import com.aixi.lv.service.OrderService; 21 | import com.aixi.lv.service.PriceService; 22 | import com.aixi.lv.strategy.buy.ReBuyStrategy; 23 | import org.junit.Test; 24 | import org.springframework.web.client.RestTemplate; 25 | 26 | /** 27 | * @author Js 28 | */ 29 | public class 箱体Test extends BaseTest { 30 | 31 | @Resource 32 | HttpService httpService; 33 | 34 | @Resource 35 | EncryptHttpService encryptHttpService; 36 | 37 | @Resource 38 | BoxService boxService; 39 | 40 | @Resource 41 | RestTemplate restTemplate; 42 | 43 | @Resource 44 | MailService mailService; 45 | 46 | @Resource 47 | PriceService priceService; 48 | 49 | @Resource 50 | OrderService orderService; 51 | 52 | @Resource 53 | AccountService accountService; 54 | 55 | @Resource 56 | ReBuyStrategy reBuyStrategy; 57 | 58 | @Test 59 | public void 找箱体() { 60 | 61 | LocalDateTime endTime = LocalDateTime.of(2022, 1, 5, 23, 30); 62 | LocalDateTime startTime = endTime.minusHours(24); 63 | 64 | // 查K线 65 | List kLines = priceService.queryKLineByTime(Symbol.ETHUSDT, Interval.MINUTE_15, 96, startTime, endTime); 66 | 67 | // 找箱体 68 | Result result = boxService.findBox(kLines); 69 | 70 | if (result.getSuccess()) { 71 | System.out.println(JSON.toJSONString(result.getData())); 72 | return; 73 | } 74 | 75 | } 76 | 77 | @Test 78 | public void 找箱体2() { 79 | 80 | 81 | 82 | // 查K线 83 | List kLines = priceService.queryKLine(Symbol.ETHUSDT, Interval.MINUTE_15, 96); 84 | 85 | // 找箱体 86 | Result result = boxService.findBox(kLines); 87 | 88 | if (result.getSuccess()) { 89 | System.out.println(JSON.toJSONString(result.getData())); 90 | return; 91 | } 92 | 93 | } 94 | 95 | @Test 96 | public void V型拐点测试(){ 97 | 98 | try{ 99 | 100 | //LocalDateTime endTime = LocalDateTime.of(2022, 1, 7, 23, 54); 101 | //LocalDateTime startTime = endTime.minusMinutes(30); 102 | // 103 | //Boolean aBoolean = reBuyStrategy.tradingVolumeBuyTest(Symbol.BTCUSDT, startTime, endTime); 104 | // 105 | //System.out.println(aBoolean); 106 | 107 | }catch (Exception e){ 108 | System.out.println(e.getMessage()); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/SMA.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | 7 | import com.aixi.lv.model.constant.Interval; 8 | import com.aixi.lv.model.constant.Symbol; 9 | import lombok.Getter; 10 | 11 | /** 12 | * @author Js 13 | * 14 | * 简单移动加权平均 15 | */ 16 | public class SMA implements Indicator { 17 | 18 | private double currentSum; 19 | private final int period; 20 | private final LinkedList prices; 21 | 22 | /** 23 | * 指标对应的 K线 开盘时间 (这根K线已经走完了) 24 | */ 25 | @Getter 26 | private LocalDateTime kLineOpenTime; 27 | 28 | @Getter 29 | private Symbol symbol; 30 | 31 | @Getter 32 | private Interval interval; 33 | 34 | public SMA(List closingPrices, int period) { 35 | this.period = period; 36 | prices = new LinkedList<>(); 37 | init(closingPrices); 38 | } 39 | 40 | public SMA(List closingPrices, LocalDateTime kLineOpenTime, Symbol symbol, Interval interval, int period) { 41 | this.period = period; 42 | this.kLineOpenTime = kLineOpenTime; 43 | this.symbol = symbol; 44 | this.interval = interval; 45 | prices = new LinkedList<>(); 46 | init(closingPrices); 47 | } 48 | 49 | @Override 50 | public double get() { 51 | return currentSum / (double)period; 52 | } 53 | 54 | @Override 55 | public double getTemp(double newPrice) { 56 | return ((currentSum - prices.get(0) + newPrice) / (double)period); 57 | } 58 | 59 | @Override 60 | public void init(List closingPrices) { 61 | if (period > closingPrices.size()) {return;} 62 | 63 | //Initial sum 64 | for (int i = closingPrices.size() - period - 1; i < closingPrices.size() - 1; i++) { 65 | prices.add(closingPrices.get(i)); 66 | currentSum += (closingPrices.get(i)); 67 | } 68 | } 69 | 70 | @Override 71 | public void update(double newPrice) { 72 | currentSum -= prices.get(0); 73 | prices.removeFirst(); 74 | prices.add(newPrice); 75 | currentSum += newPrice; 76 | } 77 | 78 | @Override 79 | public int check(double newPrice) { 80 | return 0; 81 | } 82 | 83 | @Override 84 | public String getExplanation() { 85 | return null; 86 | } 87 | 88 | public double standardDeviation() { 89 | double mean = currentSum / (double)period; 90 | double stdev = 0.0; 91 | for (double price : prices) { 92 | stdev += Math.pow(price - mean, 2); 93 | } 94 | return Math.sqrt(stdev / (double)period); 95 | } 96 | 97 | public double tempStandardDeviation(double newPrice) { 98 | 99 | double tempMean = (currentSum - prices.get(0) + newPrice) / (double)period; 100 | double tempStdev = 0.0; 101 | 102 | for (int i = 1; i < prices.size(); i++) { 103 | tempStdev += Math.pow(prices.get(i) - tempMean, 2); 104 | } 105 | 106 | tempStdev += Math.pow(newPrice - tempMean, 2); 107 | return Math.sqrt(tempStdev / (double)period); 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/CombineUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.ArrayList; 5 | import java.util.Deque; 6 | import java.util.List; 7 | 8 | import com.aixi.lv.model.constant.Symbol; 9 | import com.google.common.collect.Lists; 10 | 11 | /** 12 | * @author Js 13 | 14 | */ 15 | public class CombineUtil { 16 | 17 | /** 18 | * 获取所有的币种组合 19 | * 20 | * @param symbolList 21 | * @return 22 | */ 23 | public static List> allSymbolCombine(List symbolList) { 24 | 25 | List> combineList = Lists.newArrayList(); 26 | 27 | List> allIndexList = allIndexCombine(symbolList.size() - 1); 28 | 29 | for (List indexList : allIndexList) { 30 | 31 | List temp = Lists.newArrayList(); 32 | for (Integer index : indexList) { 33 | temp.add(symbolList.get(index)); 34 | } 35 | 36 | combineList.add(temp); 37 | } 38 | 39 | return combineList; 40 | 41 | } 42 | 43 | /** 44 | * 指定币种数量组合 45 | * 46 | * @param symbolList 47 | * @param num 48 | * @return 49 | */ 50 | public static List> assignSymbolCombine(List symbolList, Integer num) { 51 | 52 | List> combineList = Lists.newArrayList(); 53 | 54 | List> allIndexList = combine(symbolList.size() - 1, num); 55 | 56 | for (List indexList : allIndexList) { 57 | 58 | List temp = Lists.newArrayList(); 59 | for (Integer index : indexList) { 60 | temp.add(symbolList.get(index)); 61 | } 62 | 63 | combineList.add(temp); 64 | } 65 | 66 | return combineList; 67 | 68 | } 69 | 70 | private static List> allIndexCombine(Integer maxIndex) { 71 | 72 | List> result = new ArrayList<>(); 73 | 74 | Integer size = maxIndex + 1; 75 | 76 | while (size > 2) { 77 | List> combine = combine(maxIndex, size); 78 | 79 | result.addAll(combine); 80 | 81 | size--; 82 | } 83 | 84 | return result; 85 | 86 | } 87 | 88 | /** 89 | * 获取所有组合 90 | * 91 | * @param maxIndex 数组最大的脚标 92 | * @param k 93 | * @return 94 | */ 95 | private static List> combine(int maxIndex, int k) { 96 | 97 | List> res = new ArrayList<>(); 98 | 99 | Deque path = new ArrayDeque<>(); 100 | dfs(maxIndex, k, 0, path, res); 101 | return res; 102 | } 103 | 104 | private static void dfs(int n, int k, int begin, Deque path, List> res) { 105 | // 递归终止条件是:path 的长度等于 k 106 | if (path.size() == k) { 107 | res.add(new ArrayList<>(path)); 108 | return; 109 | } 110 | 111 | // 遍历可能的搜索起点 112 | for (int i = begin; i <= n; i++) { 113 | // 向路径变量里添加一个数 114 | path.addLast(i); 115 | // 下一轮搜索,设置的搜索起点要加 1,因为组合数理不允许出现重复的元素 116 | dfs(n, k, i + 1, path, res); 117 | // 重点理解这里:深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作 118 | path.removeLast(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/EasyTest.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import com.alibaba.fastjson.JSON; 10 | 11 | import com.aixi.lv.model.constant.Symbol; 12 | import com.aixi.lv.model.domain.MacdAccount; 13 | import com.aixi.lv.util.CombineUtil; 14 | import com.google.common.collect.Lists; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.apache.commons.lang3.tuple.MutablePair; 17 | import org.junit.Test; 18 | 19 | import static com.aixi.lv.config.BackTestConfig.BACK_TEST_ACCOUNT_NAME; 20 | import static com.aixi.lv.strategy.indicator.MacdBuySellStrategy.MACD_ACCOUNT_MAP; 21 | 22 | /** 23 | * @author Js 24 | */ 25 | public class EasyTest { 26 | 27 | @Test 28 | public void 测试1() { 29 | 30 | String str = BACK_TEST_ACCOUNT_NAME + Symbol.BTCUSDT.getCode() + " " + Symbol.ETHUSDT + " "; 31 | 32 | System.out.println(str); 33 | 34 | String substring = str.substring(BACK_TEST_ACCOUNT_NAME.length()); 35 | 36 | System.out.println(substring); 37 | 38 | String[] s = StringUtils.split(substring, " "); 39 | 40 | for (String temp : s) { 41 | System.out.println(Symbol.getByCode(temp)); 42 | } 43 | } 44 | 45 | @Test 46 | public void 测试2() { 47 | 48 | // 9选4 ,有126种组合 49 | 50 | // 9选3 ,有84种组合 51 | // 8选3 ,有56种组合 52 | // 7选3 ,有35种组合 53 | // 6选3 ,有20种组合 54 | 55 | // 7选4 ,有35种组合 56 | // 7选5 ,有21种组合 57 | 58 | // 8选4 ,有70种组合 59 | // 8选5 ,有56种组合 60 | 61 | // 13选7 ,有1716种组合 62 | // 12选7 ,有792种组合 63 | // 11选7 ,有330种组合 64 | List symbolList = Lists.newArrayList(); 65 | symbolList.add(Symbol.BTCUSDT); 66 | symbolList.add(Symbol.ETHUSDT); 67 | symbolList.add(Symbol.DOGEUSDT); 68 | symbolList.add(Symbol.SHIBUSDT); 69 | symbolList.add(Symbol.FTMUSDT); 70 | symbolList.add(Symbol.ROSEUSDT); 71 | symbolList.add(Symbol.LUNAUSDT); 72 | symbolList.add(Symbol.SLPUSDT); 73 | symbolList.add(Symbol.MATICUSDT); 74 | symbolList.add(Symbol.BNBUSDT); 75 | symbolList.add(Symbol.AVAXUSDT); 76 | //symbolList.add(Symbol.ADAUSDT); 77 | //symbolList.add(Symbol.ACHUSDT); 78 | List> lists = CombineUtil.assignSymbolCombine(symbolList, 7); 79 | 80 | System.out.println(lists.size()); 81 | } 82 | 83 | @Test 84 | public void 测试3() { 85 | 86 | List> tempList = Lists.newArrayList(); 87 | 88 | tempList.add(MutablePair.of(Symbol.ETHUSDT,new BigDecimal(0.05))); 89 | tempList.add(MutablePair.of(Symbol.BNBUSDT,new BigDecimal(0.31))); 90 | 91 | Collections.sort(tempList, (o1, o2) -> { 92 | if (o1.getRight().compareTo(o2.getRight()) < 0) { 93 | return 1; 94 | } else { 95 | return -1; 96 | } 97 | }); 98 | 99 | List resultList = Lists.newArrayList(); 100 | 101 | for (int i = 0; i < tempList.size(); i++) { 102 | 103 | // 只取前3 104 | if (i >= 3) { 105 | break; 106 | } 107 | 108 | MutablePair pair = tempList.get(i); 109 | 110 | resultList.add(pair.getLeft()); 111 | } 112 | 113 | System.out.println(JSON.toJSONString(resultList)); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/价格Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | import javax.annotation.Resource; 11 | 12 | import com.alibaba.fastjson.JSONObject; 13 | 14 | import com.aixi.lv.config.BackTestConfig; 15 | import com.aixi.lv.model.constant.Interval; 16 | import com.aixi.lv.model.constant.Symbol; 17 | import com.aixi.lv.model.domain.KLine; 18 | import com.aixi.lv.service.PriceService; 19 | import com.aixi.lv.util.NumUtil; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.assertj.core.util.Lists; 22 | import org.junit.Test; 23 | 24 | /** 25 | * @author Js 26 | */ 27 | @Slf4j 28 | public class 价格Test extends BaseTest { 29 | 30 | @Resource 31 | PriceService priceService; 32 | 33 | @Test 34 | public void 今年1月以来的价格涨幅() { 35 | 36 | List list = Lists.newArrayList(); 37 | 38 | for (Symbol symbol : Symbol.values()) { 39 | 40 | try { 41 | 42 | LocalDateTime startDay = LocalDateTime.of(2022, 1, 1, 0, 0); 43 | LocalDateTime endDay = startDay.plusDays(1); 44 | List kLines = priceService.queryKLineByTime(symbol, Interval.DAY_1, 2, startDay, endDay); 45 | 46 | BigDecimal startPrice = kLines.get(0).getClosingPrice(); 47 | 48 | BigDecimal endPrice = priceService.queryNewPrice(symbol); 49 | 50 | Double rate = endPrice.subtract(startPrice).divide(startPrice, 4, RoundingMode.HALF_DOWN).doubleValue(); 51 | 52 | JSONObject jsonObject = new JSONObject(); 53 | jsonObject.put("symbol", symbol.getCode()); 54 | jsonObject.put("rate", rate); 55 | list.add(jsonObject); 56 | 57 | } catch (Exception e) { 58 | System.out.println("异常币种是 " + symbol); 59 | System.out.println(e); 60 | } 61 | 62 | } 63 | 64 | // 排序 65 | Collections.sort(list, (o1, o2) -> { 66 | if (o1.getDouble("rate") <= o2.getDouble("rate")) { 67 | return 1; 68 | } else { 69 | return -1; 70 | } 71 | }); 72 | 73 | for (JSONObject jo : list) { 74 | System.out.println(jo.getString("symbol") + " 增长率 = " + NumUtil.percent(jo.getDouble("rate"))); 75 | } 76 | } 77 | 78 | @Test 79 | public void 上市以来价格涨幅() { 80 | 81 | for (Symbol symbol : Symbol.values()) { 82 | 83 | try { 84 | 85 | LocalDate saleDate = symbol.getSaleDate(); 86 | LocalDateTime startDay = LocalDateTime.of(saleDate.getYear(), saleDate.getMonth(), 87 | saleDate.getDayOfMonth(), 88 | 0, 0); 89 | LocalDateTime endDay = startDay.plusDays(1); 90 | 91 | // 时间最早的排 index=0 , 时间最晚的排 index=size-1 92 | List kLines = priceService.queryKLineByTime(symbol, Interval.DAY_1, 2, startDay, endDay); 93 | 94 | BigDecimal startPrice = kLines.get(0).getClosingPrice(); 95 | 96 | BigDecimal endPrice = priceService.queryNewPrice(symbol); 97 | 98 | Double rate = endPrice.subtract(startPrice).divide(startPrice, 4, RoundingMode.HALF_DOWN).doubleValue(); 99 | 100 | System.out.println(symbol + " 增长率 = " + NumUtil.percent(rate)); 101 | 102 | } catch (Exception e) { 103 | System.out.println("异常币种是 " + symbol); 104 | System.out.println(e); 105 | } 106 | 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/近期回测Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.aixi.lv.analysis.SymbolChoiceAnalysis; 9 | import com.aixi.lv.config.BackTestConfig; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.google.common.collect.Lists; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.collections4.CollectionUtils; 14 | import org.junit.Test; 15 | 16 | /** 17 | * @author Js 18 | */ 19 | @Slf4j 20 | public class 近期回测Test extends BaseTest { 21 | 22 | @Resource 23 | SymbolChoiceAnalysis symbolChoiceAnalysis; 24 | 25 | @Test 26 | public void 近期_单次初测() { 27 | 28 | this.近期_单次初测Action(null, null); 29 | 30 | } 31 | 32 | /** 33 | * 34 | 近期_单次初测Action ,结束日期 2022-03-27T23:00 35 | 天数 = 30 | 账户增长率 = 31.69% | 币种增长率 = 21.43% | 盈亏比 = 3.263 | 盈利 = 4841.531 | 亏损 = 1483.942 | 盈利次数 = 16.371 | 亏损次数 = 4.171 36 | 天数 = 14 | 账户增长率 = 12.14% | 币种增长率 = 30.19% | 盈亏比 = 3.375 | 盈利 = 1843.041 | 亏损 = 546.017 | 盈利次数 = 8.471 | 亏损次数 = 1.843 37 | 天数 = 5 | 账户增长率 = 4.84% | 币种增长率 = 12.30% | 盈亏比 = 4.826 | 盈利 = 641.522 | 亏损 = 132.921 | 盈利次数 = 2.843 | 亏损次数 = 0.286 38 | 天数 = 3 | 账户增长率 = 3.02% | 币种增长率 = 6.41% | 盈亏比 = 0 | 盈利 = 311.525 | 亏损 = 0 | 盈利次数 = 1.286 | 亏损次数 = 0 39 | 40 | */ 41 | @Test 42 | public void 近期_对比() { 43 | 44 | List list = Lists.newArrayList(); 45 | list.add(Symbol.BTCUSDT); 46 | list.add(Symbol.ETHUSDT); 47 | list.add(Symbol.XRPUSDT); 48 | list.add(Symbol.LUNAUSDT); 49 | list.add(Symbol.ADAUSDT); 50 | list.add(Symbol.SOLUSDT); 51 | list.add(Symbol.DOTUSDT); 52 | list.add(Symbol.PEOPLEUSDT); 53 | 54 | this.近期_单次初测Action(list, 4); 55 | 56 | } 57 | 58 | /** 59 | * 60 | 近期_单次初测Action ,结束日期 2022-03-27T23:00 61 | 天数 = 30 | 账户增长率 = 8.98% | 币种增长率 = 13.12% | 盈亏比 = 1.5 | 盈利 = 3176.702 | 亏损 = 2117.902 | 盈利次数 = 15.571 | 亏损次数 = 4.667 62 | 天数 = 14 | 账户增长率 = 5.93% | 币种增长率 = 22.68% | 盈亏比 = 1.777 | 盈利 = 1537.382 | 亏损 = 865.208 | 盈利次数 = 8.095 | 亏损次数 = 2 63 | 天数 = 5 | 账户增长率 = 0.00% | 币种增长率 = 9.67% | 盈亏比 = 1.05 | 盈利 = 496.265 | 亏损 = 472.763 | 盈利次数 = 1.952 | 亏损次数 = 1 64 | 天数 = 3 | 账户增长率 = -5.48% | 币种增长率 = 3.80% | 盈亏比 = 0 | 盈利 = 0 | 亏损 = 535.162 | 盈利次数 = 0 | 亏损次数 = 1.714 65 | 66 | */ 67 | private void 近期_单次初测Action(List list, Integer combineSize) { 68 | 69 | // 开启 1分钟级 K线 70 | BackTestConfig.MINUTE_ONE_K_LINE_OPEN = Boolean.TRUE; 71 | 72 | // 初始可供选择的币种 7选5 = 21 个账户 73 | if (CollectionUtils.isEmpty(list)) { 74 | list = Lists.newArrayList(); 75 | list.add(Symbol.GALAUSDT); 76 | list.add(Symbol.MATICUSDT); 77 | list.add(Symbol.BNBUSDT); 78 | list.add(Symbol.SHIBUSDT); 79 | list.add(Symbol.SOLUSDT); 80 | list.add(Symbol.ETHUSDT); 81 | list.add(Symbol.LUNAUSDT); 82 | } 83 | 84 | // 组合账户币种数量 85 | if (combineSize == null) { 86 | combineSize = 5; 87 | } 88 | 89 | // 回测截止日期 90 | LocalDateTime endTime = LocalDateTime.of(2022, 3, 27, 23, 0); 91 | 92 | StringBuilder sb = new StringBuilder(); 93 | 94 | // 分析逻辑 95 | symbolChoiceAnalysis.币种组合分析(endTime, 30, null, list, combineSize, sb, null); 96 | 97 | symbolChoiceAnalysis.币种组合分析(endTime, 14, null, list, combineSize, sb, null); 98 | 99 | symbolChoiceAnalysis.币种组合分析(endTime, 5, null, list, combineSize, sb, null); 100 | 101 | symbolChoiceAnalysis.币种组合分析(endTime, 3, null, list, combineSize, sb, null); 102 | 103 | System.out.println("\n"); 104 | System.out.println(" 近期_单次初测Action ,结束日期 " + endTime); 105 | System.out.println(sb); 106 | 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/util/NumUtil.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.util; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.text.DecimalFormat; 6 | 7 | import com.aixi.lv.config.ExchangeInfoConfig; 8 | import com.aixi.lv.model.constant.Symbol; 9 | import com.aixi.lv.model.domain.ExchangeInfoAmountFilter; 10 | import com.aixi.lv.model.domain.ExchangeInfoPriceFilter; 11 | import com.aixi.lv.model.domain.ExchangeInfoQtyFilter; 12 | 13 | /** 14 | * @author Js 15 | 16 | */ 17 | public class NumUtil { 18 | 19 | /** 20 | * 价格精度过滤 21 | * 22 | * @param symbol 23 | * @param price 24 | * @return 25 | */ 26 | public static BigDecimal pricePrecision(Symbol symbol, BigDecimal price) { 27 | 28 | ExchangeInfoPriceFilter priceFilter = ExchangeInfoConfig.PRICE_FILTER_MAP.get(symbol); 29 | 30 | if (priceFilter == null) { 31 | throw new RuntimeException(" 价格过滤器未找到 | symbol=" + symbol); 32 | } 33 | 34 | return price.setScale(priceFilter.getPriceScale(), RoundingMode.HALF_UP); 35 | } 36 | 37 | /** 38 | * 数量精度过滤 39 | * 40 | * @param symbol 41 | * @param quantity 42 | * @return 43 | */ 44 | public static BigDecimal qtyPrecision(Symbol symbol, BigDecimal quantity) { 45 | 46 | ExchangeInfoQtyFilter qtyFilter = ExchangeInfoConfig.QTY_FILTER_MAP.get(symbol); 47 | 48 | if (qtyFilter == null) { 49 | throw new RuntimeException(" 数量过滤器未找到 | symbol=" + symbol); 50 | } 51 | 52 | return quantity.setScale(qtyFilter.getQtyScale(), RoundingMode.DOWN); 53 | } 54 | 55 | /** 56 | * 去掉多余的零 57 | * 58 | * @param num 59 | * @return 60 | */ 61 | public static String cutZero(BigDecimal num) { 62 | return num.stripTrailingZeros().toPlainString(); 63 | } 64 | 65 | /** 66 | * 最小订单金额检查 67 | * 68 | * @param symbol 69 | * @param quantity 70 | * @param price 71 | * @return 72 | */ 73 | public static Boolean isErrorAmount(Symbol symbol, BigDecimal quantity, BigDecimal price) { 74 | 75 | ExchangeInfoAmountFilter amountFilter = ExchangeInfoConfig.AMOUNT_FILTER_MAP.get(symbol); 76 | 77 | if (amountFilter == null) { 78 | throw new RuntimeException(" 金额过滤器未找到 | symbol=" + symbol); 79 | } 80 | 81 | BigDecimal orderAmount = quantity.multiply(price); 82 | 83 | BigDecimal minAmount = amountFilter.getMinNotional(); 84 | 85 | if (orderAmount.compareTo(minAmount) > 0) { 86 | return Boolean.FALSE; 87 | } else { 88 | return Boolean.TRUE; 89 | 90 | } 91 | } 92 | 93 | /** 94 | * 获取两者之间更小的 95 | * 96 | * @param numA 97 | * @param numB 98 | * @return 99 | */ 100 | public static BigDecimal getSmallerPrice(BigDecimal numA, BigDecimal numB) { 101 | 102 | if (numA.compareTo(numB) <= 0) { 103 | return numA; 104 | } else { 105 | return numB; 106 | } 107 | } 108 | 109 | /** 110 | * 获取两者之间更大的 111 | * 112 | * @param numA 113 | * @param numB 114 | * @return 115 | */ 116 | public static BigDecimal getBiggerPrice(BigDecimal numA, BigDecimal numB) { 117 | 118 | if (numA.compareTo(numB) >= 0) { 119 | return numA; 120 | } else { 121 | return numB; 122 | } 123 | } 124 | 125 | /** 126 | * 百分比显示 127 | * 128 | * @param num 129 | * @return 130 | */ 131 | public static String percent(Double num) { 132 | 133 | DecimalFormat df = new DecimalFormat("0.00%"); 134 | String str = df.format(num); 135 | 136 | return str; 137 | } 138 | 139 | /** 140 | * 最多只展示3位小数 141 | * 142 | * @param num 143 | * @return 144 | */ 145 | public static String showDouble(Double num) { 146 | 147 | BigDecimal bigDecimal = new BigDecimal(num); 148 | return bigDecimal.setScale(3, RoundingMode.HALF_DOWN).stripTrailingZeros().toPlainString(); 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/schedule/offline/ProfitTask.java: -------------------------------------------------------------------------------- 1 | //package com.aixi.lv.schedule; 2 | // 3 | //import javax.annotation.Resource; 4 | // 5 | //import com.aixi.lv.model.constant.Symbol; 6 | //import com.aixi.lv.service.MailService; 7 | //import com.aixi.lv.strategy.profit.FirstProfitStrategy; 8 | //import com.aixi.lv.strategy.profit.ForceProfitStrategy; 9 | //import com.aixi.lv.strategy.profit.SecondProfitStrategy; 10 | //import com.aixi.lv.strategy.profit.ThirdProfitStrategy; 11 | //import lombok.extern.slf4j.Slf4j; 12 | //import org.springframework.scheduling.annotation.Async; 13 | //import org.springframework.scheduling.annotation.EnableAsync; 14 | //import org.springframework.scheduling.annotation.EnableScheduling; 15 | //import org.springframework.scheduling.annotation.Scheduled; 16 | //import org.springframework.stereotype.Component; 17 | // 18 | ///** 19 | // * @author Js 20 | // */ 21 | ////@Component 22 | ////@EnableScheduling // 1.开启定时任务 23 | ////@EnableAsync // 2.开启多线程 24 | //@Slf4j 25 | //public class ProfitTask { 26 | // 27 | // @Resource 28 | // FirstProfitStrategy firstProfitStrategy; 29 | // 30 | // @Resource 31 | // SecondProfitStrategy secondProfitStrategy; 32 | // 33 | // @Resource 34 | // ThirdProfitStrategy thirdProfitStrategy; 35 | // 36 | // @Resource 37 | // ForceProfitStrategy forceProfitStrategy; 38 | // 39 | // @Resource 40 | // MailService mailService; 41 | // 42 | // /** 43 | // * 轮询止盈策略 44 | // */ 45 | // @Async 46 | // @Scheduled(cron = "40 0/1 * * * ? ") // 每分钟第40秒 47 | // public void firstProfit() { 48 | // 49 | // try { 50 | // 51 | // firstProfitStrategy.firstProfit(Symbol.ETHUSDT); 52 | // firstProfitStrategy.firstProfit(Symbol.DOGEUSDT); 53 | // firstProfitStrategy.firstProfit(Symbol.BTCUSDT); 54 | // 55 | // } catch (Exception e) { 56 | // log.error("ProfitTask异常 " + e.getMessage(), e); 57 | // mailService.sendEmail("ProfitTask异常", e.getMessage()); 58 | // // 终止程序运行 59 | // System.exit(0); 60 | // } 61 | // 62 | // } 63 | // 64 | // /** 65 | // * 轮询止盈策略 66 | // */ 67 | // @Async 68 | // @Scheduled(cron = "43 0/1 * * * ? ") // 每分钟第43秒 69 | // public void secondProfit() { 70 | // 71 | // try { 72 | // 73 | // secondProfitStrategy.secondProfit(Symbol.ETHUSDT); 74 | // secondProfitStrategy.secondProfit(Symbol.DOGEUSDT); 75 | // secondProfitStrategy.secondProfit(Symbol.BTCUSDT); 76 | // 77 | // } catch (Exception e) { 78 | // log.error("ProfitTask异常 " + e.getMessage(), e); 79 | // mailService.sendEmail("ProfitTask异常", e.getMessage()); 80 | // // 终止程序运行 81 | // System.exit(0); 82 | // } 83 | // 84 | // } 85 | // 86 | // /** 87 | // * 轮询止盈策略 88 | // */ 89 | // @Async 90 | // @Scheduled(cron = "46 0/1 * * * ? ") // 每分钟第46秒 91 | // public void thirdProfit() { 92 | // 93 | // try { 94 | // 95 | // thirdProfitStrategy.thirdProfit(Symbol.ETHUSDT); 96 | // thirdProfitStrategy.thirdProfit(Symbol.DOGEUSDT); 97 | // thirdProfitStrategy.thirdProfit(Symbol.BTCUSDT); 98 | // 99 | // } catch (Exception e) { 100 | // log.error("ProfitTask异常 " + e.getMessage(), e); 101 | // mailService.sendEmail("ProfitTask异常", e.getMessage()); 102 | // // 终止程序运行 103 | // System.exit(0); 104 | // } 105 | // 106 | // } 107 | // 108 | // /** 109 | // * 轮询止盈策略 110 | // */ 111 | // @Async 112 | // @Scheduled(cron = "49 0/1 * * * ? ") // 每分钟第49秒 113 | // public void forceProfit() { 114 | // 115 | // try { 116 | // 117 | // forceProfitStrategy.forceProfit(Symbol.ETHUSDT); 118 | // forceProfitStrategy.forceProfit(Symbol.DOGEUSDT); 119 | // forceProfitStrategy.forceProfit(Symbol.BTCUSDT); 120 | // 121 | // } catch (Exception e) { 122 | // log.error("ProfitTask异常 " + e.getMessage(), e); 123 | // mailService.sendEmail("ProfitTask异常", e.getMessage()); 124 | // // 终止程序运行 125 | // System.exit(0); 126 | // } 127 | // 128 | // } 129 | // 130 | //} 131 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/domain/KLine.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.Instant; 5 | import java.time.LocalDateTime; 6 | import java.time.ZoneOffset; 7 | import java.util.List; 8 | 9 | import com.alibaba.fastjson.annotation.JSONField; 10 | 11 | import com.aixi.lv.model.constant.Symbol; 12 | import lombok.AllArgsConstructor; 13 | import lombok.Builder; 14 | import lombok.Data; 15 | import lombok.NoArgsConstructor; 16 | 17 | /** 18 | * @author Js 19 | */ 20 | @Data 21 | @Builder 22 | @AllArgsConstructor 23 | @NoArgsConstructor 24 | public class KLine { 25 | 26 | /** 27 | * 参数值示例 28 | * 1499040000000, // 开盘时间 29 | * "0.01634790", // 开盘价 30 | * "0.80000000", // 最高价 31 | * "0.01575800", // 最低价 32 | * "0.01577100", // 收盘价(当前K线未结束的即为最新价) 33 | * "148976.11427815", // 成交量 34 | * 1499644799999, // 收盘时间 35 | * "2434.19055334", // 成交额 36 | * 308, // 成交笔数 37 | * "1756.87402397", // 主动买入成交量 38 | * "28.46694368", // 主动买入成交额 39 | * "17928899.62484339" // 请忽略该参数 40 | **/ 41 | 42 | // 开盘时间 43 | @JSONField(ordinal = 2) 44 | private LocalDateTime openingTime; 45 | 46 | // 开盘价 47 | @JSONField(ordinal = 99) 48 | private BigDecimal openingPrice; 49 | 50 | // 最高价 51 | @JSONField(ordinal = 5) 52 | private BigDecimal maxPrice; 53 | 54 | // 最低价 55 | @JSONField(ordinal = 6) 56 | private BigDecimal minPrice; 57 | 58 | // 收盘价(当前K线未结束的即为最新价) 59 | @JSONField(ordinal = 3) 60 | private BigDecimal closingPrice; 61 | 62 | // 成交量 63 | @JSONField(ordinal = 4) 64 | private BigDecimal tradingVolume; 65 | 66 | // 收盘时间 67 | @JSONField(ordinal = 99) 68 | private LocalDateTime closingTime; 69 | 70 | // 成交额 71 | @JSONField(ordinal = 99) 72 | private BigDecimal tradingAmount; 73 | 74 | // 成交笔数 75 | @JSONField(ordinal = 99) 76 | private Integer tradingNumber; 77 | 78 | // 主动买入成交量 79 | @JSONField(ordinal = 99) 80 | private BigDecimal buyTradingVolume; 81 | 82 | // 主动买入成交额 83 | @JSONField(ordinal = 99) 84 | private BigDecimal buyTradingAmount; 85 | 86 | // 请忽略该参数 87 | @JSONField(ordinal = 99) 88 | private BigDecimal ignoreArg; 89 | 90 | // 交易对(币种) 91 | @JSONField(ordinal = 1) 92 | private Symbol symbol; 93 | 94 | public static KLine parseList(List list) { 95 | 96 | KLine kLine = new KLine(); 97 | 98 | LocalDateTime openingTime = 99 | Instant.ofEpochMilli((Long)list.get(0)).atZone(ZoneOffset.ofHours(8)).toLocalDateTime(); 100 | kLine.setOpeningTime(openingTime); 101 | 102 | BigDecimal openingPrice = new BigDecimal((String)list.get(1)); 103 | kLine.setOpeningPrice(openingPrice); 104 | 105 | BigDecimal maxPrice = new BigDecimal((String)list.get(2)); 106 | kLine.setMaxPrice(maxPrice); 107 | 108 | BigDecimal minPrice = new BigDecimal((String)list.get(3)); 109 | kLine.setMinPrice(minPrice); 110 | 111 | BigDecimal closingPrice = new BigDecimal((String)list.get(4)); 112 | kLine.setClosingPrice(closingPrice); 113 | 114 | BigDecimal tradingVolume = new BigDecimal((String)list.get(5)); 115 | kLine.setTradingVolume(tradingVolume); 116 | 117 | LocalDateTime closingTime = 118 | Instant.ofEpochMilli((Long)list.get(6)).atZone(ZoneOffset.ofHours(8)).toLocalDateTime(); 119 | kLine.setClosingTime(closingTime); 120 | 121 | BigDecimal tradingAmount = new BigDecimal((String)list.get(7)); 122 | kLine.setTradingAmount(tradingAmount); 123 | 124 | Integer tradingNumber = (Integer)list.get(8); 125 | kLine.setTradingNumber(tradingNumber); 126 | 127 | BigDecimal buyTradingVolume = new BigDecimal((String)list.get(9)); 128 | kLine.setBuyTradingVolume(buyTradingVolume); 129 | 130 | BigDecimal buyTradingAmount = new BigDecimal((String)list.get(10)); 131 | kLine.setBuyTradingAmount(buyTradingAmount); 132 | 133 | BigDecimal ignoreArg = new BigDecimal((String)list.get(11)); 134 | kLine.setIgnoreArg(ignoreArg); 135 | 136 | return kLine; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/HttpService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.net.URI; 4 | import java.util.Map; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.alibaba.fastjson.JSON; 9 | import com.alibaba.fastjson.JSONArray; 10 | import com.alibaba.fastjson.JSONObject; 11 | 12 | import com.aixi.lv.util.ApiUtil; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.apache.commons.collections4.MapUtils; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.http.RequestEntity; 18 | import org.springframework.http.ResponseEntity; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.util.LinkedMultiValueMap; 21 | import org.springframework.util.MultiValueMap; 22 | import org.springframework.web.client.RestTemplate; 23 | import org.springframework.web.util.UriComponentsBuilder; 24 | 25 | /** 26 | * @author Js 27 | */ 28 | @Component 29 | @Slf4j 30 | public class HttpService { 31 | 32 | @Resource 33 | RestTemplate restTemplate; 34 | 35 | /** 36 | * 测试连通性 37 | * 38 | * @return 39 | */ 40 | public Boolean testConnected() { 41 | 42 | try { 43 | 44 | String url = ApiUtil.url("/api/v3/time"); 45 | 46 | JSONObject response = this.getObject(url, null); 47 | 48 | Long serverTime = response.getLong("serverTime"); 49 | 50 | if (serverTime == null) { 51 | return Boolean.FALSE; 52 | } 53 | 54 | } catch (Exception e) { 55 | log.error(" HttpService | testConnected_fail | 服务不通"); 56 | } 57 | 58 | return Boolean.TRUE; 59 | 60 | } 61 | 62 | /** 63 | * GET 请求 64 | * 65 | * @param url 66 | * @param params 67 | * @return 对象 68 | */ 69 | public JSONObject getObject(String url, JSONObject params) { 70 | 71 | RequestEntity request = this.buildGetRequest(url, params); 72 | 73 | ResponseEntity response = restTemplate.exchange(request, JSONObject.class); 74 | 75 | if (!response.getStatusCode().equals(HttpStatus.OK)) { 76 | log.error(" getObject | HttpStatus_error | request={} | resp={}", JSON.toJSONString(request), 77 | JSON.toJSONString(response)); 78 | throw new RuntimeException(" getObject | HttpStatus_error "); 79 | } 80 | 81 | return response.getBody(); 82 | } 83 | 84 | /** 85 | * GET 请求 86 | * 87 | * @param url 88 | * @param params 89 | * @return 数组 90 | */ 91 | public JSONArray getArray(String url, JSONObject params) { 92 | 93 | RequestEntity request = this.buildGetRequest(url, params); 94 | 95 | ResponseEntity response = restTemplate.exchange(request, JSONArray.class); 96 | 97 | if (!response.getStatusCode().equals(HttpStatus.OK)) { 98 | log.error(" getArray | HttpStatus_error | request={} | resp={}", JSON.toJSONString(request), 99 | JSON.toJSONString(response)); 100 | throw new RuntimeException(" getArray | HttpStatus_error "); 101 | } 102 | 103 | return response.getBody(); 104 | 105 | } 106 | 107 | /** 108 | * 请求参数组装 109 | * 110 | * @param url 111 | * @param params 112 | * @return 113 | */ 114 | private RequestEntity buildGetRequest(String url, JSONObject params) { 115 | 116 | UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url); 117 | 118 | if (params != null) { 119 | 120 | MultiValueMap paramMap = new LinkedMultiValueMap<>(); 121 | 122 | for (Map.Entry entry : params.entrySet()) { 123 | paramMap.add(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); 124 | } 125 | 126 | if (MapUtils.isNotEmpty(paramMap)) { 127 | builder.queryParams(paramMap); 128 | } 129 | } 130 | 131 | URI uri = builder.build().toUri(); 132 | 133 | RequestEntity request = RequestEntity.get(uri) 134 | .accept(MediaType.APPLICATION_JSON) 135 | .build(); 136 | 137 | return request; 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/BOLL.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | import com.aixi.lv.model.constant.Interval; 7 | import com.aixi.lv.model.constant.Symbol; 8 | import lombok.Getter; 9 | 10 | /** 11 | * @author Js 12 | * 13 | * 双布林带 14 | */ 15 | public class BOLL implements Indicator { 16 | 17 | private double closingPrice; 18 | private double standardDeviation; 19 | private final int period; 20 | 21 | /** 22 | * 外层高线 (上带) 23 | */ 24 | @Getter 25 | private double upperBand; 26 | 27 | /** 28 | * 内层高线 (上中带) 29 | */ 30 | @Getter 31 | private double upperMidBand; 32 | 33 | /** 34 | * 中线(中带) 35 | */ 36 | @Getter 37 | private double middleBand; 38 | 39 | /** 40 | * 内层低线(下中带) 41 | */ 42 | @Getter 43 | private double lowerMidBand; 44 | 45 | /** 46 | * 外层低线(下带) 47 | */ 48 | @Getter 49 | private double lowerBand; 50 | 51 | private String explanation; 52 | 53 | private SMA sma; 54 | 55 | /** 56 | * 指标对应的 K线 开盘时间 (这根K线已经走完了) 57 | */ 58 | @Getter 59 | private LocalDateTime kLineOpenTime; 60 | 61 | @Getter 62 | private Symbol symbol; 63 | 64 | @Getter 65 | private Interval interval; 66 | 67 | public BOLL(List closingPrices, int period) { 68 | this.period = period; 69 | this.sma = new SMA(closingPrices, period); 70 | init(closingPrices); 71 | } 72 | 73 | public BOLL(List closingPrices, LocalDateTime kLineOpenTime, Symbol symbol, Interval interval, int period) { 74 | this.period = period; 75 | this.kLineOpenTime = kLineOpenTime; 76 | this.symbol = symbol; 77 | this.interval = interval; 78 | this.sma = new SMA(closingPrices, period); 79 | init(closingPrices); 80 | } 81 | 82 | @Override 83 | public double get() { 84 | if ((upperBand - lowerBand) / middleBand < 0.005) { 85 | return 0; 86 | } 87 | if (upperMidBand < closingPrice && closingPrice <= upperBand) { 88 | return 1; 89 | } 90 | if (lowerBand < closingPrice && closingPrice <= lowerMidBand) { 91 | return -1; 92 | } else { 93 | return 0; 94 | } 95 | } 96 | 97 | @Override 98 | public double getTemp(double newPrice) { 99 | double tempMidBand = sma.getTemp(newPrice); 100 | double tempStdev = sma.tempStandardDeviation(newPrice); 101 | double tempUpperBand = tempMidBand + tempStdev * 2; 102 | double tempUpperMidBand = tempMidBand + tempStdev; 103 | double tempLowerMidBand = tempMidBand - tempStdev; 104 | double tempLowerBand = tempMidBand - tempStdev * 2; 105 | if ((tempUpperBand - tempLowerBand) / tempMidBand < 0.005) //Low volatility case 106 | {return 0;} 107 | if (tempUpperMidBand < newPrice && newPrice <= tempUpperBand) {return 1;} 108 | if (tempLowerBand < newPrice && newPrice <= tempLowerMidBand) {return -1;} else {return 0;} 109 | } 110 | 111 | @Override 112 | public void init(List closingPrices) { 113 | if (period > closingPrices.size()) { 114 | return; 115 | } 116 | 117 | closingPrice = closingPrices.get(closingPrices.size() - 2); 118 | standardDeviation = sma.standardDeviation(); 119 | middleBand = sma.get(); 120 | upperBand = middleBand + standardDeviation * 2; 121 | upperMidBand = middleBand + standardDeviation; 122 | lowerMidBand = middleBand - standardDeviation; 123 | lowerBand = middleBand - standardDeviation * 2; 124 | 125 | } 126 | 127 | @Override 128 | public void update(double newPrice) { 129 | closingPrice = newPrice; 130 | sma.update(newPrice); 131 | standardDeviation = sma.standardDeviation(); 132 | middleBand = sma.get(); 133 | upperBand = middleBand + standardDeviation * 2; 134 | upperMidBand = middleBand + standardDeviation; 135 | lowerMidBand = middleBand - standardDeviation; 136 | lowerBand = middleBand - standardDeviation * 2; 137 | } 138 | 139 | @Override 140 | public int check(double newPrice) { 141 | if (getTemp(newPrice) == 1) { 142 | explanation = "Price in DBB buy zone"; 143 | return 1; 144 | } 145 | if (getTemp(newPrice) == -1) { 146 | explanation = "Price in DBB sell zone"; 147 | return -1; 148 | } 149 | explanation = ""; 150 | return 0; 151 | } 152 | 153 | @Override 154 | public String getExplanation() { 155 | return explanation; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/MacdTradeConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import com.aixi.lv.model.constant.Symbol; 10 | import com.aixi.lv.model.domain.MacdAccount; 11 | import com.aixi.lv.util.CombineUtil; 12 | import com.aixi.lv.util.TimeUtil; 13 | import com.google.common.collect.Lists; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.springframework.boot.context.event.ApplicationReadyEvent; 17 | import org.springframework.context.ApplicationListener; 18 | import org.springframework.context.annotation.DependsOn; 19 | import org.springframework.stereotype.Component; 20 | 21 | import static com.aixi.lv.config.BackTestConfig.OPEN; 22 | import static com.aixi.lv.strategy.indicator.MacdBuySellStrategy.MACD_ACCOUNT_MAP; 23 | 24 | /** 25 | * @author Js 26 | */ 27 | @Component 28 | @DependsOn("backTestConfig") 29 | @Slf4j 30 | public class MacdTradeConfig implements ApplicationListener { 31 | 32 | public static final String JS_ACCOUNT_NAME = "精选币账户"; 33 | 34 | public static final ThreadLocal THREAD_LOCAL_ACCOUNT = new ThreadLocal<>(); 35 | 36 | /** 37 | * 有些新上的币,就不做天级别MACD检查了 38 | */ 39 | public static final List IGNORE_DAY_MACD_SYMBOL_LIST = Lists.newArrayList(); 40 | 41 | @Override 42 | public void onApplicationEvent(ApplicationReadyEvent event) { 43 | 44 | log.warn(" START | 加载 MACD 账户"); 45 | 46 | if (!OPEN) { 47 | 48 | // 初始化账户 49 | List list = Lists.newArrayList(); 50 | list.add(Symbol.ETHUSDT); 51 | list.add(Symbol.XRPUSDT); 52 | list.add(Symbol.LUNAUSDT); 53 | list.add(Symbol.BNBUSDT); 54 | list.add(Symbol.SOLUSDT); 55 | list.add(Symbol.SHIBUSDT); 56 | list.add(Symbol.NEARUSDT); 57 | this.loadBelongAccount(list, JS_ACCOUNT_NAME); 58 | 59 | // 有些新上的币,就不做天级别MACD检查了 60 | this.loadIgnoreDaySymbol(); 61 | } 62 | 63 | log.warn(" FINISH | 加载 MACD 账户"); 64 | } 65 | 66 | /** 67 | * 生产账户-归属账户 68 | */ 69 | private void loadBelongAccount(List list, String name) { 70 | 71 | // 5个币为一个组合 72 | Integer symbolCombineNum = 5; 73 | 74 | List> combineList = CombineUtil.assignSymbolCombine(list, symbolCombineNum); 75 | 76 | for (int i = 0; i < combineList.size(); i++) { 77 | List symbolList = combineList.get(i); 78 | String accountName = name + "_" + String.format("%02d", i + 1); // 账户从01开始 79 | String belongAccount = name + "_归属账户"; 80 | MacdAccount account = initAccount(accountName, symbolList, 30, belongAccount); 81 | MACD_ACCOUNT_MAP.put(account.getName(), account); 82 | } 83 | 84 | } 85 | 86 | private MacdAccount initAccount(String accountName, List symbolList, Integer amount, String belongAccount) { 87 | 88 | List collect = symbolList.stream().distinct().collect(Collectors.toList()); 89 | 90 | MacdAccount account = new MacdAccount(); 91 | account.setName(accountName); 92 | account.setSymbolList(collect); 93 | account.setCurHoldQty(BigDecimal.ZERO); 94 | account.setCurHoldAmount(new BigDecimal(amount)); 95 | account.setReadySellFlag(Boolean.FALSE); 96 | 97 | // 归属账户,多币种组合时,方便统计 98 | if (StringUtils.isNotEmpty(belongAccount)) { 99 | account.setBelongAccount(belongAccount); 100 | } 101 | 102 | return account; 103 | } 104 | 105 | /** 106 | * 有些新上的币,就不做天级别MACD检查了 107 | */ 108 | private void loadIgnoreDaySymbol() { 109 | 110 | for (Symbol symbol : Symbol.values()) { 111 | if (isIgnoreDayMacd(symbol)) { 112 | IGNORE_DAY_MACD_SYMBOL_LIST.add(symbol); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * 是否忽略天级MACD 119 | * 120 | * @param symbol 121 | * @return 122 | */ 123 | private Boolean isIgnoreDayMacd(Symbol symbol) { 124 | 125 | LocalDate saleDate = symbol.getSaleDate(); 126 | LocalDateTime now = TimeUtil.now(); 127 | LocalDate nowDate = LocalDate.of(now.getYear(), now.getMonth(), now.getDayOfMonth()); 128 | long diffDay = nowDate.toEpochDay() - saleDate.toEpochDay(); 129 | 130 | // 理论上 计算天级MACD至少需要60天的数据,这里写62保险一点 131 | if (diffDay <= 62) { 132 | return Boolean.TRUE; 133 | } else { 134 | return Boolean.FALSE; 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/com/aixi/lv/准备数据Test.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.concurrent.ArrayBlockingQueue; 7 | import java.util.concurrent.CountDownLatch; 8 | import java.util.concurrent.ThreadPoolExecutor; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import javax.annotation.Nullable; 12 | import javax.annotation.Resource; 13 | 14 | import com.aixi.lv.service.BackTestAppendService; 15 | import com.aixi.lv.service.BackTestReadService; 16 | import com.aixi.lv.service.BackTestWriteService; 17 | import com.aixi.lv.model.constant.Interval; 18 | import com.aixi.lv.model.constant.Symbol; 19 | import com.aixi.lv.model.domain.KLine; 20 | import com.aixi.lv.service.PriceService; 21 | import com.google.common.collect.Lists; 22 | import com.google.common.util.concurrent.FutureCallback; 23 | import com.google.common.util.concurrent.Futures; 24 | import com.google.common.util.concurrent.ListenableFuture; 25 | import com.google.common.util.concurrent.ListeningExecutorService; 26 | import com.google.common.util.concurrent.MoreExecutors; 27 | import lombok.extern.slf4j.Slf4j; 28 | import org.junit.Test; 29 | 30 | /** 31 | * @author Js 32 | */ 33 | @Slf4j 34 | public class 准备数据Test extends BaseTest { 35 | 36 | @Resource 37 | ListeningExecutorService listeningExecutorService; 38 | 39 | @Resource 40 | PriceService priceService; 41 | 42 | @Resource 43 | BackTestWriteService backTestWriteService; 44 | 45 | @Resource 46 | BackTestReadService backTestReadService; 47 | 48 | @Resource 49 | BackTestAppendService backTestAppendService; 50 | 51 | @Test 52 | public void 追加K线数据到此时() { 53 | 54 | //for (Symbol symbol : Symbol.values()) { 55 | // backTestAppendService.appendData(symbol); 56 | // System.out.println(symbol.getCode() + " 追加K线数据完成"); 57 | //} 58 | 59 | // 需要1分钟级数据的 60 | List needMinuteOne = Lists.newArrayList(); 61 | needMinuteOne.add(Symbol.APEUSDT); 62 | 63 | for (Symbol symbol : needMinuteOne) { 64 | backTestAppendService.appendMinuteOne(symbol); 65 | } 66 | 67 | } 68 | 69 | /** 70 | * 初始化K线数据到此时 71 | */ 72 | @Test 73 | public void 初始化K线数据() { 74 | 75 | backTestWriteService.initData(Symbol.APEUSDT); 76 | //backTestWriteService.initData(Symbol.SHIBUSDT); 77 | //backTestWriteService.initData(Symbol.LUNAUSDT); 78 | //backTestWriteService.initData(Symbol.NEARUSDT); 79 | //backTestWriteService.initData(Symbol.PEOPLEUSDT); 80 | //backTestWriteService.initData(Symbol.SOLUSDT); 81 | //backTestWriteService.initData(Symbol.GMTUSDT); 82 | 83 | } 84 | 85 | private void 初始化单个Symbol数据(Symbol symbol) { 86 | backTestWriteService.minuteOne(symbol, symbol.getSaleDate().atStartOfDay(), LocalDateTime.now()); 87 | } 88 | 89 | /** 90 | * 准备K线数据到此时 91 | */ 92 | @Test 93 | public void 准备K线数据_多线程() { 94 | 95 | List list = Lists.newArrayList(); 96 | list.add(Symbol.SOLUSDT); 97 | 98 | final CountDownLatch cd = new CountDownLatch(list.size()); 99 | final List throwableList = Lists.newArrayList(); 100 | 101 | for (Symbol symbol : list) { 102 | 103 | ListenableFuture future = listeningExecutorService.submit( 104 | () -> backTestWriteService.initData(symbol)); 105 | 106 | Futures.addCallback(future, new FutureCallback() { 107 | 108 | @Override 109 | public void onSuccess(@Nullable Object o) { 110 | try { 111 | 112 | } finally { 113 | cd.countDown(); 114 | } 115 | } 116 | 117 | @Override 118 | public void onFailure(Throwable throwable) { 119 | try { 120 | throwableList.add(throwable); 121 | } finally { 122 | cd.countDown(); 123 | } 124 | } 125 | }, listeningExecutorService); 126 | } 127 | 128 | try { 129 | // cd 计数器减到0 或者超时时间到了,继续执行主线程 130 | boolean await = cd.await(30, TimeUnit.MINUTES); 131 | if (!await) { 132 | throw new RuntimeException(" 准备K线数据 | 主线程等待超过了设定的30秒超时时间"); 133 | } 134 | } catch (InterruptedException e) { 135 | throw new RuntimeException(" 准备K线数据 | 计数器异常", e); 136 | } 137 | 138 | // 异常判断必须在 cd.await之后 139 | if (throwableList.size() > 0) { 140 | throw new RuntimeException(throwableList.get(0)); 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/schedule/offline/BuyTask.java: -------------------------------------------------------------------------------- 1 | //package com.aixi.lv.schedule; 2 | // 3 | //import javax.annotation.Resource; 4 | // 5 | //import com.aixi.lv.model.constant.Interval; 6 | //import com.aixi.lv.model.constant.Symbol; 7 | //import com.aixi.lv.service.MailService; 8 | //import com.aixi.lv.strategy.buy.BuyStrategy; 9 | //import lombok.extern.slf4j.Slf4j; 10 | //import org.springframework.scheduling.annotation.Async; 11 | //import org.springframework.scheduling.annotation.EnableAsync; 12 | //import org.springframework.scheduling.annotation.EnableScheduling; 13 | //import org.springframework.scheduling.annotation.Scheduled; 14 | //import org.springframework.stereotype.Component; 15 | // 16 | ///** 17 | // * @author Js 18 | // */ 19 | ////@Component 20 | ////@EnableScheduling // 1.开启定时任务 21 | ////@EnableAsync // 2.开启多线程 22 | //@Slf4j 23 | //public class BuyTask { 24 | // 25 | // @Resource 26 | // BuyStrategy buyStrategy; 27 | // 28 | // @Resource 29 | // MailService mailService; 30 | // 31 | // /** 32 | // * * 33 | // * * 34 | // * * 35 | // * * 36 | // * * 37 | // * ************************** 附加配置任务 ********************* 38 | // * * 39 | // * * 40 | // * * 41 | // * * 42 | // * * 43 | // */ 44 | // @Async 45 | // @Scheduled(cron = "0 0 0/6 * * ? ") // 每6小时 46 | // public void clearBuyTimes() { 47 | // 48 | // // 清理购买上限次数 49 | // buyStrategy.clearBuyLimitMap(); 50 | // } 51 | // 52 | // /** 53 | // * * 54 | // * * 55 | // * * 56 | // * * 57 | // * * 58 | // * ************************** 5分钟的 ********************* 59 | // * * 60 | // * * 61 | // * * 62 | // * * 63 | // * * 64 | // */ 65 | // 66 | // @Async 67 | // @Scheduled(cron = "30 0/1 * * * ? ") // 每分钟第30秒 68 | // public void MINUTE_5_ID_2000() { 69 | // 70 | // try { 71 | // 72 | // buyStrategy.buy(Symbol.ETHUSDT, Interval.MINUTE_5, 144, 2000); 73 | // buyStrategy.buy(Symbol.BTCUSDT, Interval.MINUTE_5, 144, 2000); 74 | // 75 | // } catch (Exception e) { 76 | // log.error("BuyTask异常 " + e.getMessage(), e); 77 | // mailService.sendEmail("BuyTask异常", e.getMessage()); 78 | // // 终止程序运行 79 | // System.exit(0); 80 | // } 81 | // } 82 | // 83 | // @Async 84 | // @Scheduled(cron = "0/5 * * * * ? ") // 每5秒执行一次 85 | // public void MINUTE_5_ID_2000_High() { 86 | // 87 | // try { 88 | // 89 | // buyStrategy.buyHighFrequencyScan(Symbol.ETHUSDT, Interval.MINUTE_5, 2000); 90 | // buyStrategy.buyHighFrequencyScan(Symbol.BTCUSDT, Interval.MINUTE_5, 2000); 91 | // 92 | // } catch (Exception e) { 93 | // log.error("BuyTask异常 " + e.getMessage(), e); 94 | // mailService.sendEmail("BuyTask异常", e.getMessage()); 95 | // // 终止程序运行 96 | // System.exit(0); 97 | // } 98 | // } 99 | // 100 | // /** 101 | // * * 102 | // * * 103 | // * * 104 | // * * 105 | // * * 106 | // * ************************** 10分钟的 ********************* 107 | // * * 108 | // * * 109 | // * * 110 | // * * 111 | // * * 112 | // */ 113 | // 114 | // @Async 115 | // @Scheduled(cron = "0 0/1 * * * ? ") // 每分钟第0秒 116 | // public void MINUTE_15_ID_1000() { 117 | // 118 | // try { 119 | // 120 | // buyStrategy.buy(Symbol.ETHUSDT, Interval.MINUTE_15, 96, 1000); 121 | // buyStrategy.buy(Symbol.BTCUSDT, Interval.MINUTE_15, 96, 1000); 122 | // 123 | // } catch (Exception e) { 124 | // log.error("BuyTask异常 " + e.getMessage(), e); 125 | // mailService.sendEmail("BuyTask异常", e.getMessage()); 126 | // // 终止程序运行 127 | // System.exit(0); 128 | // } 129 | // } 130 | // 131 | // @Async 132 | // @Scheduled(cron = "0/5 * * * * ? ") // 每5秒执行一次 133 | // public void MINUTE_15_ID_1000_High() { 134 | // 135 | // try { 136 | // 137 | // buyStrategy.buyHighFrequencyScan(Symbol.ETHUSDT, Interval.MINUTE_15, 1000); 138 | // buyStrategy.buyHighFrequencyScan(Symbol.BTCUSDT, Interval.MINUTE_15, 1000); 139 | // 140 | // } catch (Exception e) { 141 | // log.error("BuyTask异常 " + e.getMessage(), e); 142 | // mailService.sendEmail("BuyTask异常", e.getMessage()); 143 | // // 终止程序运行 144 | // System.exit(0); 145 | // } 146 | // } 147 | // 148 | // /** 149 | // * * 150 | // * * 151 | // * * 152 | // * * 上面是DTS任务区,下面都是private方法 153 | // * * 154 | // * * 155 | // * * 156 | // * * 157 | // * * 158 | // * * 159 | // * * ∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧∧ 160 | // */ 161 | //} 162 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/indicator/BollSellStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.indicator; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.model.constant.Interval; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.KLine; 12 | import com.aixi.lv.model.domain.MacdAccount; 13 | import com.aixi.lv.model.indicator.BOLL; 14 | import com.aixi.lv.service.BackTestOrderService; 15 | import com.aixi.lv.service.IndicatorService; 16 | import com.aixi.lv.service.PriceFaceService; 17 | import com.aixi.lv.util.TimeUtil; 18 | import lombok.extern.slf4j.Slf4j; 19 | import org.springframework.stereotype.Component; 20 | 21 | import static com.aixi.lv.config.BackTestConfig.OPEN; 22 | import static com.aixi.lv.strategy.indicator.MacdBuySellStrategy.MACD_ACCOUNT_MAP; 23 | 24 | /** 25 | * @author Js 26 | * 27 | * 布林带卖出策略, 和 MacdBuySellStrategy 一起结合使用 28 | */ 29 | @Component 30 | @Slf4j 31 | public class BollSellStrategy { 32 | 33 | @Resource 34 | MacdBuySellStrategy macdBuySellStrategy; 35 | 36 | @Resource 37 | IndicatorService indicatorService; 38 | 39 | @Resource 40 | PriceFaceService priceFaceService; 41 | 42 | @Resource 43 | BackTestOrderService backTestOrderService; 44 | 45 | /** 46 | * 布林线卖出探测 47 | */ 48 | public void detectSell() { 49 | 50 | for (MacdAccount account : MACD_ACCOUNT_MAP.values()) { 51 | 52 | Symbol symbol = account.getCurHoldSymbol(); 53 | 54 | if (symbol == null) { 55 | continue; 56 | } 57 | 58 | if (isUnderLowerBand(symbol, account)) { 59 | // 卖出 60 | if (OPEN) { 61 | backTestOrderService.sellAction(symbol, account, "跌破布林线卖出", BigDecimal.ONE); 62 | } else { 63 | macdBuySellStrategy.sellAction(symbol, account, "跌破布林线卖出 " + account.getName(), BigDecimal.ONE); 64 | } 65 | } 66 | 67 | } 68 | 69 | } 70 | 71 | /** 72 | * 看最近2根K线是否持续低于 BOLL 带 73 | * 74 | * @param symbol 75 | * @return 76 | */ 77 | private Boolean isUnderLowerBand(Symbol symbol, MacdAccount account) { 78 | 79 | LocalDateTime initEndTime = TimeUtil.now(); 80 | 81 | LocalDateTime endTime = initEndTime; 82 | LocalDateTime startTime = endTime.minusMinutes((500 - 1) * 5); 83 | BOLL boll = indicatorService.getBOLLByTime(symbol, Interval.MINUTE_5, startTime, endTime); 84 | BigDecimal lowerBandPrice = new BigDecimal(boll.getLowerBand()); 85 | 86 | List kLines = priceFaceService.queryKLineByTime(symbol, Interval.MINUTE_5, 10, endTime.minusMinutes(10), 87 | endTime); 88 | KLine kLine = kLines.get(kLines.size() - 2); 89 | 90 | if (kLine.getClosingPrice().compareTo(lowerBandPrice) >= 0) { 91 | return Boolean.FALSE; 92 | } 93 | 94 | LocalDateTime endTime2 = endTime.minusMinutes(5); 95 | LocalDateTime startTime2 = endTime2.minusMinutes((500 - 1) * 5); 96 | BOLL boll2 = indicatorService.getBOLLByTime(symbol, Interval.MINUTE_5, startTime2, endTime2); 97 | BigDecimal lowerBandPrice2 = new BigDecimal(boll2.getLowerBand()); 98 | 99 | List kLines2 = priceFaceService.queryKLineByTime(symbol, Interval.MINUTE_5, 10, 100 | endTime2.minusMinutes(10), 101 | endTime2); 102 | KLine kLine2 = kLines2.get(kLines2.size() - 2); 103 | 104 | if (kLine2.getClosingPrice().compareTo(lowerBandPrice2) >= 0) { 105 | return Boolean.FALSE; 106 | } 107 | 108 | double bollDiffRate = (boll.getUpperBand() - boll.getLowerBand()) / boll.getMiddleBand(); 109 | if (bollDiffRate < 0.011) { 110 | log.info( 111 | " BOLL 策略 | {} | 布林区间过小 {} | upperBand = {} | lowerBand = {} | bollDiffRate = {} | bollTime = {} | " 112 | + "kLineClosingPrice = {}", 113 | account.getName(), symbol, boll.getUpperBand(), boll.getLowerBand(), bollDiffRate, 114 | boll.getKLineOpenTime(), kLine.getClosingPrice()); 115 | return Boolean.FALSE; 116 | } 117 | 118 | // 持续低于布林带 下带 ,卖出 119 | log.info(" BOLL 策略 | {} | 布林线卖出 {} | 最近一根K线 | openTime = {} | closingPrice = {} | lowerBandPrice= {}", 120 | account.getName(), symbol, kLine.getOpeningTime(), kLine.getClosingPrice(), lowerBandPrice); 121 | log.info(" BOLL 策略 | {} | 布林线卖出 {} | 次近一根K线 | openTime = {} | closingPrice = {} | lowerBandPrice= {}", 122 | account.getName(), symbol, kLine2.getOpeningTime(), kLine2.getClosingPrice(), lowerBandPrice2); 123 | return Boolean.TRUE; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/RSI.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | 8 | import com.aixi.lv.model.constant.Interval; 9 | import com.aixi.lv.model.constant.Symbol; 10 | import lombok.Getter; 11 | 12 | /** 13 | * @author Js 14 | * 15 | * 相对强弱指数 16 | */ 17 | public class RSI implements Indicator { 18 | 19 | private double avgUp; 20 | private double avgDwn; 21 | private double prevClose; 22 | private final int period; 23 | private String explanation; 24 | 25 | /** 26 | * 严重超卖 27 | */ 28 | public static final Integer OVER_SELL_MIN = 15; 29 | 30 | /** 31 | * 超卖 32 | */ 33 | public static final Integer OVER_SELL_MAX = 30; 34 | 35 | /** 36 | * 超买 37 | */ 38 | public static final Integer OVER_BUY_MIN = 70; 39 | 40 | /** 41 | * 严重超买 42 | */ 43 | public static final Integer OVER_BUY_MAX = 78; 44 | 45 | /** 46 | * 指标对应的 K线 开盘时间 (这根K线已经走完了) 47 | */ 48 | @Getter 49 | private LocalDateTime kLineOpenTime; 50 | 51 | @Getter 52 | private Symbol symbol; 53 | 54 | @Getter 55 | private Interval interval; 56 | 57 | public RSI(List closingPrice, int period) { 58 | avgUp = 0; 59 | avgDwn = 0; 60 | this.period = period; 61 | explanation = ""; 62 | init(closingPrice); 63 | } 64 | 65 | public RSI(List closingPrice, LocalDateTime kLineOpenTime, Symbol symbol, Interval interval, int period) { 66 | avgUp = 0; 67 | avgDwn = 0; 68 | this.period = period; 69 | explanation = ""; 70 | this.kLineOpenTime = kLineOpenTime; 71 | this.symbol = symbol; 72 | this.interval = interval; 73 | init(closingPrice); 74 | } 75 | 76 | @Override 77 | public void init(List closingPrices) { 78 | prevClose = closingPrices.get(0); 79 | for (int i = 1; i < period + 1; i++) { 80 | double change = closingPrices.get(i) - prevClose; 81 | if (change > 0) { 82 | avgUp += change; 83 | } else { 84 | avgDwn += Math.abs(change); 85 | } 86 | } 87 | 88 | //Initial SMA values 89 | avgUp = avgUp / (double)period; 90 | avgDwn = avgDwn / (double)period; 91 | 92 | //Dont use latest unclosed value 93 | for (int i = period + 1; i < closingPrices.size() - 1; i++) { 94 | update(closingPrices.get(i)); 95 | } 96 | } 97 | 98 | /** 99 | * get 返回 RSI 具体值 100 | * 101 | * @return 102 | */ 103 | @Override 104 | public double get() { 105 | double rsi = 100 - 100.0 / (1 + avgUp / avgDwn); 106 | return new BigDecimal(rsi).setScale(2, RoundingMode.DOWN).doubleValue(); 107 | } 108 | 109 | @Override 110 | public double getTemp(double newPrice) { 111 | double change = newPrice - prevClose; 112 | double tempUp; 113 | double tempDwn; 114 | if (change > 0) { 115 | tempUp = (avgUp * (period - 1) + change) / (double)period; 116 | tempDwn = (avgDwn * (period - 1)) / (double)period; 117 | } else { 118 | tempDwn = (avgDwn * (period - 1) + Math.abs(change)) / (double)period; 119 | tempUp = (avgUp * (period - 1)) / (double)period; 120 | } 121 | return 100 - 100.0 / (1 + tempUp / tempDwn); 122 | } 123 | 124 | @Override 125 | public void update(double newPrice) { 126 | double change = newPrice - prevClose; 127 | if (change > 0) { 128 | avgUp = (avgUp * (period - 1) + change) / (double)period; 129 | avgDwn = (avgDwn * (period - 1)) / (double)period; 130 | } else { 131 | avgUp = (avgUp * (period - 1)) / (double)period; 132 | avgDwn = (avgDwn * (period - 1) + Math.abs(change)) / (double)period; 133 | } 134 | prevClose = newPrice; 135 | } 136 | 137 | /** 138 | * 各指标数相加,>=2 就应该买入!!! <= -2 就应该卖出 !!! 139 | * @param newPrice 140 | * @return 141 | */ 142 | @Override 143 | public int check(double newPrice) { 144 | double temp = getTemp(newPrice); 145 | if (temp < OVER_SELL_MIN) { 146 | return 2; 147 | } 148 | if (temp < OVER_SELL_MAX) { 149 | return 1; 150 | } 151 | if (temp > OVER_BUY_MAX) { 152 | return -2; 153 | } 154 | if (temp > OVER_BUY_MIN) { 155 | return -1; 156 | } 157 | explanation = ""; 158 | return 0; 159 | } 160 | 161 | @Override 162 | public String getExplanation() { 163 | return explanation; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.1 9 | 10 | 11 | com.aixi 12 | lv 13 | 0.0.1-SNAPSHOT 14 | lv 15 | Demo project for Spring Boot 16 | 17 | 1.8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | 27 | 28 | 29 | io.springfox 30 | springfox-boot-starter 31 | 3.0.0 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-configuration-processor 37 | true 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | true 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | junit 51 | junit 52 | test 53 | 54 | 55 | 56 | com.alibaba 57 | fastjson 58 | 1.2.79 59 | 60 | 61 | 62 | 63 | org.apache.commons 64 | commons-lang3 65 | 3.9 66 | 67 | 68 | 69 | 70 | org.apache.lucene 71 | lucene-core 72 | 8.11.1 73 | 74 | 75 | 76 | commons-io 77 | commons-io 78 | 2.6 79 | 80 | 81 | 82 | org.apache.commons 83 | commons-collections4 84 | 4.4 85 | 86 | 87 | 88 | com.google.guava 89 | guava 90 | 31.0-jre 91 | 92 | 93 | 94 | mysql 95 | mysql-connector-java 96 | 5.1.47 97 | 98 | 99 | 100 | org.simplejavamail 101 | simple-java-mail 102 | 6.7.5 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | maven-war-plugin 111 | 3.2.0 112 | 113 | false 114 | UTF-8 115 | 116 | 117 | 118 | 119 | org.springframework.boot 120 | spring-boot-maven-plugin 121 | 122 | 123 | 124 | org.projectlombok 125 | lombok 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/BackTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.model.constant.Interval; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.KLine; 12 | import com.aixi.lv.service.BackTestReadService; 13 | import com.google.common.collect.Lists; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.boot.context.event.ApplicationReadyEvent; 16 | import org.springframework.context.ApplicationListener; 17 | import org.springframework.stereotype.Component; 18 | 19 | /** 20 | * @author Js 21 | * 22 | * 回测配置 23 | */ 24 | @Component 25 | @Slf4j 26 | public class BackTestConfig implements ApplicationListener { 27 | 28 | @Resource 29 | BackTestReadService backTestReadService; 30 | 31 | public static final Integer INIT_BACK_TEST_AMOUNT = 10000; 32 | 33 | public static final String BACK_TEST_ACCOUNT_NAME = "BACK_TEST_ACCOUNT:"; 34 | 35 | /** 36 | * 回测数据 37 | */ 38 | public static final Map> 39 | BACK_TEST_DATA_MAP = new HashMap<>(); 40 | 41 | /** 42 | * 是否开启回测 43 | */ 44 | public static final Boolean OPEN = Boolean.TRUE; 45 | 46 | /** 47 | * 是否开启灵活币种账户回测 48 | */ 49 | public static final Boolean SWITCH_ACCOUNT_OPEN = Boolean.FALSE; 50 | 51 | /** 52 | * 是否开启 1分钟级 K线 (默认 关闭) 53 | */ 54 | public static Boolean MINUTE_ONE_K_LINE_OPEN = Boolean.TRUE; 55 | 56 | @Override 57 | public void onApplicationEvent(ApplicationReadyEvent event) { 58 | 59 | try { 60 | 61 | if (OPEN) { 62 | 63 | log.warn(" START | 加载回测数据"); 64 | 65 | this.loadSome(); 66 | //this.loadAll(); 67 | 68 | log.warn(" FINISH | 加载回测数据"); 69 | 70 | } 71 | 72 | } catch (Exception e) { 73 | log.error("BackTestConfig 异常", e); 74 | throw e; 75 | } 76 | } 77 | 78 | /** 79 | * 回测数据 Map key 80 | * 81 | * @param symbol 82 | * @param interval 83 | * @return 84 | */ 85 | public static String key(Symbol symbol, Interval interval) { 86 | return symbol.getCode() + interval.getCode(); 87 | } 88 | 89 | /** 90 | * 回测账户-所有账户 91 | */ 92 | private void loadAll() { 93 | 94 | for (Symbol symbol : Symbol.values()) { 95 | this.loadData(symbol); 96 | } 97 | 98 | } 99 | 100 | /** 101 | * 回测账户-指定币种 102 | */ 103 | private void loadSome() { 104 | 105 | List list = Lists.newArrayList(); 106 | list.add(Symbol.DOGEUSDT); 107 | list.add(Symbol.SHIBUSDT); 108 | list.add(Symbol.LUNAUSDT); 109 | list.add(Symbol.NEARUSDT); 110 | list.add(Symbol.PEOPLEUSDT); 111 | list.add(Symbol.SOLUSDT); 112 | list.add(Symbol.GMTUSDT); 113 | list.add(Symbol.BTCUSDT); 114 | list.add(Symbol.APEUSDT); 115 | 116 | for (Symbol symbol : list) { 117 | this.loadData(symbol); 118 | } 119 | 120 | //this.loadMinuteOne(); 121 | 122 | } 123 | 124 | /** 125 | * 初始化数据 126 | * 127 | * @param symbol 128 | */ 129 | private void loadData(Symbol symbol) { 130 | 131 | if (!symbol.getBackFlag()) { 132 | return; 133 | } 134 | 135 | //BACK_TEST_DATA_MAP.put(key(symbol, Interval.DAY_1), 136 | // backTestReadService.readFromFile(symbol, Interval.DAY_1)); 137 | // 138 | //BACK_TEST_DATA_MAP.put(key(symbol, Interval.HOUR_4), 139 | // backTestReadService.readFromFile(symbol, Interval.HOUR_4)); 140 | // 141 | //BACK_TEST_DATA_MAP.put(key(symbol, Interval.HOUR_1), 142 | // backTestReadService.readFromFile(symbol, Interval.HOUR_1)); 143 | // 144 | //BACK_TEST_DATA_MAP.put(key(symbol, Interval.MINUTE_5), 145 | // backTestReadService.readFromFile(symbol, Interval.MINUTE_5)); 146 | 147 | BACK_TEST_DATA_MAP.put(key(symbol, Interval.MINUTE_1), 148 | backTestReadService.readFromFile(symbol, Interval.MINUTE_1)); 149 | } 150 | 151 | /** 152 | * 1分钟级K线 153 | */ 154 | private void loadMinuteOne() { 155 | 156 | List needMinuteOne = Lists.newArrayList(); 157 | needMinuteOne.add(Symbol.BTCUSDT); 158 | needMinuteOne.add(Symbol.ETHUSDT); 159 | needMinuteOne.add(Symbol.XRPUSDT); 160 | needMinuteOne.add(Symbol.LUNAUSDT); 161 | needMinuteOne.add(Symbol.ADAUSDT); 162 | needMinuteOne.add(Symbol.SOLUSDT); 163 | needMinuteOne.add(Symbol.DOTUSDT); 164 | needMinuteOne.add(Symbol.PEOPLEUSDT); 165 | needMinuteOne.add(Symbol.GALAUSDT); 166 | needMinuteOne.add(Symbol.MATICUSDT); 167 | needMinuteOne.add(Symbol.BNBUSDT); 168 | needMinuteOne.add(Symbol.SHIBUSDT); 169 | 170 | for (Symbol symbol : needMinuteOne) { 171 | BACK_TEST_DATA_MAP.put(key(symbol, Interval.MINUTE_1), 172 | backTestReadService.readFromFile(symbol, Interval.MINUTE_1)); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/analysis/ContractBackTestAnalysis.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.analysis; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.time.Duration; 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import com.aixi.lv.model.constant.Symbol; 12 | import com.aixi.lv.model.domain.ContractAccount; 13 | import com.aixi.lv.model.domain.MacdAccount; 14 | import com.aixi.lv.service.PriceFaceService; 15 | import com.aixi.lv.strategy.contract.PriceContractStrategy; 16 | import com.aixi.lv.util.NumUtil; 17 | import com.google.common.collect.Lists; 18 | import lombok.extern.slf4j.Slf4j; 19 | import org.apache.commons.lang3.StringUtils; 20 | import org.springframework.stereotype.Component; 21 | 22 | import static com.aixi.lv.config.BackTestConfig.OPEN; 23 | import static com.aixi.lv.config.MacdTradeConfig.THREAD_LOCAL_ACCOUNT; 24 | 25 | /** 26 | * @author Js 27 | 28 | */ 29 | @Component 30 | @Slf4j 31 | public class ContractBackTestAnalysis { 32 | 33 | public static final ThreadLocal> THREAD_LOCAL_CONTRACT_ACCOUNT = new ThreadLocal<>(); 34 | 35 | public static final BigDecimal INIT_CONTRACT_AMOUNT = new BigDecimal(10000); 36 | 37 | @Resource 38 | PriceContractStrategy priceContractStrategy; 39 | 40 | @Resource 41 | PriceFaceService priceFaceService; 42 | 43 | /** 44 | * 执行分析 45 | * 46 | * @param start 47 | * @param end 48 | */ 49 | public void doAnalysis(LocalDateTime start, LocalDateTime end) { 50 | 51 | // 初始化账户 52 | this.initContractAccount(); 53 | 54 | if (!OPEN) { 55 | throw new RuntimeException("回测状态未打开"); 56 | } 57 | 58 | if (start.getMinute() != 0 || end.getMinute() != 0) { 59 | throw new RuntimeException("回测起止时间必须是0分"); 60 | } 61 | 62 | Duration between = Duration.between(start, end); 63 | 64 | long steps = between.toMinutes() / 1; 65 | 66 | LocalDateTime natureTime = start; 67 | 68 | for (int i = 0; i <= steps; i++) { 69 | 70 | List accounts = THREAD_LOCAL_CONTRACT_ACCOUNT.get(); 71 | THREAD_LOCAL_ACCOUNT.get().setCurBackTestComputeTime(natureTime); 72 | 73 | for (ContractAccount account : accounts) { 74 | 75 | // 设置当前回测账户的自然时间 76 | account.setCurBackTestComputeTime(natureTime); 77 | 78 | } 79 | 80 | priceContractStrategy.buyDetect(accounts); 81 | 82 | priceContractStrategy.sellDetect(accounts); 83 | 84 | natureTime = natureTime.plusMinutes(1); 85 | 86 | } 87 | 88 | // 信息打印 89 | this.accountInfo(); 90 | 91 | } 92 | 93 | private void initContractAccount() { 94 | 95 | List list = Lists.newArrayList(); 96 | list.add(Symbol.DOGEUSDT); 97 | list.add(Symbol.SHIBUSDT); 98 | list.add(Symbol.LUNAUSDT); 99 | list.add(Symbol.NEARUSDT); 100 | list.add(Symbol.PEOPLEUSDT); 101 | list.add(Symbol.SOLUSDT); 102 | list.add(Symbol.GMTUSDT); 103 | list.add(Symbol.BTCUSDT); 104 | list.add(Symbol.APEUSDT); 105 | 106 | List accounts = Lists.newArrayList(); 107 | 108 | for (Symbol symbol : list) { 109 | accounts.add(this.buildAccount(symbol)); 110 | } 111 | 112 | THREAD_LOCAL_CONTRACT_ACCOUNT.set(accounts); 113 | 114 | MacdAccount macdAccount = new MacdAccount(); 115 | THREAD_LOCAL_ACCOUNT.set(macdAccount); 116 | 117 | } 118 | 119 | private ContractAccount buildAccount(Symbol symbol) { 120 | 121 | ContractAccount account = new ContractAccount(); 122 | account.setName(symbol.name()); 123 | account.setSymbol(symbol); 124 | account.setHoldAmount(INIT_CONTRACT_AMOUNT); 125 | account.setHoldQty(BigDecimal.ZERO); 126 | account.setHoldFlag(false); 127 | account.setContractSide(null); 128 | account.setBuyPrice(null); 129 | account.setProfitPrice(null); 130 | account.setLossPrice(null); 131 | account.setCurBackTestComputeTime(null); 132 | account.setBackTestTotalProfit(null); 133 | account.setBackTestTotalLoss(null); 134 | account.setBackTestProfitTimes(0); 135 | account.setBackTestLossTimes(0); 136 | 137 | return account; 138 | 139 | } 140 | 141 | /** 142 | * 打印账户信息 143 | */ 144 | private void accountInfo() { 145 | 146 | List accounts = THREAD_LOCAL_CONTRACT_ACCOUNT.get(); 147 | 148 | for (ContractAccount account : accounts) { 149 | 150 | Double curRate = account.getHoldAmount().subtract(INIT_CONTRACT_AMOUNT) 151 | .divide(INIT_CONTRACT_AMOUNT, 4, RoundingMode.HALF_DOWN) 152 | .doubleValue(); 153 | String percent = NumUtil.percent(curRate); 154 | 155 | System.out.println( 156 | String.format(" 回测结束 | %s | 增长率 %s | 当前账户总额 %s | 初始金额 %s", 157 | StringUtils.rightPad(account.getSymbol().name(), 10), 158 | StringUtils.rightPad(percent, 8), 159 | account.getHoldAmount(), INIT_CONTRACT_AMOUNT)); 160 | 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/IndicatorService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.model.constant.Interval; 10 | import com.aixi.lv.model.constant.Symbol; 11 | import com.aixi.lv.model.domain.KLine; 12 | import com.aixi.lv.model.indicator.BOLL; 13 | import com.aixi.lv.model.indicator.MACD; 14 | import com.aixi.lv.model.indicator.RSI; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.stereotype.Component; 17 | 18 | import static com.aixi.lv.config.BackTestConfig.OPEN; 19 | 20 | /** 21 | * @author Js 22 | */ 23 | @Component 24 | @Slf4j 25 | public class IndicatorService { 26 | 27 | @Resource 28 | PriceFaceService priceFaceService; 29 | 30 | /** 31 | * 查询当前时间,最近一根K线的 MACD 32 | * 33 | * @param symbol 34 | * @param interval 35 | * @return 36 | */ 37 | public MACD getMACD(Symbol symbol, Interval interval) { 38 | 39 | try { 40 | List kLines = priceFaceService.queryKLine(symbol, interval, 500); 41 | 42 | List closingPrices = kLines.stream() 43 | .map(kLine -> kLine.getClosingPrice().doubleValue()) 44 | .collect(Collectors.toList()); 45 | 46 | KLine kLine = kLines.get(kLines.size() - 2); 47 | LocalDateTime openingTime = kLine.getOpeningTime(); 48 | 49 | MACD macd = new MACD(closingPrices, openingTime, symbol, interval, 12, 26, 9); 50 | 51 | return macd; 52 | } catch (Exception e) { 53 | log.error("异常币种 = {}", symbol, e); 54 | throw e; 55 | } 56 | } 57 | 58 | /** 59 | * 根据时间查MACD 60 | * 61 | * @param symbol 62 | * @param interval 63 | * @param startTime 64 | * @param endTime 65 | * @return 66 | */ 67 | public MACD getMACDByTime(Symbol symbol, Interval interval, LocalDateTime startTime, LocalDateTime endTime) { 68 | 69 | try { 70 | 71 | List kLines = priceFaceService.queryKLineByTime(symbol, interval, 500, startTime, endTime); 72 | 73 | List closingPrices = kLines.stream() 74 | .map(kLine -> kLine.getClosingPrice().doubleValue()) 75 | .collect(Collectors.toList()); 76 | 77 | KLine kLine = kLines.get(kLines.size() - 2); 78 | LocalDateTime openingTime = kLine.getOpeningTime(); 79 | 80 | MACD macd = new MACD(closingPrices, openingTime, symbol, interval, 12, 26, 9); 81 | 82 | return macd; 83 | 84 | } catch (Exception e) { 85 | log.error("异常币种 = {}", symbol, e); 86 | throw e; 87 | } 88 | } 89 | 90 | /** 91 | * 查询当前时间,最近一根K线的 RSI 92 | * 93 | * @param symbol 94 | * @param interval 95 | * @return 96 | */ 97 | public RSI getRSI(Symbol symbol, Interval interval) { 98 | 99 | List kLines = priceFaceService.queryKLine(symbol, interval, 500); 100 | 101 | List closingPrices = kLines.stream() 102 | .map(kLine -> kLine.getClosingPrice().doubleValue()) 103 | .collect(Collectors.toList()); 104 | 105 | KLine kLine = kLines.get(kLines.size() - 2); 106 | LocalDateTime openingTime = kLine.getOpeningTime(); 107 | 108 | RSI rsi = new RSI(closingPrices, openingTime, symbol, interval, 12); 109 | 110 | return rsi; 111 | 112 | } 113 | 114 | /** 115 | * 只适用于5分钟线 116 | * 117 | * 查询当前时间,最近一根K线的 BOLL 118 | * 119 | * @param symbol 120 | * @param interval 121 | * @return 122 | */ 123 | public BOLL getBOLL(Symbol symbol, Interval interval) { 124 | 125 | if (Interval.MINUTE_5 != interval) { 126 | throw new RuntimeException(" BOLL 只支持5分钟线"); 127 | } 128 | 129 | List kLines = priceFaceService.queryKLine(symbol, interval, 50); 130 | 131 | List closingPrices = kLines.stream() 132 | .map(kLine -> kLine.getClosingPrice().doubleValue()) 133 | .collect(Collectors.toList()); 134 | 135 | KLine kLine = kLines.get(kLines.size() - 2); 136 | LocalDateTime openingTime = kLine.getOpeningTime(); 137 | 138 | BOLL boll = new BOLL(closingPrices, openingTime, symbol, interval, 20); 139 | 140 | return boll; 141 | 142 | } 143 | 144 | /** 145 | * 根据时间取 BOLL 146 | * 147 | * @param symbol 148 | * @param interval 149 | * @param startTime 150 | * @param endTime 151 | * @return 152 | */ 153 | public BOLL getBOLLByTime(Symbol symbol, Interval interval, LocalDateTime startTime, LocalDateTime endTime) { 154 | 155 | if (Interval.MINUTE_5 != interval) { 156 | throw new RuntimeException(" BOLL 只支持5分钟线"); 157 | } 158 | 159 | List kLines = priceFaceService.queryKLineByTime(symbol, interval, 500, startTime, endTime); 160 | 161 | List closingPrices = kLines.stream() 162 | .map(kLine -> kLine.getClosingPrice().doubleValue()) 163 | .collect(Collectors.toList()); 164 | 165 | KLine kLine = kLines.get(kLines.size() - 2); 166 | LocalDateTime openingTime = kLine.getOpeningTime(); 167 | 168 | BOLL boll = new BOLL(closingPrices, openingTime, symbol, interval, 20); 169 | 170 | return boll; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/config/ExchangeInfoConfig.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.config; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import javax.annotation.Resource; 9 | 10 | import com.alibaba.fastjson.JSON; 11 | import com.alibaba.fastjson.JSONObject; 12 | 13 | import com.aixi.lv.model.constant.Symbol; 14 | import com.aixi.lv.model.domain.ExchangeInfoAmountFilter; 15 | import com.aixi.lv.model.domain.ExchangeInfoPriceFilter; 16 | import com.aixi.lv.model.domain.ExchangeInfoQtyFilter; 17 | import com.aixi.lv.service.HttpService; 18 | import com.aixi.lv.util.ApiUtil; 19 | import com.google.common.collect.Lists; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.springframework.boot.context.event.ApplicationReadyEvent; 22 | import org.springframework.context.ApplicationListener; 23 | import org.springframework.context.annotation.DependsOn; 24 | import org.springframework.stereotype.Component; 25 | 26 | import static com.aixi.lv.config.BackTestConfig.OPEN; 27 | 28 | /** 29 | * @author Js 30 | * @date 2022/1/2 11:35 上午 31 | */ 32 | @Component 33 | @DependsOn("backTestConfig") 34 | @Slf4j 35 | public class ExchangeInfoConfig implements ApplicationListener { 36 | 37 | @Resource 38 | HttpService httpService; 39 | 40 | public static final Map PRICE_FILTER_MAP = new HashMap<>(); 41 | 42 | public static final Map QTY_FILTER_MAP = new HashMap<>(); 43 | 44 | public static final Map AMOUNT_FILTER_MAP = new HashMap<>(); 45 | 46 | @Override 47 | public void onApplicationEvent(ApplicationReadyEvent event) { 48 | 49 | log.warn(" START | 加载交易信息配置"); 50 | 51 | loadSome(); 52 | 53 | log.warn(" FINISH | 加载交易信息配置"); 54 | 55 | } 56 | 57 | private void loadSome() { 58 | 59 | List list = Lists.newArrayList(); 60 | list.add(Symbol.DOGEUSDT); 61 | list.add(Symbol.SHIBUSDT); 62 | list.add(Symbol.LUNAUSDT); 63 | list.add(Symbol.NEARUSDT); 64 | list.add(Symbol.PEOPLEUSDT); 65 | list.add(Symbol.SOLUSDT); 66 | list.add(Symbol.GMTUSDT); 67 | list.add(Symbol.BTCUSDT); 68 | list.add(Symbol.APEUSDT); 69 | 70 | 71 | for (Symbol symbol : list) { 72 | 73 | log.warn(" {} | 开始加载交易信息配置", symbol); 74 | this.setExchangeInfoFilter(symbol); 75 | } 76 | } 77 | 78 | private void loadAll() { 79 | 80 | for (Symbol symbol : Symbol.values()) { 81 | 82 | log.warn(" {} | 开始加载交易信息配置", symbol); 83 | this.setExchangeInfoFilter(symbol); 84 | } 85 | } 86 | 87 | private void setExchangeInfoFilter(Symbol symbol) { 88 | 89 | if (OPEN && !symbol.getBackFlag()) { 90 | return; 91 | } 92 | 93 | String url = ApiUtil.url("/api/v3/exchangeInfo"); 94 | 95 | JSONObject body = new JSONObject(); 96 | body.put("symbol", symbol.getCode()); 97 | 98 | JSONObject response = httpService.getObject(url, body); 99 | 100 | List jsonObjects = response.getJSONArray("symbols") 101 | .getJSONObject(0) 102 | .getJSONArray("filters") 103 | .toJavaList(JSONObject.class); 104 | 105 | for (JSONObject jo : jsonObjects) { 106 | 107 | // 价格配置 108 | this.setPriceFilter(symbol, jo); 109 | 110 | // 数量配置 111 | this.setQtyFilter(symbol, jo); 112 | 113 | // 金额配置 114 | this.setAmountFilter(symbol, jo); 115 | 116 | } 117 | 118 | } 119 | 120 | private void setPriceFilter(Symbol symbol, JSONObject jo) { 121 | 122 | if (jo.getString("filterType").equals("PRICE_FILTER")) { 123 | 124 | ExchangeInfoPriceFilter priceFilter = jo.toJavaObject(ExchangeInfoPriceFilter.class); 125 | 126 | if (priceFilter == null) { 127 | log.error("加载交易信息priceFilter配置错误 | symbol={}", symbol); 128 | throw new RuntimeException(" 加载交易信息priceFilter配置错误 "); 129 | } 130 | 131 | BigDecimal tickSize = priceFilter.getTickSize(); 132 | priceFilter.setPriceScale(tickSize.stripTrailingZeros().scale()); 133 | PRICE_FILTER_MAP.put(symbol, priceFilter); 134 | } 135 | } 136 | 137 | private void setQtyFilter(Symbol symbol, JSONObject jo) { 138 | 139 | if (jo.getString("filterType").equals("LOT_SIZE")) { 140 | 141 | ExchangeInfoQtyFilter qtyFilter = jo.toJavaObject(ExchangeInfoQtyFilter.class); 142 | 143 | if (qtyFilter == null) { 144 | log.error("加载交易信息qtyFilter配置错误 | symbol={}", symbol); 145 | throw new RuntimeException(" 加载交易qtyFilter信息配置错误 "); 146 | } 147 | 148 | BigDecimal stepSize = qtyFilter.getStepSize(); 149 | qtyFilter.setQtyScale(stepSize.stripTrailingZeros().scale()); 150 | QTY_FILTER_MAP.put(symbol, qtyFilter); 151 | } 152 | } 153 | 154 | private void setAmountFilter(Symbol symbol, JSONObject jo) { 155 | 156 | if (jo.getString("filterType").equals("MIN_NOTIONAL")) { 157 | 158 | ExchangeInfoAmountFilter amountFilter = jo.toJavaObject(ExchangeInfoAmountFilter.class); 159 | 160 | if (amountFilter == null) { 161 | log.error("加载交易信息amountFilter配置错误 | symbol={}", symbol); 162 | throw new RuntimeException(" 加载交易amountFilter信息配置错误 "); 163 | } 164 | 165 | AMOUNT_FILTER_MAP.put(symbol, amountFilter); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/constant/Symbol.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.constant; 2 | 3 | import java.time.LocalDate; 4 | 5 | import lombok.Getter; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | /** 9 | * @author Js 10 | * 11 | * 交易对(币种) 12 | * 13 | * 14 | * 天级MACD需要至少60天的数据 15 | * 4小时级MACD需要至少10天的数据,因此至少要上市10天后的币种才能列进来 16 | */ 17 | public enum Symbol { 18 | 19 | /************************** 主流 ****************************/ 20 | 21 | BTCUSDT("BTCUSDT", LocalDate.of(2020, 1, 1), true), 22 | 23 | ETHUSDT("ETHUSDT", LocalDate.of(2020, 1, 1), true), 24 | 25 | BNBUSDT("BNBUSDT", LocalDate.of(2020, 1, 1), true), 26 | 27 | /************************** 20倍 ****************************/ 28 | 29 | DOGEUSDT("DOGEUSDT", LocalDate.of(2020, 1, 1), true), 30 | 31 | LUNAUSDT("LUNAUSDT", LocalDate.of(2020, 8, 24), true), 32 | 33 | FTMUSDT("FTMUSDT", LocalDate.of(2020, 1, 1), true), 34 | 35 | NEARUSDT("NEARUSDT", LocalDate.of(2020, 10, 19), true), 36 | 37 | MATICUSDT("MATICUSDT", LocalDate.of(2020, 1, 1), true), 38 | 39 | SANDUSDT("SANDUSDT", LocalDate.of(2020, 8, 17), true), 40 | 41 | SOLUSDT("SOLUSDT", LocalDate.of(2020, 8, 17), true), 42 | 43 | ADAUSDT("ADAUSDT", LocalDate.of(2020, 1, 1), true), 44 | 45 | ONEUSDT("ONEUSDT", LocalDate.of(2020, 1, 1), true), 46 | 47 | MANAUSDT("MANAUSDT", LocalDate.of(2020, 8, 10), true), 48 | 49 | AXSUSDT("AXSUSDT", LocalDate.of(2020, 11, 9), true), 50 | 51 | COTIUSDT("COTIUSDT", LocalDate.of(2020, 3, 2), true), 52 | 53 | CHRUSDT("CHRUSDT", LocalDate.of(2020, 5, 11), true), 54 | 55 | AVAXUSDT("AVAXUSDT", LocalDate.of(2020, 9, 28), true), 56 | 57 | /************************** 5倍 ****************************/ 58 | 59 | ROSEUSDT("ROSEUSDT", LocalDate.of(2020, 11, 23), true), 60 | 61 | SHIBUSDT("SHIBUSDT", LocalDate.of(2021, 5, 17), true), 62 | 63 | DUSKUSDT("DUSKUSDT", LocalDate.of(2020, 1, 1), true), 64 | 65 | OMGUSDT("OMGUSDT", LocalDate.of(2020, 1, 1), true), 66 | 67 | /************************** 负增长 ****************************/ 68 | 69 | ICPUSDT("ICPUSDT", LocalDate.of(2021, 5, 17), true), 70 | 71 | SLPUSDT("SLPUSDT", LocalDate.of(2021, 5, 3), true), 72 | 73 | SUPERUSDT("SUPERUSDT", LocalDate.of(2021, 3, 29), true), 74 | 75 | /************************** 新币种-用于灵活账户 ****************************/ 76 | 77 | GALAUSDT("GALAUSDT", LocalDate.of(2021, 9, 14), true), 78 | 79 | PEOPLEUSDT("PEOPLEUSDT", LocalDate.of(2021, 12, 24), true), 80 | 81 | ACHUSDT("ACHUSDT", LocalDate.of(2022, 1, 11), true), 82 | 83 | RAREUSDT("RAREUSDT", LocalDate.of(2021, 10, 12), true), 84 | 85 | DIAUSDT("DIAUSDT", LocalDate.of(2020, 9, 14), false), 86 | 87 | ALPACAUSDT("ALPACAUSDT", LocalDate.of(2021, 8, 16), false), 88 | 89 | INJUSDT("INJUSDT", LocalDate.of(2020, 10, 26), false), 90 | 91 | IDEXUSDT("IDEXUSDT", LocalDate.of(2021, 9, 13), false), 92 | 93 | STRAXUSDT("STRAXUSDT", LocalDate.of(2020, 11, 23), false), 94 | 95 | XRPUSDT("XRPUSDT", LocalDate.of(2020, 1, 1), true), 96 | 97 | LTCUSDT("LTCUSDT", LocalDate.of(2020, 1, 1), false), 98 | 99 | BETAUSDT("BETAUSDT", LocalDate.of(2021, 10, 11), true), 100 | 101 | TRXUSDT("TRXUSDT", LocalDate.of(2020, 1, 1), true), 102 | 103 | DOTUSDT("DOTUSDT", LocalDate.of(2020, 8, 24), true), 104 | 105 | LINKUSDT("LINKUSDT", LocalDate.of(2020, 1, 1), false), 106 | 107 | ATOMUSDT("ATOMUSDT", LocalDate.of(2020, 1, 1), true), 108 | 109 | FILUSDT("FILUSDT", LocalDate.of(2020, 10, 19), false), 110 | 111 | ETCUSDT("ETCUSDT", LocalDate.of(2020, 1, 1), false), 112 | 113 | VETUSDT("VETUSDT", LocalDate.of(2020, 1, 1), false), 114 | 115 | EOSUSDT("EOSUSDT", LocalDate.of(2020, 1, 1), false), 116 | 117 | THETAUSDT("THETAUSDT", LocalDate.of(2020, 1, 1), false), 118 | 119 | NEOUSDT("NEOUSDT", LocalDate.of(2020, 1, 1), false), 120 | 121 | XLMUSDT("XLMUSDT", LocalDate.of(2020, 1, 1), false), 122 | 123 | KAVAUSDT("KAVAUSDT", LocalDate.of(2020, 1, 1), false), 124 | 125 | CRVUSDT("CRVUSDT", LocalDate.of(2020, 8, 17), false), 126 | 127 | COCOSUSDT("COCOSUSDT", LocalDate.of(2021, 1, 25), false), 128 | 129 | KLAYUSDT("KLAYUSDT", LocalDate.of(2021, 6, 28), false), 130 | 131 | OOKIUSDT("OOKIUSDT", LocalDate.of(2021, 12, 27), false), 132 | 133 | RADUSDT("RADUSDT", LocalDate.of(2021, 10, 11), false), 134 | 135 | STPTUSDT("STPTUSDT", LocalDate.of(2020, 3, 30), false), 136 | 137 | TORNUSDT("TORNUSDT", LocalDate.of(2021, 6, 14), false), 138 | 139 | KMDUSDT("KMDUSDT", LocalDate.of(2020, 8, 17), false), 140 | 141 | HBARUSDT("HBARUSDT", LocalDate.of(2020, 1, 1), false), 142 | 143 | GMTUSDT("GMTUSDT", LocalDate.of(2022, 3, 10), true), 144 | 145 | APEUSDT("APEUSDT", LocalDate.of(2022, 3, 18), true), 146 | 147 | ; 148 | 149 | Symbol(String code, LocalDate saleDate, Boolean backFlag) { 150 | this.code = code; 151 | this.saleDate = saleDate; 152 | this.backFlag = backFlag; 153 | } 154 | 155 | /** 156 | * 交易对编码 157 | */ 158 | @Getter 159 | private String code; 160 | 161 | /** 162 | * 首次交易日期,最早到2020年1月1日,因为不会回测这之前的数据 163 | */ 164 | @Getter 165 | private LocalDate saleDate; 166 | 167 | /** 168 | * 是否可用于回测 169 | * true : 可以 170 | * false : 不可用 171 | */ 172 | @Getter 173 | private Boolean backFlag; 174 | 175 | public static Symbol getByCode(String code) { 176 | 177 | for (Symbol item : Symbol.values()) { 178 | if (StringUtils.equals(item.getCode(), code)) { 179 | return item; 180 | } 181 | } 182 | 183 | throw new RuntimeException("非法 Symbol | input = " + code); 184 | 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/PriceService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import javax.annotation.Resource; 9 | 10 | import com.alibaba.fastjson.JSONArray; 11 | import com.alibaba.fastjson.JSONObject; 12 | 13 | import com.aixi.lv.model.constant.Interval; 14 | import com.aixi.lv.model.constant.Symbol; 15 | import com.aixi.lv.model.domain.KLine; 16 | import com.aixi.lv.util.ApiUtil; 17 | import com.aixi.lv.util.TimeUtil; 18 | import com.google.common.collect.Lists; 19 | import lombok.extern.slf4j.Slf4j; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * @author Js 24 | * 25 | * 行情(价格)服务 26 | */ 27 | @Component 28 | @Slf4j 29 | public class PriceService { 30 | 31 | @Resource 32 | private HttpService httpService; 33 | 34 | private static final Integer MAX_RETRY_TIMES = 5; 35 | 36 | /** 37 | * K线查询 38 | * 39 | * @param symbol 40 | * @param interval 41 | * @param limit 42 | * @return 43 | */ 44 | public List queryKLine(Symbol symbol, Interval interval, Integer limit) { 45 | 46 | Integer retryTimes = 0; 47 | 48 | while (true) { 49 | 50 | if (retryTimes >= MAX_RETRY_TIMES) { 51 | log.error("queryKLine 达到最大重试次数 | symbol= {} ", symbol); 52 | throw new RuntimeException("queryKLine 达到最大重试次数"); 53 | } 54 | 55 | try { 56 | 57 | String url = ApiUtil.url("/api/v3/klines"); 58 | 59 | JSONObject params = new JSONObject(); 60 | params.put("symbol", symbol.getCode()); 61 | params.put("interval", interval.getCode()); 62 | params.put("limit", limit); 63 | 64 | JSONArray response = httpService.getArray(url, params); 65 | 66 | List arrayLists = response.toJavaList(ArrayList.class); 67 | 68 | List kLines = Lists.newArrayList(); 69 | 70 | for (ArrayList item : arrayLists) { 71 | KLine kLine = KLine.parseList(item); 72 | kLines.add(kLine); 73 | } 74 | 75 | return kLines; 76 | 77 | } catch (Exception e) { 78 | log.error( 79 | String.format(" PriceService | queryKLine_fail | symbol=%s | interval=%s | limit=%s", symbol, 80 | interval, limit), e); 81 | retryTimes++; 82 | } 83 | 84 | } 85 | } 86 | 87 | /** 88 | * 根据时间范围查K线 89 | * 90 | * @param symbol 91 | * @param interval 92 | * @param limit 从开始时间往后数limit 93 | * @param startTime 94 | * @param endTime 95 | * @return 96 | */ 97 | public List queryKLineByTime(Symbol symbol, Interval interval, Integer limit, LocalDateTime startTime, 98 | LocalDateTime endTime) { 99 | 100 | Integer retryTimes = 0; 101 | 102 | while (true) { 103 | 104 | if (retryTimes >= MAX_RETRY_TIMES) { 105 | log.error("queryKLine 达到最大重试次数 | symbol= {} ", symbol); 106 | throw new RuntimeException("queryKLine 达到最大重试次数"); 107 | } 108 | 109 | try { 110 | 111 | String url = ApiUtil.url("/api/v3/klines"); 112 | 113 | JSONObject params = new JSONObject(); 114 | params.put("symbol", symbol.getCode()); 115 | params.put("interval", interval.getCode()); 116 | params.put("startTime", TimeUtil.localToLong(startTime)); 117 | params.put("endTime", TimeUtil.localToLong(endTime)); 118 | params.put("limit", limit); 119 | 120 | JSONArray response = httpService.getArray(url, params); 121 | 122 | List arrayLists = response.toJavaList(ArrayList.class); 123 | 124 | List kLines = Lists.newArrayList(); 125 | 126 | for (ArrayList item : arrayLists) { 127 | KLine kLine = KLine.parseList(item); 128 | kLine.setSymbol(symbol); 129 | kLines.add(kLine); 130 | } 131 | 132 | return kLines; 133 | 134 | } catch (Exception e) { 135 | log.error( 136 | String.format(" PriceService | queryKLineByTime_fail | symbol=%s | interval=%s | limit=%s", symbol, 137 | interval, 138 | limit), e); 139 | retryTimes++; 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * 查最新价格 146 | * 147 | * @param symbol 148 | * @return 149 | */ 150 | public BigDecimal queryNewPrice(Symbol symbol) { 151 | 152 | Integer retryTimes = 0; 153 | 154 | while (true) { 155 | 156 | if (retryTimes >= MAX_RETRY_TIMES) { 157 | log.error("queryKLine 达到最大重试次数 | symbol= {} ", symbol); 158 | throw new RuntimeException("queryKLine 达到最大重试次数"); 159 | } 160 | 161 | try { 162 | 163 | String url = ApiUtil.url("/api/v3/ticker/price"); 164 | 165 | JSONObject params = new JSONObject(); 166 | params.put("symbol", symbol.getCode()); 167 | 168 | JSONObject response = httpService.getObject(url, params); 169 | 170 | return response.getBigDecimal("price"); 171 | 172 | } catch (Exception e) { 173 | log.error( 174 | String.format(" PriceService | queryNewPrice_fail | symbol=%s ", symbol), e); 175 | retryTimes++; 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/profit/SecondProfitStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.profit; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.aixi.lv.manage.OrderLifeManage; 9 | import com.aixi.lv.model.constant.OrderSide; 10 | import com.aixi.lv.model.constant.OrderStatus; 11 | import com.aixi.lv.config.ProfitRateConfig; 12 | import com.aixi.lv.model.constant.Symbol; 13 | import com.aixi.lv.model.constant.TradePairStatus; 14 | import com.aixi.lv.model.domain.OrderLife; 15 | import com.aixi.lv.model.domain.TradePair; 16 | import com.aixi.lv.service.MailService; 17 | import com.aixi.lv.service.OrderService; 18 | import com.aixi.lv.service.PriceService; 19 | import com.aixi.lv.util.TimeUtil; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.apache.commons.collections4.CollectionUtils; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * @author Js 26 | * 27 | * 止盈策略 28 | * 29 | * 当探测到市场价格到达止盈点时,设置止盈卖单,分批卖出获利 30 | */ 31 | @Component 32 | @Slf4j 33 | public class SecondProfitStrategy { 34 | 35 | @Resource 36 | OrderLifeManage orderLifeManage; 37 | 38 | @Resource 39 | PriceService priceService; 40 | 41 | @Resource 42 | OrderService orderService; 43 | 44 | @Resource 45 | MailService mailService; 46 | 47 | /** 48 | * 二阶段止盈监测 49 | * 50 | * @param symbol 51 | */ 52 | public void secondProfit(Symbol symbol) { 53 | 54 | List pairList = orderLifeManage.getPairBySymbol(symbol); 55 | 56 | if (CollectionUtils.isEmpty(pairList)) { 57 | return; 58 | } 59 | 60 | // 循环处理 61 | for (TradePair pair : pairList) { 62 | 63 | OrderLife buyOrder = pair.getBuyOrder(); 64 | 65 | if (!isNeedSecondProfitCheck(pair)) { 66 | continue; 67 | } 68 | 69 | // 市场最新价 70 | BigDecimal newPrice = priceService.queryNewPrice(symbol); 71 | 72 | // 二阶段止盈价 73 | BigDecimal secondPrice = buyOrder.getTopPrice().multiply(ProfitRateConfig.SECOND_PRICE_RATE); 74 | 75 | // 市场价高于二阶段止盈价 76 | if (newPrice.compareTo(secondPrice) >= 0) { 77 | 78 | // 设置二阶段止盈 79 | this.secondProfitAction(symbol, pair, newPrice); 80 | 81 | log.info(" 二阶止盈 | 止盈Action | symbol={} | newPrice={} | secondPrice={}", 82 | symbol, newPrice, secondPrice); 83 | } else { 84 | log.info(" 二阶止盈 | 未达处理价 | symbol={} | newPrice={} | secondPrice={}", 85 | symbol, newPrice, secondPrice); 86 | } 87 | 88 | } 89 | } 90 | 91 | /** 92 | * 是否需要检查止盈 93 | * 94 | * @param pair 95 | * @return 96 | */ 97 | private Boolean isNeedSecondProfitCheck(TradePair pair) { 98 | 99 | TradePairStatus status = pair.getStatus(); 100 | 101 | if (TradePairStatus.FIRST_DONE == status 102 | || TradePairStatus.SECOND_PROFIT == status) { 103 | 104 | return Boolean.TRUE; 105 | } 106 | 107 | return Boolean.FALSE; 108 | } 109 | 110 | private void secondProfitAction(Symbol symbol, TradePair pair, BigDecimal newPrice) { 111 | 112 | OrderLife buyOrder = pair.getBuyOrder(); 113 | 114 | // 一阶段止盈单已完成 115 | if (TradePairStatus.FIRST_DONE == pair.getStatus()) { 116 | 117 | // 下二阶段止盈单 118 | this.postSecondProfitOrder(symbol, buyOrder, newPrice); 119 | return; 120 | } 121 | 122 | // 二阶段止盈单生效中 123 | if (TradePairStatus.SECOND_PROFIT == pair.getStatus()) { 124 | 125 | OrderLife secondOrder = pair.getSecondProfitOrder(); 126 | // 先查服务器状态 127 | OrderLife secondServer = orderService.queryByOrderId(symbol, secondOrder.getOrderId()); 128 | 129 | // 服务端已取消,下新的一阶段止盈单 130 | if (OrderStatus.CANCELED == secondServer.getStatus() 131 | || OrderStatus.EXPIRED == secondServer.getStatus() 132 | || OrderStatus.REJECTED == secondServer.getStatus()) { 133 | 134 | // Pair 状态更新 135 | orderLifeManage.removeSecondProfit(buyOrder.getOrderId(), secondServer); 136 | this.postSecondProfitOrder(symbol, buyOrder, newPrice); 137 | return; 138 | } 139 | 140 | // 其余状态 交给 OrderLifeTask 探测处理 141 | return; 142 | } 143 | } 144 | 145 | private void postSecondProfitOrder(Symbol symbol, OrderLife buyOrder, BigDecimal newPrice) { 146 | 147 | BigDecimal quantity = buyOrder.getExecutedQty().multiply(ProfitRateConfig.SECOND_QTY_RATE); 148 | 149 | // 二阶段止盈卖单 150 | OrderLife sellOrder = orderService.limitSellOrder(OrderSide.SELL, symbol, quantity, newPrice); 151 | 152 | sellOrder.setBuyOrderId(buyOrder.getOrderId()); 153 | 154 | // Pair 状态更新 155 | orderLifeManage.putSecondProfit(buyOrder.getOrderId(), sellOrder); 156 | 157 | String title = symbol.getCode() + " 二阶段止盈下单"; 158 | StringBuilder content = new StringBuilder(); 159 | content.append("交易对 : " + symbol.getCode()); 160 | content.append("\n"); 161 | content.append("时间 : " + TimeUtil.getCurrentTime()); 162 | content.append("\n"); 163 | content.append("当前价格 : " + newPrice); 164 | content.append("\n"); 165 | content.append("止盈价格 : " + sellOrder.getSellPrice()); 166 | content.append("\n"); 167 | content.append("买入价格 : " + buyOrder.getBuyPrice()); 168 | content.append("\n"); 169 | content.append("卖出数量 : " + quantity); 170 | content.append("\n"); 171 | content.append("卖出金额 : " + sellOrder.getSellPrice().multiply(quantity)); 172 | content.append("\n"); 173 | 174 | log.info(" 二阶段止盈策略 | {} | {}", title, content); 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/profit/FirstProfitStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.profit; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.aixi.lv.manage.OrderLifeManage; 9 | import com.aixi.lv.model.constant.OrderSide; 10 | import com.aixi.lv.model.constant.OrderStatus; 11 | import com.aixi.lv.config.ProfitRateConfig; 12 | import com.aixi.lv.model.constant.Symbol; 13 | import com.aixi.lv.model.constant.TradePairStatus; 14 | import com.aixi.lv.model.domain.OrderLife; 15 | import com.aixi.lv.model.domain.TradePair; 16 | import com.aixi.lv.service.MailService; 17 | import com.aixi.lv.service.OrderService; 18 | import com.aixi.lv.service.PriceService; 19 | import com.aixi.lv.util.TimeUtil; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.apache.commons.collections4.CollectionUtils; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * @author Js 26 | * 27 | * 止盈策略 28 | * 29 | * 当探测到市场价格到达止盈点时,设置止盈卖单,分批卖出获利 30 | */ 31 | @Component 32 | @Slf4j 33 | public class FirstProfitStrategy { 34 | 35 | @Resource 36 | OrderLifeManage orderLifeManage; 37 | 38 | @Resource 39 | PriceService priceService; 40 | 41 | @Resource 42 | OrderService orderService; 43 | 44 | @Resource 45 | MailService mailService; 46 | 47 | /** 48 | * 快到箱顶了就能卖 49 | */ 50 | private static final BigDecimal TOP_SELL_RATE = new BigDecimal("0.998"); 51 | 52 | /** 53 | * 一阶段止盈监测 54 | * 55 | * @param symbol 56 | */ 57 | public void firstProfit(Symbol symbol) { 58 | 59 | List pairList = orderLifeManage.getPairBySymbol(symbol); 60 | 61 | if (CollectionUtils.isEmpty(pairList)) { 62 | return; 63 | } 64 | 65 | // 循环处理 66 | for (TradePair pair : pairList) { 67 | 68 | OrderLife buyOrder = pair.getBuyOrder(); 69 | 70 | if (!isNeedFirstProfitCheck(pair)) { 71 | continue; 72 | } 73 | 74 | BigDecimal newPrice = priceService.queryNewPrice(symbol); 75 | BigDecimal topSellPrice = buyOrder.getTopPrice().multiply(TOP_SELL_RATE); 76 | 77 | // 市场价高于箱顶价格 78 | if (newPrice.compareTo(topSellPrice) >= 0) { 79 | 80 | // 设置一阶段止盈 81 | this.firstProfitAction(symbol, pair, newPrice); 82 | 83 | log.info(" 一阶止盈 | 止盈Action | symbol={} | newPrice={} | topSellPrice={}", 84 | symbol, newPrice, topSellPrice); 85 | } else { 86 | log.info(" 一阶止盈 | 未达处理价 | symbol={} | newPrice={} | topSellPrice={}", 87 | symbol, newPrice, topSellPrice); 88 | } 89 | 90 | } 91 | } 92 | 93 | /** 94 | * 是否需要检查止盈 95 | * 96 | * @param pair 97 | * @return 98 | */ 99 | private Boolean isNeedFirstProfitCheck(TradePair pair) { 100 | 101 | TradePairStatus status = pair.getStatus(); 102 | 103 | if (TradePairStatus.ALREADY == status 104 | || TradePairStatus.FIRST_PROFIT == status) { 105 | 106 | return Boolean.TRUE; 107 | } 108 | 109 | return Boolean.FALSE; 110 | } 111 | 112 | public void firstProfitAction(Symbol symbol, TradePair pair, BigDecimal newPrice) { 113 | 114 | OrderLife buyOrder = pair.getBuyOrder(); 115 | 116 | // 买单已完成 117 | if (TradePairStatus.ALREADY == pair.getStatus()) { 118 | 119 | // 下一阶段止盈单 120 | this.postFirstProfitOrder(symbol, buyOrder, newPrice); 121 | return; 122 | } 123 | 124 | // 一阶段止盈单生效中 125 | if (TradePairStatus.FIRST_PROFIT == pair.getStatus()) { 126 | 127 | OrderLife firstOrder = pair.getFirstProfitOrder(); 128 | // 先查服务器状态 129 | OrderLife firstServer = orderService.queryByOrderId(symbol, firstOrder.getOrderId()); 130 | 131 | // 服务端已取消,下新的一阶段止盈单 132 | if (OrderStatus.CANCELED == firstServer.getStatus() 133 | || OrderStatus.EXPIRED == firstServer.getStatus() 134 | || OrderStatus.REJECTED == firstServer.getStatus()) { 135 | 136 | // Pair 状态更新 137 | orderLifeManage.removeFirstProfit(buyOrder.getOrderId(), firstServer); 138 | this.postFirstProfitOrder(symbol, buyOrder, newPrice); 139 | return; 140 | } 141 | 142 | // 其余状态 交给 OrderLifeTask 探测处理 143 | return; 144 | 145 | } 146 | } 147 | 148 | private void postFirstProfitOrder(Symbol symbol, OrderLife buyOrder, BigDecimal newPrice) { 149 | 150 | BigDecimal quantity = buyOrder.getExecutedQty().multiply(ProfitRateConfig.FIRST_QTY_RATE); 151 | 152 | // 一阶段止盈卖单 153 | OrderLife sellOrder = orderService.limitSellOrder(OrderSide.SELL, symbol, quantity, newPrice); 154 | 155 | sellOrder.setBuyOrderId(buyOrder.getOrderId()); 156 | 157 | // Pair 状态更新 158 | orderLifeManage.putFirstProfit(buyOrder.getOrderId(), sellOrder); 159 | 160 | String title = symbol.getCode() + " 一阶段止盈下单"; 161 | StringBuilder content = new StringBuilder(); 162 | content.append("交易对 : " + symbol.getCode()); 163 | content.append("\n"); 164 | content.append("时间 : " + TimeUtil.getCurrentTime()); 165 | content.append("\n"); 166 | content.append("当前价格 : " + newPrice); 167 | content.append("\n"); 168 | content.append("止盈价格 : " + sellOrder.getSellPrice()); 169 | content.append("\n"); 170 | content.append("买入价格 : " + buyOrder.getBuyPrice()); 171 | content.append("\n"); 172 | content.append("卖出数量 : " + quantity); 173 | content.append("\n"); 174 | content.append("卖出金额 : " + sellOrder.getSellPrice().multiply(quantity)); 175 | content.append("\n"); 176 | 177 | log.info(" 一阶段止盈策略 | {} | {}", title, content); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/contract/PriceContractStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.contract; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.util.List; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.aixi.lv.model.constant.ContractSide; 10 | import com.aixi.lv.model.constant.Interval; 11 | import com.aixi.lv.model.constant.Symbol; 12 | import com.aixi.lv.model.domain.ContractAccount; 13 | import com.aixi.lv.model.domain.KLine; 14 | import com.aixi.lv.service.BackTestOrderService; 15 | import com.aixi.lv.service.PriceFaceService; 16 | import lombok.extern.slf4j.Slf4j; 17 | import org.springframework.stereotype.Component; 18 | 19 | /** 20 | * @author Js 21 | * 22 | * 合约价格策略 23 | */ 24 | @Component 25 | @Slf4j 26 | public class PriceContractStrategy { 27 | 28 | @Resource 29 | PriceFaceService priceFaceService; 30 | 31 | @Resource 32 | BackTestOrderService backTestOrderService; 33 | 34 | public static final Double LONG_PROFIT = 1.03; 35 | 36 | public static final Double LONG_LOSS = 0.9; 37 | 38 | public static final Double SHORT_PROFIT = 0.98; 39 | 40 | public static final Double SHORT_LOSS = 1.1; 41 | 42 | /** 43 | * 开仓探测 44 | * 45 | * @param accounts 46 | */ 47 | public void buyDetect(List accounts) { 48 | 49 | for (ContractAccount account : accounts) { 50 | 51 | if (account.getHoldFlag()) { 52 | continue; 53 | } 54 | 55 | Symbol symbol = account.getSymbol(); 56 | 57 | if (isPriceRise(symbol)) { 58 | // 下多单 59 | this.longOrder(account); 60 | continue; 61 | } 62 | 63 | if (isPriceFall(symbol)) { 64 | // 下空单 65 | this.shortOrder(account); 66 | continue; 67 | } 68 | 69 | } 70 | 71 | } 72 | 73 | /** 74 | * 平仓探测 75 | * 76 | * @param accounts 77 | */ 78 | public void sellDetect(List accounts) { 79 | 80 | for (ContractAccount account : accounts) { 81 | 82 | if (!account.getHoldFlag()) { 83 | continue; 84 | } 85 | 86 | Symbol symbol = account.getSymbol(); 87 | ContractSide contractSide = account.getContractSide(); 88 | BigDecimal profitPrice = account.getProfitPrice(); 89 | BigDecimal lossPrice = account.getLossPrice(); 90 | BigDecimal newPrice = priceFaceService.queryNewPrice(symbol); 91 | 92 | if (ContractSide.LONG == contractSide) { 93 | if (newPrice.compareTo(profitPrice) > 0) { 94 | backTestOrderService.sellContract(account); 95 | } 96 | if (newPrice.compareTo(lossPrice) < 0) { 97 | backTestOrderService.sellContract(account); 98 | } 99 | } 100 | 101 | if (ContractSide.SHORT == contractSide) { 102 | if (newPrice.compareTo(profitPrice) < 0) { 103 | backTestOrderService.sellContract(account); 104 | } 105 | if (newPrice.compareTo(lossPrice) > 0) { 106 | backTestOrderService.sellContract(account); 107 | } 108 | } 109 | 110 | } 111 | } 112 | 113 | /** 114 | * 做多 115 | * 116 | * @param account 117 | */ 118 | private void longOrder(ContractAccount account) { 119 | 120 | backTestOrderService.buyContract(account, ContractSide.LONG); 121 | } 122 | 123 | /** 124 | * 做空 125 | * 126 | * @param account 127 | */ 128 | private void shortOrder(ContractAccount account) { 129 | 130 | //Symbol symbol = account.getSymbol(); 131 | //if (Symbol.GMTUSDT == symbol 132 | // || Symbol.APEUSDT == symbol) { 133 | // return; 134 | //} 135 | 136 | backTestOrderService.buyContract(account, ContractSide.SHORT); 137 | } 138 | 139 | /** 140 | * 价格是否快速上涨 141 | * 142 | * 15分钟内涨幅大于3% 143 | * 144 | * @param symbol 145 | * @return 146 | */ 147 | private Boolean isPriceRise(Symbol symbol) { 148 | 149 | List kLines = priceFaceService.queryKLine(symbol, Interval.MINUTE_1, 15); 150 | 151 | BigDecimal newPrice = priceFaceService.queryNewPrice(symbol); 152 | 153 | BigDecimal beforePrice = kLines.get(0).getClosingPrice(); 154 | 155 | //for (KLine kLine : kLines) { 156 | // 157 | // BigDecimal closingPrice = kLine.getClosingPrice(); 158 | // 159 | // if (newPrice.divide(closingPrice, 8, RoundingMode.DOWN).doubleValue() > 1.03) { 160 | // return true; 161 | // } 162 | //} 163 | 164 | if (newPrice.divide(beforePrice, 8, RoundingMode.DOWN).doubleValue() > 1.029) { 165 | return true; 166 | } 167 | 168 | return false; 169 | 170 | } 171 | 172 | /** 173 | * 价格是否快速下跌 174 | * 175 | * 15分钟内跌幅大于3% 176 | * 177 | * @param symbol 178 | * @return 179 | */ 180 | private Boolean isPriceFall(Symbol symbol) { 181 | 182 | List kLines = priceFaceService.queryKLine(symbol, Interval.MINUTE_1, 15); 183 | 184 | BigDecimal newPrice = priceFaceService.queryNewPrice(symbol); 185 | 186 | BigDecimal beforePrice = kLines.get(0).getClosingPrice(); 187 | 188 | //for (KLine kLine : kLines) { 189 | // 190 | // BigDecimal closingPrice = kLine.getClosingPrice(); 191 | // 192 | // if (newPrice.divide(closingPrice, 8, RoundingMode.UP).doubleValue() < 0.96) { 193 | // return true; 194 | // } 195 | //} 196 | 197 | if (newPrice.divide(beforePrice, 8, RoundingMode.UP).doubleValue() < 0.97) { 198 | return true; 199 | } 200 | 201 | return false; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/profit/ThirdProfitStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.profit; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.aixi.lv.manage.OrderLifeManage; 9 | import com.aixi.lv.model.constant.OrderSide; 10 | import com.aixi.lv.model.constant.OrderStatus; 11 | import com.aixi.lv.config.ProfitRateConfig; 12 | import com.aixi.lv.model.constant.Symbol; 13 | import com.aixi.lv.model.constant.TradePairStatus; 14 | import com.aixi.lv.model.domain.OrderLife; 15 | import com.aixi.lv.model.domain.TradePair; 16 | import com.aixi.lv.service.MailService; 17 | import com.aixi.lv.service.OrderService; 18 | import com.aixi.lv.service.PriceService; 19 | import com.aixi.lv.util.TimeUtil; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.apache.commons.collections4.CollectionUtils; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * @author Js 26 | * 27 | * 止盈策略 28 | * 29 | * 当探测到市场价格到达止盈点时,设置止盈卖单,分批卖出获利 30 | */ 31 | @Component 32 | @Slf4j 33 | public class ThirdProfitStrategy { 34 | 35 | @Resource 36 | OrderLifeManage orderLifeManage; 37 | 38 | @Resource 39 | PriceService priceService; 40 | 41 | @Resource 42 | OrderService orderService; 43 | 44 | @Resource 45 | MailService mailService; 46 | 47 | /** 48 | * 三阶段止盈监测 49 | * 50 | * @param symbol 51 | */ 52 | public void thirdProfit(Symbol symbol) { 53 | 54 | List pairList = orderLifeManage.getPairBySymbol(symbol); 55 | 56 | if (CollectionUtils.isEmpty(pairList)) { 57 | return; 58 | } 59 | 60 | // 循环处理 61 | for (TradePair pair : pairList) { 62 | 63 | OrderLife buyOrder = pair.getBuyOrder(); 64 | 65 | if (!isNeedThirdProfitCheck(pair)) { 66 | continue; 67 | } 68 | 69 | // 市场最新价 70 | BigDecimal newPrice = priceService.queryNewPrice(symbol); 71 | 72 | // 三阶段止盈价 73 | BigDecimal thirdPrice = buyOrder.getTopPrice().multiply(ProfitRateConfig.THIRD_PRICE_RATE); 74 | 75 | // 市场价高于三阶段止盈价 76 | if (newPrice.compareTo(thirdPrice) >= 0) { 77 | 78 | // 设置三阶段止盈 79 | this.thirdProfitAction(symbol, pair, newPrice); 80 | 81 | log.info(" 三阶止盈 | 止盈Action | symbol={} | newPrice={} | thirdPrice={}", 82 | symbol, newPrice, thirdPrice); 83 | } else { 84 | log.info(" 三阶止盈 | 未达处理价 | symbol={} | newPrice={} | thirdPrice={}", 85 | symbol, newPrice, thirdPrice); 86 | } 87 | 88 | } 89 | } 90 | 91 | /** 92 | * 是否需要检查止盈 93 | * 94 | * @param pair 95 | * @return 96 | */ 97 | private Boolean isNeedThirdProfitCheck(TradePair pair) { 98 | 99 | TradePairStatus status = pair.getStatus(); 100 | 101 | if (TradePairStatus.SECOND_DONE == status 102 | || TradePairStatus.THIRD_PROFIT == status) { 103 | 104 | return Boolean.TRUE; 105 | } 106 | 107 | return Boolean.FALSE; 108 | } 109 | 110 | private void thirdProfitAction(Symbol symbol, TradePair pair, BigDecimal newPrice) { 111 | 112 | OrderLife buyOrder = pair.getBuyOrder(); 113 | 114 | // 二阶段止盈单已完成 115 | if (TradePairStatus.SECOND_DONE == pair.getStatus()) { 116 | 117 | // 下三阶段止盈单 118 | this.postThirdProfitOrder(symbol, pair, newPrice); 119 | return; 120 | } 121 | 122 | // 三阶段止盈单生效中 123 | if (TradePairStatus.THIRD_PROFIT == pair.getStatus()) { 124 | 125 | OrderLife thirdOrder = pair.getThirdProfitOrder(); 126 | // 先查服务器状态 127 | OrderLife thirdServer = orderService.queryByOrderId(symbol, thirdOrder.getOrderId()); 128 | 129 | // 服务端已取消,下新的一阶段止盈单 130 | if (OrderStatus.CANCELED == thirdServer.getStatus() 131 | || OrderStatus.EXPIRED == thirdServer.getStatus() 132 | || OrderStatus.REJECTED == thirdServer.getStatus()) { 133 | 134 | // Pair 状态更新 135 | orderLifeManage.removeThirdProfit(buyOrder.getOrderId(), thirdServer); 136 | this.postThirdProfitOrder(symbol, pair, newPrice); 137 | return; 138 | } 139 | 140 | // 其余状态 交给 OrderLifeTask 探测处理 141 | return; 142 | } 143 | } 144 | 145 | private void postThirdProfitOrder(Symbol symbol, TradePair pair, BigDecimal newPrice) { 146 | 147 | OrderLife buyOrder = pair.getBuyOrder(); 148 | BigDecimal firstQty = pair.getFirstProfitOrder().getExecutedQty(); 149 | BigDecimal secondQty = pair.getSecondProfitOrder().getExecutedQty(); 150 | 151 | BigDecimal quantity = buyOrder.getExecutedQty().subtract(firstQty).subtract(secondQty); 152 | 153 | // 三阶段止盈卖单 154 | OrderLife sellOrder = orderService.limitSellOrder(OrderSide.SELL, symbol, quantity, newPrice); 155 | 156 | sellOrder.setBuyOrderId(buyOrder.getOrderId()); 157 | 158 | // 更新 Pair 状态 159 | orderLifeManage.putThirdProfit(buyOrder.getOrderId(), sellOrder); 160 | 161 | String title = symbol.getCode() + " 三阶段止盈下单"; 162 | StringBuilder content = new StringBuilder(); 163 | content.append("交易对 : " + symbol.getCode()); 164 | content.append("\n"); 165 | content.append("时间 : " + TimeUtil.getCurrentTime()); 166 | content.append("\n"); 167 | content.append("当前价格 : " + newPrice); 168 | content.append("\n"); 169 | content.append("止盈价格 : " + sellOrder.getSellPrice()); 170 | content.append("\n"); 171 | content.append("买入价格 : " + buyOrder.getBuyPrice()); 172 | content.append("\n"); 173 | content.append("卖出数量 : " + quantity); 174 | content.append("\n"); 175 | content.append("卖出金额 : " + sellOrder.getSellPrice().multiply(quantity)); 176 | content.append("\n"); 177 | 178 | log.info(" 三阶段止盈策略 | {} | {}", title, content); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/model/indicator/MACD.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.model.indicator; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.RoundingMode; 5 | import java.text.DecimalFormat; 6 | import java.text.NumberFormat; 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | 10 | import com.aixi.lv.model.constant.Interval; 11 | import com.aixi.lv.model.constant.Symbol; 12 | import lombok.Getter; 13 | 14 | /** 15 | * @author Js 16 | * 17 | * MACD (移动平均收敛散度) 18 | * 19 | * Default setting in crypto are period of 9, short 12 and long 26. 20 | * MACD = 12 EMA - 26 EMA and compare to 9 period of MACD value. 21 | */ 22 | public class MACD implements Indicator { 23 | 24 | @Getter 25 | private double currentDIF; // DIF 26 | 27 | @Getter 28 | private double currentDEM; // DEM 29 | 30 | private final EMA shortEMA; //Will be the EMA object for shortEMA- 31 | private final EMA longEMA; //Will be the EMA object for longEMA. 32 | private final int period; //Only value that has to be calculated in setInitial. 33 | private final double multiplier; 34 | private final int periodDifference; 35 | private String explanation; 36 | public static double SIGNAL_CHANGE = 0.25; 37 | 38 | private double lastTick; 39 | 40 | /** 41 | * 指标对应的 K线 开盘时间 (这根K线已经走完了) 42 | */ 43 | @Getter 44 | private LocalDateTime kLineOpenTime; 45 | 46 | @Getter 47 | private Symbol symbol; 48 | 49 | @Getter 50 | private Interval interval; 51 | 52 | public MACD(List closingPrices, int shortPeriod, int longPeriod, int signalPeriod) { 53 | this.shortEMA = new EMA(closingPrices, shortPeriod, 54 | true); //true, because history is needed in MACD calculations. 55 | this.longEMA = new EMA(closingPrices, longPeriod, true); //true for the same reasons. 56 | this.period = signalPeriod; 57 | this.multiplier = 2.0 / (double)(signalPeriod + 1); 58 | this.periodDifference = longPeriod - shortPeriod; 59 | explanation = ""; 60 | init(closingPrices); //initializing the calculations to get current MACD and signal line. 61 | } 62 | 63 | public MACD(List closingPrices, LocalDateTime kLineOpenTime, Symbol symbol, Interval interval, 64 | int shortPeriod, int longPeriod, int signalPeriod) { 65 | 66 | this.shortEMA = new EMA(closingPrices, shortPeriod, 67 | true); //true, because history is needed in MACD calculations. 68 | this.longEMA = new EMA(closingPrices, longPeriod, true); //true for the same reasons. 69 | this.period = signalPeriod; 70 | this.multiplier = 2.0 / (double)(signalPeriod + 1); 71 | this.periodDifference = longPeriod - shortPeriod; 72 | explanation = ""; 73 | this.kLineOpenTime = kLineOpenTime; 74 | this.symbol = symbol; 75 | this.interval = interval; 76 | init(closingPrices); //initializing the calculations to get current MACD and signal line. 77 | } 78 | 79 | /** 80 | * 这才是MACD ,计算的是排除当前K线,上一根K线时期的MACD 81 | * 82 | * @return 83 | */ 84 | @Override 85 | public double get() { 86 | return currentDIF - currentDEM; 87 | } 88 | 89 | public double getCurrentMACD() { 90 | return this.get(); 91 | } 92 | 93 | @Override 94 | public double getTemp(double newPrice) { 95 | //temporary values 96 | double longTemp = longEMA.getTemp(newPrice); 97 | double shortTemp = shortEMA.getTemp(newPrice); 98 | 99 | double tempDIF = shortTemp - longTemp; 100 | double tempDEM = tempDIF * multiplier + currentDEM * (1 - multiplier); 101 | return tempDIF - tempDEM; //Getting the difference between the two signals. 102 | } 103 | 104 | @Override 105 | public void init(List closingPrices) { 106 | //Initial signal line 107 | //i = longEMA.getPeriod(); because the sizes of shortEMA and longEMA are different. 108 | for (int i = longEMA.getPeriod(); i < longEMA.getPeriod() + period; i++) { 109 | //i value with shortEMA gets changed to compensate the list size difference 110 | currentDIF = shortEMA.getEMAhistory().get(i + periodDifference) - longEMA.getEMAhistory().get(i); 111 | currentDEM += currentDIF; 112 | } 113 | currentDEM = currentDEM / (double)period; 114 | 115 | //Everything after the first calculation of signal line. 116 | for (int i = longEMA.getPeriod() + period; i < longEMA.getEMAhistory().size(); i++) { 117 | currentDIF = shortEMA.getEMAhistory().get(i + periodDifference) - longEMA.getEMAhistory().get(i); 118 | currentDEM = currentDIF * multiplier + currentDEM * (1 - multiplier); 119 | } 120 | 121 | lastTick = get(); 122 | } 123 | 124 | @Override 125 | public void update(double newPrice) { 126 | //Updating the EMA values before updating MACD and Signal line. 127 | lastTick = get(); 128 | shortEMA.update(newPrice); 129 | longEMA.update(newPrice); 130 | currentDIF = shortEMA.get() - longEMA.get(); 131 | currentDEM = currentDIF * multiplier + currentDEM * (1 - multiplier); 132 | } 133 | 134 | /** 135 | * MACD 变化的斜率超过 0.25 并且 当前MACD<0 ,则买入 136 | * 137 | * @param newPrice 138 | * @return 139 | */ 140 | @Override 141 | public int check(double newPrice) { 142 | // change 指当前价格输入后, MACD 指标变化的斜率 143 | double change = (getTemp(newPrice) - lastTick) / Math.abs(lastTick); 144 | if (change > MACD.SIGNAL_CHANGE && get() < 0) { 145 | NumberFormat PERCENT_FORMAT = new DecimalFormat("0.000%"); 146 | explanation = "MACD histogram grew by " + PERCENT_FORMAT.format(change); 147 | return 1; 148 | } 149 | /*if (change < -MACD.change) { 150 | explanation = "MACD histogram fell by " + Formatter.formatPercent(change); 151 | return -1; 152 | }*/ 153 | explanation = ""; 154 | return 0; 155 | } 156 | 157 | @Override 158 | public String getExplanation() { 159 | return explanation; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/BackTestReadService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.time.LocalDateTime; 8 | import java.util.Comparator; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | 14 | import com.alibaba.fastjson.JSON; 15 | 16 | import com.aixi.lv.model.constant.Interval; 17 | import com.aixi.lv.model.constant.Symbol; 18 | import com.aixi.lv.model.domain.BackTestData; 19 | import com.aixi.lv.model.domain.KLine; 20 | import com.aixi.lv.util.RamUtil; 21 | import com.google.common.collect.Lists; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.apache.commons.lang3.StringUtils; 24 | import org.springframework.stereotype.Component; 25 | 26 | /** 27 | * @author Js 28 | * 29 | * 读txt加载K线数据 30 | */ 31 | @Component 32 | @Slf4j 33 | public class BackTestReadService { 34 | 35 | public static final String FILE_PREFIX = "/Users/jinsong/project/money/backtest/"; 36 | 37 | /** 38 | * 加载天级K线 39 | * 40 | * @param symbol 41 | */ 42 | public Map day(Symbol symbol) { 43 | 44 | return this.readFromFile(symbol, Interval.DAY_1); 45 | 46 | } 47 | 48 | public Map minuteFive(Symbol symbol) { 49 | 50 | return this.readFromFile(symbol, Interval.MINUTE_5); 51 | 52 | } 53 | 54 | public Map hourOne(Symbol symbol) { 55 | 56 | return this.readFromFile(symbol, Interval.HOUR_1); 57 | 58 | } 59 | 60 | public Map hourFour(Symbol symbol) { 61 | 62 | return this.readFromFile(symbol, Interval.HOUR_4); 63 | 64 | } 65 | 66 | public Map readFromFile(Symbol symbol, Interval interval) { 67 | 68 | String currency = StringUtils.substringBefore(symbol.getCode(), "USDT"); 69 | String fileName = FILE_PREFIX + currency + "/" + symbol.getCode() + "_" + interval.getCode() + ".txt"; 70 | 71 | BufferedReader reader = null; 72 | 73 | Map map = new HashMap<>(); 74 | 75 | try { 76 | 77 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName))); 78 | String content; 79 | 80 | do { 81 | content = reader.readLine(); 82 | BackTestData back = JSON.parseObject(content, BackTestData.class); 83 | 84 | if (content == null || back == null) { 85 | System.out.println(symbol.getCode() + interval.getCode() + " 内存大小 : " + RamUtil.getRamSize(map)); 86 | return map; 87 | } 88 | 89 | KLine kLine = KLine.builder() 90 | .symbol(symbol) 91 | .openingTime(back.getOpeningTime()) 92 | .closingPrice(back.getClosingPrice()) 93 | .tradingVolume(back.getTradingVolume()) 94 | .build(); 95 | 96 | map.put(kLine.getOpeningTime().toString(), kLine); 97 | 98 | } while (content != null); 99 | 100 | } catch (Exception e) { 101 | log.error("readFromFile", e); 102 | } finally { 103 | try { 104 | reader.close(); 105 | } catch (IOException e) { 106 | log.error("readFromFile", e); 107 | } 108 | } 109 | 110 | System.out.println(symbol.getCode() + interval.getCode() + " 内存大小 : " + RamUtil.getRamSize(map)); 111 | return map; 112 | 113 | } 114 | 115 | public void checkData(Symbol symbol, Interval interval) { 116 | 117 | String currency = StringUtils.substringBefore(symbol.getCode(), "USDT"); 118 | String fileName = FILE_PREFIX + currency + "/" + symbol.getCode() + "_" + interval.getCode() + ".txt"; 119 | 120 | BufferedReader reader = null; 121 | 122 | List list = Lists.newArrayList(); 123 | 124 | try { 125 | 126 | reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName))); 127 | String content; 128 | 129 | do { 130 | content = reader.readLine(); 131 | BackTestData back = JSON.parseObject(content, BackTestData.class); 132 | 133 | if (content == null || back == null) { 134 | break; 135 | } 136 | 137 | KLine kLine = KLine.builder() 138 | .symbol(symbol) 139 | .openingTime(back.getOpeningTime()) 140 | .closingPrice(back.getClosingPrice()) 141 | .tradingVolume(back.getTradingVolume()) 142 | .build(); 143 | 144 | list.add(kLine); 145 | 146 | } while (content != null); 147 | 148 | } catch (Exception e) { 149 | log.error("readFromFile", e); 150 | } finally { 151 | try { 152 | reader.close(); 153 | } catch (IOException e) { 154 | log.error("readFromFile", e); 155 | } 156 | } 157 | 158 | List sortedList = list.stream() 159 | .sorted(Comparator.comparing(KLine::getOpeningTime)) 160 | .collect(Collectors.toList()); 161 | 162 | LocalDateTime tempTime = sortedList.get(0).getOpeningTime(); 163 | 164 | // 最后一个不检查了 165 | for (int i = 0; i < sortedList.size()-1; i++) { 166 | if (!tempTime.isEqual(sortedList.get(i).getOpeningTime())) { 167 | throw new RuntimeException("检查出异常" + symbol + " " + interval + "缺失的时间是" + tempTime); 168 | } 169 | tempTime = this.nextTime(tempTime, interval); 170 | } 171 | } 172 | 173 | private LocalDateTime nextTime(LocalDateTime curTime, Interval interval) { 174 | 175 | if (Interval.MINUTE_1 == interval) { 176 | return curTime.plusMinutes(1); 177 | } 178 | 179 | if (Interval.MINUTE_5 == interval) { 180 | return curTime.plusMinutes(5); 181 | } 182 | 183 | if (Interval.HOUR_1 == interval) { 184 | return curTime.plusHours(1); 185 | } 186 | 187 | if (Interval.HOUR_4 == interval) { 188 | return curTime.plusHours(4); 189 | } 190 | 191 | if (Interval.DAY_1 == interval) { 192 | return curTime.plusDays(1); 193 | } 194 | 195 | throw new RuntimeException("不支持的类型"); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/service/BackTestAppendService.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.service; 2 | 3 | import java.io.File; 4 | import java.nio.charset.Charset; 5 | import java.time.LocalDateTime; 6 | 7 | import javax.annotation.Resource; 8 | 9 | import com.alibaba.fastjson.JSON; 10 | 11 | import com.aixi.lv.model.constant.Interval; 12 | import com.aixi.lv.model.constant.Symbol; 13 | import com.aixi.lv.model.domain.BackTestData; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.apache.commons.io.input.ReversedLinesFileReader; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.springframework.stereotype.Component; 18 | 19 | import static com.aixi.lv.config.BackTestConfig.OPEN; 20 | import static com.aixi.lv.service.BackTestReadService.FILE_PREFIX; 21 | 22 | /** 23 | * @author Js 24 | * 25 | * 追加写数据 26 | */ 27 | @Component 28 | @Slf4j 29 | public class BackTestAppendService { 30 | 31 | @Resource 32 | BackTestReadService backTestReadService; 33 | 34 | @Resource 35 | BackTestWriteService backTestWriteService; 36 | 37 | /** 38 | * 追加回测数据 39 | * 40 | * @param symbol 41 | */ 42 | public void appendData(Symbol symbol) { 43 | 44 | if (OPEN) { 45 | throw new RuntimeException("追加数据失败,回测开关处于开启状态"); 46 | } 47 | 48 | if (!symbol.getBackFlag()) { 49 | return; 50 | } 51 | 52 | LocalDateTime finalTime = LocalDateTime.now(); 53 | 54 | this.day(symbol, finalTime); 55 | backTestReadService.checkData(symbol, Interval.DAY_1); 56 | 57 | this.hourFour(symbol, finalTime); 58 | backTestReadService.checkData(symbol, Interval.HOUR_4); 59 | 60 | this.hourOne(symbol, finalTime); 61 | backTestReadService.checkData(symbol, Interval.HOUR_1); 62 | 63 | this.minuteFive(symbol, finalTime); 64 | backTestReadService.checkData(symbol, Interval.MINUTE_5); 65 | 66 | } 67 | 68 | public void appendMinuteOne(Symbol symbol){ 69 | 70 | if (OPEN) { 71 | throw new RuntimeException("追加数据失败,回测开关处于开启状态"); 72 | } 73 | 74 | if (!symbol.getBackFlag()) { 75 | return; 76 | } 77 | 78 | LocalDateTime finalTime = LocalDateTime.now(); 79 | 80 | this.minuteOne(symbol, finalTime); 81 | backTestReadService.checkData(symbol, Interval.MINUTE_1); 82 | } 83 | 84 | /** 85 | * 天级K线 86 | * 87 | * @param symbol 88 | * @param finalTime 89 | */ 90 | public void day(Symbol symbol, LocalDateTime finalTime) { 91 | 92 | LocalDateTime lastOpenTime = this.readLastLine(symbol, Interval.DAY_1); 93 | 94 | if (!lastOpenTime.isBefore(finalTime)) { 95 | return; 96 | } 97 | 98 | LocalDateTime initTime = lastOpenTime.plusDays(1); 99 | 100 | backTestWriteService.day(symbol, initTime, finalTime); 101 | 102 | } 103 | 104 | /** 105 | * 4小时级K线 106 | * 107 | * @param symbol 108 | * @param finalTime 109 | */ 110 | public void hourFour(Symbol symbol, LocalDateTime finalTime) { 111 | 112 | LocalDateTime lastOpenTime = this.readLastLine(symbol, Interval.HOUR_4); 113 | 114 | if (!lastOpenTime.isBefore(finalTime)) { 115 | return; 116 | } 117 | 118 | LocalDateTime initTime = lastOpenTime.plusHours(4); 119 | 120 | backTestWriteService.hourFour(symbol, initTime, finalTime); 121 | 122 | } 123 | 124 | /** 125 | * 1小时级K线 126 | * 127 | * @param symbol 128 | * @param finalTime 129 | */ 130 | public void hourOne(Symbol symbol, LocalDateTime finalTime) { 131 | 132 | LocalDateTime lastOpenTime = this.readLastLine(symbol, Interval.HOUR_1); 133 | 134 | if (!lastOpenTime.isBefore(finalTime)) { 135 | return; 136 | } 137 | 138 | LocalDateTime initTime = lastOpenTime.plusHours(1); 139 | 140 | backTestWriteService.hourOne(symbol, initTime, finalTime); 141 | } 142 | 143 | /** 144 | * 5分钟级K线 145 | * 146 | * @param symbol 147 | * @param finalTime 148 | */ 149 | public void minuteFive(Symbol symbol, LocalDateTime finalTime) { 150 | 151 | LocalDateTime lastOpenTime = this.readLastLine(symbol, Interval.MINUTE_5); 152 | 153 | if (!lastOpenTime.isBefore(finalTime)) { 154 | return; 155 | } 156 | 157 | if (lastOpenTime.isAfter(LocalDateTime.now().minusMinutes(30))) { 158 | return; 159 | } 160 | 161 | LocalDateTime initTime = lastOpenTime.plusMinutes(5); 162 | 163 | backTestWriteService.minuteFive(symbol, initTime, finalTime); 164 | } 165 | 166 | /** 167 | * 1分钟级K线 168 | * 169 | * @param symbol 170 | * @param finalTime 171 | */ 172 | public void minuteOne(Symbol symbol, LocalDateTime finalTime) { 173 | 174 | LocalDateTime lastOpenTime = this.readLastLine(symbol, Interval.MINUTE_1); 175 | 176 | if (!lastOpenTime.isBefore(finalTime)) { 177 | return; 178 | } 179 | 180 | if (lastOpenTime.isAfter(LocalDateTime.now().minusMinutes(30))) { 181 | return; 182 | } 183 | 184 | LocalDateTime initTime = lastOpenTime.plusMinutes(1); 185 | 186 | backTestWriteService.minuteOne(symbol, initTime, finalTime); 187 | } 188 | 189 | /** 190 | * 读取最后一行数据 191 | * 192 | * @param symbol 193 | * @param interval 194 | * @return 195 | */ 196 | public LocalDateTime readLastLine(Symbol symbol, Interval interval) { 197 | 198 | String currency = StringUtils.substringBefore(symbol.getCode(), "USDT"); 199 | String fileName = FILE_PREFIX + currency + "/" + symbol.getCode() + "_" + interval.getCode() + ".txt"; 200 | 201 | File file = new File(fileName); 202 | 203 | String lastLine; 204 | 205 | Integer times = 0; 206 | Integer maxTimes = 1; 207 | 208 | try (ReversedLinesFileReader reversedLinesReader = new ReversedLinesFileReader(file, 209 | Charset.forName("UTF-8"))) { 210 | 211 | while (true) { 212 | 213 | if (times > maxTimes) { 214 | throw new RuntimeException("末尾空行超出限制"); 215 | } 216 | 217 | lastLine = reversedLinesReader.readLine(); 218 | 219 | if (lastLine == null || StringUtils.isEmpty(lastLine)) { 220 | times++; 221 | continue; 222 | } 223 | 224 | BackTestData back = JSON.parseObject(lastLine, BackTestData.class); 225 | 226 | return back.getOpeningTime(); 227 | } 228 | 229 | } catch (Exception e) { 230 | log.error("readLastLine error, msg:{}", e.getMessage(), e); 231 | throw new RuntimeException(e); 232 | } 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/com/aixi/lv/strategy/defense/DefenseStrategy.java: -------------------------------------------------------------------------------- 1 | package com.aixi.lv.strategy.defense; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | import javax.annotation.Resource; 7 | 8 | import com.alibaba.fastjson.JSON; 9 | 10 | import com.aixi.lv.manage.OrderLifeManage; 11 | import com.aixi.lv.model.constant.OrderSide; 12 | import com.aixi.lv.model.constant.OrderStatus; 13 | import com.aixi.lv.model.constant.Symbol; 14 | import com.aixi.lv.model.constant.TradePairStatus; 15 | import com.aixi.lv.model.domain.OrderLife; 16 | import com.aixi.lv.model.domain.TradePair; 17 | import com.aixi.lv.service.MailService; 18 | import com.aixi.lv.service.OrderService; 19 | import com.aixi.lv.service.PriceService; 20 | import com.aixi.lv.util.TimeUtil; 21 | import lombok.extern.slf4j.Slf4j; 22 | import org.apache.commons.collections4.CollectionUtils; 23 | import org.springframework.stereotype.Component; 24 | 25 | /** 26 | * @author Js 27 | * 28 | * 防守策略 29 | * 30 | * 当探测到市场价高于买入价时,随时调整止损单价格 31 | */ 32 | @Component 33 | @Slf4j 34 | public class DefenseStrategy { 35 | 36 | @Resource 37 | OrderLifeManage orderLifeManage; 38 | 39 | @Resource 40 | PriceService priceService; 41 | 42 | @Resource 43 | OrderService orderService; 44 | 45 | @Resource 46 | MailService mailService; 47 | 48 | /** 49 | * 防守线,大于 买卖合计手续费 0.15% 50 | */ 51 | private static final BigDecimal DEFENSE_PRICE_RATE = new BigDecimal("1.002"); 52 | 53 | /** 54 | * 防守检查 55 | * 56 | * @param symbol 57 | */ 58 | public void defense(Symbol symbol) { 59 | 60 | List pairList = orderLifeManage.getPairBySymbol(symbol); 61 | 62 | if (CollectionUtils.isEmpty(pairList)) { 63 | return; 64 | } 65 | 66 | // 循环处理 67 | for (TradePair pair : pairList) { 68 | 69 | OrderLife buyOrder = pair.getBuyOrder(); 70 | 71 | if (!isNeedCheck(pair)) { 72 | continue; 73 | } 74 | 75 | BigDecimal newPrice = priceService.queryNewPrice(symbol); 76 | 77 | // TODO 防守线的逻辑还需要再理一理 78 | // 达到防守线 79 | if (newPrice.compareTo(buyOrder.getBuyPrice().multiply(DEFENSE_PRICE_RATE)) >= 0) { 80 | // 下止损单 81 | this.defenseAction(symbol, pair, newPrice); 82 | } 83 | 84 | } 85 | 86 | } 87 | 88 | private Boolean isNeedCheck(TradePair pair) { 89 | 90 | TradePairStatus status = pair.getStatus(); 91 | 92 | if (TradePairStatus.ALREADY == status 93 | || TradePairStatus.LOSS == status) { 94 | 95 | return Boolean.TRUE; 96 | } 97 | 98 | return Boolean.FALSE; 99 | } 100 | 101 | private void defenseAction(Symbol symbol, TradePair pair, BigDecimal newPrice) { 102 | 103 | OrderLife buyOrder = pair.getBuyOrder(); 104 | 105 | // 买单已完成 106 | if (pair.getStatus() == TradePairStatus.ALREADY) { 107 | 108 | this.postDefenseOrder(symbol, buyOrder, newPrice); 109 | return; 110 | } 111 | 112 | // 止损单生效中 113 | if (pair.getStatus() == TradePairStatus.LOSS) { 114 | 115 | OrderLife lossOrder = pair.getLossOrder(); 116 | 117 | // 先查服务器状态 118 | OrderLife lossServer = orderService.queryByOrderId(symbol, lossOrder.getOrderId()); 119 | 120 | // 已止损 121 | if (OrderStatus.FILLED == lossServer.getStatus()) { 122 | // Pair 状态更新 【终态】 123 | TradePair pairFromManage = orderLifeManage.getPairById(buyOrder.getOrderId()); 124 | mailService.tradeDoneEmail("止损成交", pairFromManage); 125 | orderLifeManage.removeTradePair(buyOrder.getOrderId()); 126 | return; 127 | } 128 | 129 | // 部分成交,等下一次检查 130 | if (OrderStatus.PARTIALLY_FILLED == lossServer.getStatus()) { 131 | return; 132 | } 133 | 134 | // 服务端已取消止损单,下新的 止损单 135 | if (OrderStatus.CANCELED == lossServer.getStatus() 136 | || OrderStatus.EXPIRED == lossServer.getStatus() 137 | || OrderStatus.REJECTED == lossServer.getStatus()) { 138 | 139 | // Pair 状态更新 140 | orderLifeManage.removeLossOrder(buyOrder.getOrderId(), lossServer); 141 | this.postDefenseOrder(symbol, buyOrder, newPrice); 142 | } 143 | 144 | // 未成交,取消未完成的止损卖单,移除Map,后续重新止损 145 | if (OrderStatus.NEW == lossServer.getStatus()) { 146 | try { 147 | // 如果最新价大于原卖价 148 | if (newPrice.compareTo(lossServer.getSellPrice()) > 0) { 149 | // 取消原单 150 | orderService.cancelByOrderId(symbol, lossServer.getOrderId()); 151 | // Pair 状态更新 152 | orderLifeManage.removeLossOrder(buyOrder.getOrderId(), lossServer); 153 | // 下止损单 154 | this.postDefenseOrder(symbol, buyOrder, newPrice); 155 | } 156 | } catch (Exception e) { 157 | log.error( 158 | String.format(" defenseAction | 撤销订单失败 | lossOrderServer=%s", 159 | JSON.toJSONString(lossServer)), e); 160 | } 161 | } 162 | 163 | return; 164 | } 165 | 166 | } 167 | 168 | /** 169 | * 下止损单 170 | * 171 | * @param symbol 172 | * @param buyOrder 173 | * @param newPrice 174 | */ 175 | private void postDefenseOrder(Symbol symbol, OrderLife buyOrder, BigDecimal newPrice) { 176 | 177 | BigDecimal quantity = buyOrder.getExecutedQty(); 178 | BigDecimal stopPrice = newPrice.multiply(new BigDecimal("0.9996")); 179 | BigDecimal sellPrice = newPrice.multiply(new BigDecimal("0.9996")); 180 | 181 | // 止损限价卖单 182 | // TODO JS 这里的 stopPrice 可能会因为过于接近市场价而抛异常 183 | OrderLife sellOrder = orderService.stopLossOrderV2(OrderSide.SELL, symbol, quantity, stopPrice, sellPrice); 184 | 185 | sellOrder.setBuyOrderId(buyOrder.getOrderId()); 186 | 187 | // Pair 状态更新 188 | orderLifeManage.putLossOrder(buyOrder.getOrderId(), sellOrder); 189 | 190 | String title = symbol.getCode() + " 止损下单"; 191 | StringBuilder content = new StringBuilder(); 192 | content.append("交易对 : " + symbol.getCode()); 193 | content.append("\n"); 194 | content.append("时间 : " + TimeUtil.getCurrentTime()); 195 | content.append("\n"); 196 | content.append("当前价格 : " + newPrice); 197 | content.append("\n"); 198 | content.append("止损价格 : " + sellPrice); 199 | content.append("\n"); 200 | content.append("买入价格 : " + buyOrder.getBuyPrice()); 201 | content.append("\n"); 202 | content.append("数量 : " + quantity); 203 | content.append("\n"); 204 | content.append("卖出金额 : " + sellPrice.multiply(quantity)); 205 | content.append("\n"); 206 | 207 | log.info(" 止损策略 | {} | {}", title, content); 208 | } 209 | 210 | } 211 | --------------------------------------------------------------------------------