├── .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 |
15 |
16 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------