opt
This commit is contained in:
parent
19872409c5
commit
0d73531bb1
0
.cursor/commands/aipexbase-mcp-server.md
Normal file
0
.cursor/commands/aipexbase-mcp-server.md
Normal file
@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.baogutang.admin.domain.IphoneProductDto;
|
||||
import top.baogutang.admin.utils.DingTalkMsgPushUtils;
|
||||
@ -41,7 +42,7 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
|
||||
@Value("${baogutang.apple.country_code:cn}")
|
||||
private String countryCode;
|
||||
|
||||
@Value("${baogutang.apple.device_code:15-pro}")
|
||||
@Value("${baogutang.apple.device_code:17-pro}")
|
||||
private String deviceCode;
|
||||
|
||||
@Value("${baogutang.apple.location:'上海 上海 闵行区'}")
|
||||
@ -59,6 +60,25 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
|
||||
@Resource
|
||||
private DingTalkMsgPushUtils dingTalkMsgPushUtils;
|
||||
|
||||
@Scheduled(cron = "0 0/1 * * * ? ")
|
||||
public void appleInventoryMonitor() {
|
||||
if (!Boolean.TRUE.equals(appleInventoryMonitorSwitch)) {
|
||||
log.info(">>>>>>>>>>apple inventory monitor switch closed!<<<<<<<<<<");
|
||||
return;
|
||||
}
|
||||
// 获取设备信息
|
||||
List<IphoneProductDto> products = iphoneProductParserUtils.getProducts(deviceCode, countryCode);
|
||||
//监视机型型号
|
||||
products.forEach(product -> {
|
||||
this.doMonitor(product);
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
log.error(">>>>>>>>>>apple inventory monitor error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doMonitor(IphoneProductDto product) {
|
||||
|
||||
|
||||
@ -83,7 +103,7 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
|
||||
JSONObject pickupMessage = responseJsonObject.getJSONObject("body").getJSONObject("content").getJSONObject("pickupMessage");
|
||||
JSONArray stores = pickupMessage.getJSONArray("stores");
|
||||
if (stores == null) {
|
||||
log.debug(pickupMessage.toString());
|
||||
// log.info(pickupMessage.toString());
|
||||
return;
|
||||
}
|
||||
if (stores.isEmpty()) {
|
||||
|
||||
@ -66,6 +66,7 @@
|
||||
<div class="col-md-7" style="padding:0;">
|
||||
<div style="padding:10px;font-size:16px;border-bottom:solid 1px #ddd;" class="navi">
|
||||
<a href="#" class="tip zip" title="压缩" data-placement="bottom"><i class="fa fa-database"></i></a>
|
||||
<a href="#" class="tip zipCopy" title="压缩并复制" data-placement="bottom"><i class="fa fa-files-o"></i></a>
|
||||
<a href="#" class="tip xml" title="转XML" data-placement="bottom"><i class="fa fa-file-excel-o"></i></a>
|
||||
<a href="#" class="tip shown" title="显示行号" data-placement="bottom"><i
|
||||
class="glyphicon glyphicon-sort-by-order"></i></a>
|
||||
@ -264,9 +265,80 @@
|
||||
var timestamp = new Date().getTime();
|
||||
saveAs(blob, "format." + timestamp + ".json");
|
||||
});
|
||||
$('.copy').click(function () {
|
||||
$('.copy').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 取右侧展示的文本(格式化或压缩后的)
|
||||
var text = $('#json-target').innerText().replace(' ', ' ');
|
||||
|
||||
// 设置到隐藏按钮上
|
||||
$('#hiddenCopyBtn').attr('data-clipboard-text', text);
|
||||
|
||||
// 触发隐藏按钮的点击,调用 ClipboardJS
|
||||
$('#hiddenCopyBtn').click();
|
||||
});
|
||||
$('.zipCopy').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 如果没有内容,直接提示
|
||||
if (!$.trim($('#json-src').val())) {
|
||||
$.message && $.message({
|
||||
type: 'error',
|
||||
message: '没有内容可压缩'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 触发压缩按钮逻辑,确保 current_json_str 已是压缩 JSON
|
||||
$('.zip').click();
|
||||
|
||||
// current_json_str 在 keyup 和 zip 逻辑里已经维护成压缩字符串
|
||||
var text = current_json_str;
|
||||
|
||||
// 如果 current_json_str 是错误提示(带 HTML),则不复制
|
||||
if (/span/i.test(text)) {
|
||||
$.message && $.message({
|
||||
type: 'error',
|
||||
message: '当前 JSON 解析失败,不能压缩复制'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置到隐藏按钮
|
||||
$('#hiddenCopyBtn').attr('data-clipboard-text', text);
|
||||
|
||||
// 触发复制
|
||||
$('#hiddenCopyBtn').click();
|
||||
} catch (e) {
|
||||
$.message && $.message({
|
||||
type: 'error',
|
||||
message: '压缩并复制失败:' + e
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 统一复制出口:绑定到隐藏按钮
|
||||
var clipboard = new Clipboard('#hiddenCopyBtn');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
console.log('复制成功');
|
||||
$.message && $.message({
|
||||
type: 'success',
|
||||
message: '已复制到剪贴板'
|
||||
});
|
||||
e.clearSelection();
|
||||
});
|
||||
|
||||
clipboard.on('error', function(e) {
|
||||
console.log('复制失败', e);
|
||||
$.message && $.message({
|
||||
type: 'error',
|
||||
message: '复制失败,请手动复制'
|
||||
});
|
||||
});
|
||||
var clipboard = new Clipboard('.copy');
|
||||
$('#json-src').keyup();
|
||||
</script>
|
||||
<footer style="padding:30px;text-align:center;font-family:'JetBrains Mono NL', Monaco, monospace;position:relative;">
|
||||
@ -274,5 +346,7 @@
|
||||
href="http://beian.miit.gov.cn">皖ICP备2024050571号</a>All Rights Reserved.
|
||||
<p></p>
|
||||
</footer>
|
||||
<button id="hiddenCopyBtn" style="display:none;" data-clipboard-text=""></button>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
84
baogutang-strategy/pom.xml
Normal file
84
baogutang-strategy/pom.xml
Normal file
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>top.baogutang</groupId>
|
||||
<artifactId>baogutang-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>baogutang-strategy</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>top.baogutang</groupId>
|
||||
<artifactId>baogutang-common</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.alibaba.cloud</groupId>-->
|
||||
<!-- <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.alibaba.cloud</groupId>-->
|
||||
<!-- <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun.openservices</groupId>
|
||||
<artifactId>aliyun-log-logback-appender</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok(可选) -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- 统计/数学 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,131 @@
|
||||
package top.baogutang.strategy;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import top.baogutang.strategy.alerts.AlertChannel;
|
||||
import top.baogutang.strategy.alerts.CompositeAlertChannel;
|
||||
import top.baogutang.strategy.alerts.ConsoleAlertChannel;
|
||||
import top.baogutang.strategy.alerts.DingTalkRobotChannel;
|
||||
import top.baogutang.strategy.commons.configs.AppProperties;
|
||||
import top.baogutang.strategy.infra.OkxPublicClient;
|
||||
import top.baogutang.strategy.infra.OkxWebSocketClient;
|
||||
import top.baogutang.strategy.models.MultiSymbolMarketDataHub;
|
||||
import top.baogutang.strategy.models.PortfolioRiskManager;
|
||||
import top.baogutang.strategy.models.RiskManager;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
import top.baogutang.strategy.strategies.MultiSymbolStrategyEngine;
|
||||
import top.baogutang.strategy.strategies.Strategy;
|
||||
import top.baogutang.strategy.strategies.StrategyFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
public class AppApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AppApplication.class, args);
|
||||
log.info("<<<<<<<<<<baogutang strategy started>>>>>>>>>>");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OkHttpClient okHttpClient() {
|
||||
return new OkHttpClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IndicatorService indicatorService() {
|
||||
return new IndicatorService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MultiSymbolMarketDataHub multiSymbolMarketDataHub() {
|
||||
return new MultiSymbolMarketDataHub(800);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OkxPublicClient okxPublicClient(OkHttpClient c, ObjectMapper m) {
|
||||
return new OkxPublicClient(c, m);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OkxWebSocketClient okxWebSocketClient(OkHttpClient c, ObjectMapper m) {
|
||||
return new OkxWebSocketClient(c, m);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RiskManager riskManager() {
|
||||
return new RiskManager(3.0);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PortfolioRiskManager portfolioRiskManager(AppProperties props) {
|
||||
double p = props.getAccount().getPortfolioMaxOpenRiskPercent();
|
||||
if (p <= 0) return null;
|
||||
return new PortfolioRiskManager(props.getAccount().getEquity(), p);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, List<Strategy>> strategies(AppProperties props) {
|
||||
return StrategyFactory.build(props);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, Double> symbolRiskOverride(AppProperties props) {
|
||||
Map<String, Double> map = new HashMap<String, Double>();
|
||||
for (AppProperties.SymbolConfig sc : props.getSymbols()) {
|
||||
if (sc.getRiskPercentOverride() != null) {
|
||||
map.put(sc.getInstId(), sc.getRiskPercentOverride());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AlertChannel alertChannel(OkHttpClient http, ObjectMapper mapper, AppProperties props) {
|
||||
List<AlertChannel> list = new ArrayList<AlertChannel>();
|
||||
list.add(new ConsoleAlertChannel());
|
||||
if (props.getAlert() != null && props.getAlert().getDingTalk() != null) {
|
||||
List<AppProperties.Alert.DingTalkRobot> robots = props.getAlert().getDingTalk().getRobots();
|
||||
if (robots != null && !robots.isEmpty()) {
|
||||
List<DingTalkRobotChannel.RobotConfig> rcList = new ArrayList<DingTalkRobotChannel.RobotConfig>();
|
||||
for (AppProperties.Alert.DingTalkRobot r : robots) {
|
||||
DingTalkRobotChannel.RobotConfig rc = new DingTalkRobotChannel.RobotConfig();
|
||||
rc.name = r.getName();
|
||||
rc.webhook = r.getWebhook();
|
||||
rc.signEnable = r.getSecurity() != null && r.getSecurity().isEnable();
|
||||
rc.secret = r.getSecurity() != null ? r.getSecurity().getSecret() : null;
|
||||
rc.atMobiles = r.getAtMobiles();
|
||||
rc.atAll = r.isAtAll();
|
||||
rcList.add(rc);
|
||||
}
|
||||
list.add(new DingTalkRobotChannel(http, mapper, rcList));
|
||||
}
|
||||
}
|
||||
return new CompositeAlertChannel(list);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MultiSymbolStrategyEngine multiSymbolStrategyEngine(MultiSymbolMarketDataHub hub,
|
||||
IndicatorService ind,
|
||||
RiskManager riskManager,
|
||||
AlertChannel alertChannel,
|
||||
Map<String, List<Strategy>> strategies,
|
||||
AppProperties props,
|
||||
PortfolioRiskManager portfolioRisk,
|
||||
Map<String, Double> symbolRiskOverride) {
|
||||
return new MultiSymbolStrategyEngine(
|
||||
hub, ind, riskManager, alertChannel, strategies,
|
||||
props.getAccount().getEquity(), props.getAccount().getRiskPercent(),
|
||||
portfolioRisk, symbolRiskOverride);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package top.baogutang.strategy.alerts;
|
||||
|
||||
|
||||
import top.baogutang.strategy.models.PositionPlan;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
|
||||
public interface AlertChannel {
|
||||
|
||||
void send(Signal s, PositionPlan plan);
|
||||
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package top.baogutang.strategy.alerts;
|
||||
|
||||
import top.baogutang.strategy.models.PositionPlan;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeAlertChannel implements AlertChannel {
|
||||
private final List<AlertChannel> list;
|
||||
|
||||
public CompositeAlertChannel(List<AlertChannel> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public void send(Signal s, PositionPlan plan) {
|
||||
for (AlertChannel c : list) {
|
||||
try {
|
||||
c.send(s, plan);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package top.baogutang.strategy.alerts;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.baogutang.strategy.models.PositionPlan;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Slf4j
|
||||
public class ConsoleAlertChannel implements AlertChannel {
|
||||
|
||||
public void send(Signal s, PositionPlan plan) {
|
||||
|
||||
log.info("[SIGNAL] {} {} {} entry={} stop={} targets={} qty={} lev={} note={}",
|
||||
s.getSymbol(),
|
||||
s.getStrategy(),
|
||||
s.getDirection(),
|
||||
s.getEntryRef(),
|
||||
s.getStopRef(),
|
||||
Arrays.toString(s.getTargets()),
|
||||
plan.getQuantity(),
|
||||
plan.getLeverage(),
|
||||
s.getComment());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package top.baogutang.strategy.alerts;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import okhttp3.*;
|
||||
import top.baogutang.strategy.models.PositionPlan;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.utils.SignUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DingTalkRobotChannel implements AlertChannel {
|
||||
|
||||
public static class RobotConfig {
|
||||
public String name;
|
||||
public String webhook;
|
||||
public boolean signEnable;
|
||||
public String secret;
|
||||
public List<String> atMobiles;
|
||||
public boolean atAll;
|
||||
}
|
||||
|
||||
private final OkHttpClient http;
|
||||
private final ObjectMapper mapper;
|
||||
private final List<RobotConfig> robots;
|
||||
|
||||
public DingTalkRobotChannel(OkHttpClient http, ObjectMapper mapper, List<RobotConfig> robots) {
|
||||
this.http = http;
|
||||
this.mapper = mapper;
|
||||
this.robots = robots;
|
||||
}
|
||||
|
||||
public void send(Signal s, PositionPlan plan) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("【交易信号】\n");
|
||||
sb.append("品种: ").append(s.getSymbol()).append("\n");
|
||||
sb.append("策略: ").append(s.getStrategy()).append("\n");
|
||||
sb.append("方向: ").append(s.getDirection()).append(" 等级:").append(s.getGrade()).append("\n");
|
||||
sb.append("参考入场: ").append(String.format("%.10f", s.getEntryRef())).append("\n");
|
||||
sb.append("止损: ").append(String.format("%.10f", s.getStopRef())).append("\n");
|
||||
sb.append("目标: ").append(java.util.Arrays.toString(s.getTargets())).append("\n");
|
||||
sb.append("风险金额: ").append(String.format("%.2f", plan.getRiskAmount())).append("\n");
|
||||
sb.append("计划数量: ").append(String.format("%.4f", plan.getQuantity())).append(" 杠杆:")
|
||||
.append(String.format("%.2f", plan.getLeverage())).append("\n");
|
||||
sb.append("备注: ").append(s.getComment()).append("\n");
|
||||
sb.append("时间: ").append(s.getTimestamp()).append("\n");
|
||||
|
||||
String text = sb.toString();
|
||||
|
||||
for (final RobotConfig rc : robots) {
|
||||
try {
|
||||
long ts = System.currentTimeMillis();
|
||||
String finalWebhook = rc.webhook;
|
||||
if (rc.signEnable) {
|
||||
String sign = SignUtil.sign(ts, rc.secret);
|
||||
finalWebhook = finalWebhook + "×tamp=" + ts + "&sign=" + sign;
|
||||
}
|
||||
ObjectNode root = mapper.createObjectNode();
|
||||
root.put("msgtype", "text");
|
||||
ObjectNode textNode = mapper.createObjectNode();
|
||||
textNode.put("content", text);
|
||||
root.set("text", textNode);
|
||||
ObjectNode atNode = mapper.createObjectNode();
|
||||
if (rc.atMobiles != null && !rc.atMobiles.isEmpty()) {
|
||||
atNode.set("atMobiles", mapper.valueToTree(rc.atMobiles));
|
||||
} else {
|
||||
atNode.putArray("atMobiles");
|
||||
}
|
||||
atNode.put("isAtAll", rc.atAll);
|
||||
root.set("at", atNode);
|
||||
|
||||
RequestBody body = RequestBody.create(mapper.writeValueAsBytes(root),
|
||||
MediaType.parse("application/json"));
|
||||
Request req = new Request.Builder().url(finalWebhook).post(body).build();
|
||||
http.newCall(req).enqueue(new Callback() {
|
||||
public void onFailure(Call call, java.io.IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
public void onResponse(Call call, Response response) {
|
||||
response.close();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package top.baogutang.strategy.backtests;
|
||||
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.*;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
import top.baogutang.strategy.strategies.Strategy;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class BacktestEngine {
|
||||
public BacktestResult run(List<Strategy> strategies,
|
||||
Map<Timeframe, List<Kline>> data,
|
||||
double equity, double riskPercent) {
|
||||
IndicatorService ind = new IndicatorService();
|
||||
RiskManager riskManager = new RiskManager(2.0);
|
||||
MarketDataCache cache = new MarketDataCache(5000);
|
||||
List<Long> timeline = mergeTs(data);
|
||||
double peak = equity;
|
||||
double maxDD = 0;
|
||||
int trades = 0, wins = 0, losses = 0;
|
||||
List<EquityCurvePoint> curve = new ArrayList<EquityCurvePoint>();
|
||||
double startEquity = equity;
|
||||
|
||||
int idx = 0;
|
||||
for (Long ts : timeline) {
|
||||
for (Map.Entry<Timeframe, List<Kline>> e : data.entrySet()) {
|
||||
for (Kline k : e.getValue()) {
|
||||
if (k.openTime() == ts) {
|
||||
cache.upsert(e.getKey(), k);
|
||||
}
|
||||
}
|
||||
}
|
||||
StrategyContext ctx = new StrategyContext("BT", cache, ind, equity, riskPercent, ts);
|
||||
for (Strategy s : strategies) {
|
||||
java.util.Optional<Signal> maybe = s.evaluate(ctx);
|
||||
if (maybe.isPresent()) {
|
||||
trades++;
|
||||
Signal sig = maybe.get();
|
||||
PositionPlan plan = riskManager.plan(equity, riskPercent, sig.getEntryRef(), sig.getStopRef(), 0);
|
||||
boolean win = Math.random() < 0.55;
|
||||
double r = win ? 1.5 : -1.0;
|
||||
if (win) wins++;
|
||||
else losses++;
|
||||
equity += r * plan.getRiskAmount();
|
||||
}
|
||||
}
|
||||
if (equity > peak) peak = equity;
|
||||
double dd = (peak - equity) / peak;
|
||||
if (dd > maxDD) maxDD = dd;
|
||||
if (idx % 200 == 0) {
|
||||
curve.add(new EquityCurvePoint(ts, equity));
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
double winRate = trades == 0 ? 0.0 : (double) wins / trades;
|
||||
double totalR = (equity - startEquity) / (startEquity * riskPercent);
|
||||
return new BacktestResult(trades, wins, losses, winRate, totalR, maxDD, curve);
|
||||
}
|
||||
|
||||
private List<Long> mergeTs(Map<Timeframe, List<Kline>> data) {
|
||||
SortedSet<Long> set = new TreeSet<Long>();
|
||||
for (List<Kline> list : data.values()) {
|
||||
for (Kline k : list) {
|
||||
set.add(k.openTime());
|
||||
}
|
||||
}
|
||||
return new ArrayList<Long>(set);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package top.baogutang.strategy.backtests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BacktestResult {
|
||||
private final int trades;
|
||||
private final int wins;
|
||||
private final int losses;
|
||||
private final double winRate;
|
||||
private final double totalR;
|
||||
private final double maxDrawdown;
|
||||
private final List<EquityCurvePoint> curve;
|
||||
|
||||
public BacktestResult(int trades, int wins, int losses, double winRate,
|
||||
double totalR, double maxDrawdown, List<EquityCurvePoint> curve) {
|
||||
this.trades = trades;
|
||||
this.wins = wins;
|
||||
this.losses = losses;
|
||||
this.winRate = winRate;
|
||||
this.totalR = totalR;
|
||||
this.maxDrawdown = maxDrawdown;
|
||||
this.curve = curve;
|
||||
}
|
||||
|
||||
public int getTrades() {
|
||||
return trades;
|
||||
}
|
||||
|
||||
public int getWins() {
|
||||
return wins;
|
||||
}
|
||||
|
||||
public int getLosses() {
|
||||
return losses;
|
||||
}
|
||||
|
||||
public double getWinRate() {
|
||||
return winRate;
|
||||
}
|
||||
|
||||
public double getTotalR() {
|
||||
return totalR;
|
||||
}
|
||||
|
||||
public double getMaxDrawdown() {
|
||||
return maxDrawdown;
|
||||
}
|
||||
|
||||
public List<EquityCurvePoint> getCurve() {
|
||||
return curve;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package top.baogutang.strategy.backtests;
|
||||
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class CsvLoader {
|
||||
|
||||
public static List<Kline> load(String path, long tfMillis) throws Exception {
|
||||
List<String> lines = java.nio.file.Files.readAllLines(Paths.get(path));
|
||||
List<Kline> list = new ArrayList<>();
|
||||
for (int i = 1; i < lines.size(); i++) {
|
||||
String[] a = lines.get(i).split(",");
|
||||
long ts = Long.parseLong(a[0]);
|
||||
double o = Double.parseDouble(a[1]);
|
||||
double h = Double.parseDouble(a[2]);
|
||||
double l = Double.parseDouble(a[3]);
|
||||
double c = Double.parseDouble(a[4]);
|
||||
double v = Double.parseDouble(a[5]);
|
||||
list.add(new Kline(ts, o, h, l, c, v, ts + tfMillis - 1, true));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package top.baogutang.strategy.backtests;
|
||||
|
||||
public class EquityCurvePoint {
|
||||
|
||||
private final long ts;
|
||||
|
||||
private final double equity;
|
||||
|
||||
public EquityCurvePoint(long ts, double equity) {
|
||||
this.ts = ts;
|
||||
this.equity = equity;
|
||||
}
|
||||
|
||||
public long getTs() {
|
||||
return ts;
|
||||
}
|
||||
|
||||
public double getEquity() {
|
||||
return equity;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,314 @@
|
||||
package top.baogutang.strategy.commons.configs;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public class AppProperties {
|
||||
|
||||
private Account account;
|
||||
private List<String> timeframes;
|
||||
private List<SymbolConfig> symbols;
|
||||
private Alert alert;
|
||||
|
||||
public static class Account {
|
||||
private double equity;
|
||||
private double riskPercent;
|
||||
private double maxDailyLossPercent;
|
||||
private int maxConsecutiveLoss;
|
||||
private double portfolioMaxOpenRiskPercent;
|
||||
|
||||
public double getEquity() {
|
||||
return equity;
|
||||
}
|
||||
|
||||
public void setEquity(double equity) {
|
||||
this.equity = equity;
|
||||
}
|
||||
|
||||
public double getRiskPercent() {
|
||||
return riskPercent;
|
||||
}
|
||||
|
||||
public void setRiskPercent(double riskPercent) {
|
||||
this.riskPercent = riskPercent;
|
||||
}
|
||||
|
||||
public double getMaxDailyLossPercent() {
|
||||
return maxDailyLossPercent;
|
||||
}
|
||||
|
||||
public void setMaxDailyLossPercent(double maxDailyLossPercent) {
|
||||
this.maxDailyLossPercent = maxDailyLossPercent;
|
||||
}
|
||||
|
||||
public int getMaxConsecutiveLoss() {
|
||||
return maxConsecutiveLoss;
|
||||
}
|
||||
|
||||
public void setMaxConsecutiveLoss(int maxConsecutiveLoss) {
|
||||
this.maxConsecutiveLoss = maxConsecutiveLoss;
|
||||
}
|
||||
|
||||
public double getPortfolioMaxOpenRiskPercent() {
|
||||
return portfolioMaxOpenRiskPercent;
|
||||
}
|
||||
|
||||
public void setPortfolioMaxOpenRiskPercent(double portfolioMaxOpenRiskPercent) {
|
||||
this.portfolioMaxOpenRiskPercent = portfolioMaxOpenRiskPercent;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SymbolConfig {
|
||||
private String instId;
|
||||
private boolean enable = true;
|
||||
private Double riskPercentOverride;
|
||||
private Levels levels;
|
||||
private List<String> strategies;
|
||||
|
||||
public String getInstId() {
|
||||
return instId;
|
||||
}
|
||||
|
||||
public void setInstId(String instId) {
|
||||
this.instId = instId;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return enable;
|
||||
}
|
||||
|
||||
public void setEnable(boolean enable) {
|
||||
this.enable = enable;
|
||||
}
|
||||
|
||||
public Double getRiskPercentOverride() {
|
||||
return riskPercentOverride;
|
||||
}
|
||||
|
||||
public void setRiskPercentOverride(Double riskPercentOverride) {
|
||||
this.riskPercentOverride = riskPercentOverride;
|
||||
}
|
||||
|
||||
public Levels getLevels() {
|
||||
return levels;
|
||||
}
|
||||
|
||||
public void setLevels(Levels levels) {
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
public List<String> getStrategies() {
|
||||
return strategies;
|
||||
}
|
||||
|
||||
public void setStrategies(List<String> strategies) {
|
||||
this.strategies = strategies;
|
||||
}
|
||||
|
||||
public static class Levels {
|
||||
private double s1High, s1Low, s2High, s2Low;
|
||||
private double r1Low, r1High, r2Low, r2High, breakoutConfirm;
|
||||
|
||||
public double getS1High() {
|
||||
return s1High;
|
||||
}
|
||||
|
||||
public void setS1High(double s1High) {
|
||||
this.s1High = s1High;
|
||||
}
|
||||
|
||||
public double getS1Low() {
|
||||
return s1Low;
|
||||
}
|
||||
|
||||
public void setS1Low(double s1Low) {
|
||||
this.s1Low = s1Low;
|
||||
}
|
||||
|
||||
public double getS2High() {
|
||||
return s2High;
|
||||
}
|
||||
|
||||
public void setS2High(double s2High) {
|
||||
this.s2High = s2High;
|
||||
}
|
||||
|
||||
public double getS2Low() {
|
||||
return s2Low;
|
||||
}
|
||||
|
||||
public void setS2Low(double s2Low) {
|
||||
this.s2Low = s2Low;
|
||||
}
|
||||
|
||||
public double getR1Low() {
|
||||
return r1Low;
|
||||
}
|
||||
|
||||
public void setR1Low(double r1Low) {
|
||||
this.r1Low = r1Low;
|
||||
}
|
||||
|
||||
public double getR1High() {
|
||||
return r1High;
|
||||
}
|
||||
|
||||
public void setR1High(double r1High) {
|
||||
this.r1High = r1High;
|
||||
}
|
||||
|
||||
public double getR2Low() {
|
||||
return r2Low;
|
||||
}
|
||||
|
||||
public void setR2Low(double r2Low) {
|
||||
this.r2Low = r2Low;
|
||||
}
|
||||
|
||||
public double getR2High() {
|
||||
return r2High;
|
||||
}
|
||||
|
||||
public void setR2High(double r2High) {
|
||||
this.r2High = r2High;
|
||||
}
|
||||
|
||||
public double getBreakoutConfirm() {
|
||||
return breakoutConfirm;
|
||||
}
|
||||
|
||||
public void setBreakoutConfirm(double breakoutConfirm) {
|
||||
this.breakoutConfirm = breakoutConfirm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Alert {
|
||||
private DingTalk dingTalk;
|
||||
|
||||
public DingTalk getDingTalk() {
|
||||
return dingTalk;
|
||||
}
|
||||
|
||||
public void setDingTalk(DingTalk dingTalk) {
|
||||
this.dingTalk = dingTalk;
|
||||
}
|
||||
|
||||
public static class DingTalk {
|
||||
private List<DingTalkRobot> robots;
|
||||
|
||||
public List<DingTalkRobot> getRobots() {
|
||||
return robots;
|
||||
}
|
||||
|
||||
public void setRobots(List<DingTalkRobot> robots) {
|
||||
this.robots = robots;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DingTalkRobot {
|
||||
private String name;
|
||||
private String webhook;
|
||||
private Security security;
|
||||
private List<String> atMobiles;
|
||||
private boolean atAll;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getWebhook() {
|
||||
return webhook;
|
||||
}
|
||||
|
||||
public void setWebhook(String webhook) {
|
||||
this.webhook = webhook;
|
||||
}
|
||||
|
||||
public Security getSecurity() {
|
||||
return security;
|
||||
}
|
||||
|
||||
public void setSecurity(Security security) {
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
public List<String> getAtMobiles() {
|
||||
return atMobiles;
|
||||
}
|
||||
|
||||
public void setAtMobiles(List<String> atMobiles) {
|
||||
this.atMobiles = atMobiles;
|
||||
}
|
||||
|
||||
public boolean isAtAll() {
|
||||
return atAll;
|
||||
}
|
||||
|
||||
public void setAtAll(boolean atAll) {
|
||||
this.atAll = atAll;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Security {
|
||||
private boolean enable;
|
||||
private String secret;
|
||||
|
||||
public boolean isEnable() {
|
||||
return enable;
|
||||
}
|
||||
|
||||
public void setEnable(boolean enable) {
|
||||
this.enable = enable;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public List<String> getTimeframes() {
|
||||
return timeframes;
|
||||
}
|
||||
|
||||
public void setTimeframes(List<String> timeframes) {
|
||||
this.timeframes = timeframes;
|
||||
}
|
||||
|
||||
public List<SymbolConfig> getSymbols() {
|
||||
return symbols;
|
||||
}
|
||||
|
||||
public void setSymbols(List<SymbolConfig> symbols) {
|
||||
this.symbols = symbols;
|
||||
}
|
||||
|
||||
public Alert getAlert() {
|
||||
return alert;
|
||||
}
|
||||
|
||||
public void setAlert(Alert alert) {
|
||||
this.alert = alert;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package top.baogutang.strategy.commons.enums;
|
||||
|
||||
public enum Timeframe {
|
||||
M15("15m", "candle15m", 15 * 60_000L),
|
||||
H1("1h", "candle1H", 60 * 60_000L),
|
||||
H4("4h", "candle4H", 4 * 60 * 60_000L);
|
||||
public final String code;
|
||||
public final String okxChannel;
|
||||
public final long millis;
|
||||
|
||||
Timeframe(String code, String channel, long ms) {
|
||||
this.code = code;
|
||||
this.okxChannel = channel;
|
||||
this.millis = ms;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package top.baogutang.strategy.infra;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.*;
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class OkxPublicClient {
|
||||
|
||||
private static final String BASE = "https://www.okx.com";
|
||||
|
||||
private final OkHttpClient http;
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
public OkxPublicClient(OkHttpClient http, ObjectMapper mapper) {
|
||||
this.http = http;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public List<Kline> getHistory(String instId, Timeframe tf, int limit) throws IOException {
|
||||
String bar;
|
||||
switch (tf) {
|
||||
case M15:
|
||||
bar = "15m";
|
||||
break;
|
||||
case H1:
|
||||
bar = "1H";
|
||||
break;
|
||||
case H4:
|
||||
bar = "4H";
|
||||
break;
|
||||
default:
|
||||
bar = "1H";
|
||||
}
|
||||
HttpUrl url = HttpUrl.parse(BASE + "/api/v5/market/candles").newBuilder()
|
||||
.addQueryParameter("instId", instId)
|
||||
.addQueryParameter("bar", bar)
|
||||
.addQueryParameter("limit", String.valueOf(limit))
|
||||
.build();
|
||||
Request req = new Request.Builder().url(url).get().build();
|
||||
Response resp = http.newCall(req).execute();
|
||||
try {
|
||||
if (!resp.isSuccessful()) {
|
||||
throw new IOException("HTTP " + resp.code());
|
||||
}
|
||||
JsonNode root = mapper.readTree(resp.body().string());
|
||||
JsonNode data = root.get("data");
|
||||
List<Kline> list = new ArrayList<Kline>();
|
||||
for (int i = data.size() - 1; i >= 0; i--) {
|
||||
JsonNode arr = data.get(i);
|
||||
long ts = arr.get(0).asLong();
|
||||
double o = arr.get(1).asDouble();
|
||||
double h = arr.get(2).asDouble();
|
||||
double l = arr.get(3).asDouble();
|
||||
double c = arr.get(4).asDouble();
|
||||
double v = arr.get(5).asDouble();
|
||||
list.add(new Kline(ts, o, h, l, c, v, ts + tf.millis - 1, true));
|
||||
}
|
||||
return list;
|
||||
} finally {
|
||||
resp.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
package top.baogutang.strategy.infra;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Slf4j
|
||||
public class OkxWebSocketClient {
|
||||
|
||||
private final OkHttpClient http;
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
private WebSocket ws;
|
||||
|
||||
private final String URL = "wss://ws.okx.com:8443/ws/v5/business";
|
||||
|
||||
public OkxWebSocketClient(OkHttpClient http, ObjectMapper mapper) {
|
||||
this.http = http;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public static class WsKlineEvent {
|
||||
private final String instId;
|
||||
private final Timeframe timeframe;
|
||||
private final Kline kline;
|
||||
|
||||
public WsKlineEvent(String instId, Timeframe timeframe, Kline kline) {
|
||||
this.instId = instId;
|
||||
this.timeframe = timeframe;
|
||||
this.kline = kline;
|
||||
}
|
||||
|
||||
public String getInstId() {
|
||||
return instId;
|
||||
}
|
||||
|
||||
public Timeframe getTimeframe() {
|
||||
return timeframe;
|
||||
}
|
||||
|
||||
public Kline getKline() {
|
||||
return kline;
|
||||
}
|
||||
}
|
||||
|
||||
public void connect(final Map<String, List<Timeframe>> symbolMap,
|
||||
final Consumer<WsKlineEvent> consumer) {
|
||||
Request req = new Request.Builder().url(URL).build();
|
||||
ws = http.newWebSocket(req, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
log.info("[WS] onOpen code={} message={}", response.code(), response.message());
|
||||
List<Map<String, String>> args = new ArrayList<>();
|
||||
for (Map.Entry<String, List<Timeframe>> e : symbolMap.entrySet()) {
|
||||
String inst = e.getKey();
|
||||
for (Timeframe tf : e.getValue()) {
|
||||
Map<String, String> arg = new HashMap<>();
|
||||
arg.put("channel", tf.okxChannel);
|
||||
arg.put("instId", inst);
|
||||
args.add(arg);
|
||||
log.info("[WS] will-subscribe channel={} inst={}", tf.okxChannel, inst);
|
||||
}
|
||||
}
|
||||
Map<String, Object> sub = new HashMap<>();
|
||||
sub.put("op", "subscribe");
|
||||
sub.put("args", args);
|
||||
try {
|
||||
String json = mapper.writeValueAsString(sub);
|
||||
log.info("[WS] send subscribe payload={}", json);
|
||||
webSocket.send(json);
|
||||
} catch (Exception ex) {
|
||||
log.error("[WS] subscribe error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
log.debug("[WS] raw text={}", text);
|
||||
|
||||
try {
|
||||
JsonNode root = mapper.readTree(text);
|
||||
// 订阅确认等事件
|
||||
if (root.has("event")) {
|
||||
log.info("[WS] event ack raw={}", text);
|
||||
return;
|
||||
}
|
||||
if (!root.has("arg") || !root.has("data")) {
|
||||
log.debug("[WS] skip no arg/data");
|
||||
return;
|
||||
}
|
||||
JsonNode arg = root.get("arg");
|
||||
String channel = arg.get("channel").asText();
|
||||
String instId = arg.get("instId").asText();
|
||||
|
||||
Timeframe tf = null;
|
||||
for (Timeframe t : Timeframe.values()) {
|
||||
if (t.okxChannel.equals(channel)) {
|
||||
tf = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tf == null) {
|
||||
log.warn("[WS] unknown channel={} inst={}", channel, instId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (JsonNode arr : root.get("data")) {
|
||||
// OKX K线数组字段,请核对文档;这里你目前只解析前 6 个
|
||||
long ts = arr.get(0).asLong(); // 开始时间
|
||||
double o = arr.get(1).asDouble();
|
||||
double h = arr.get(2).asDouble();
|
||||
double l = arr.get(3).asDouble();
|
||||
double c = arr.get(4).asDouble();
|
||||
double v = arr.get(5).asDouble();
|
||||
// 建议用 confirm 标志(最后一个字段),而不是本地时间推断
|
||||
boolean finalBar = false;
|
||||
if (arr.size() >= 10) { // 根据 OKX 实际返回字段数调整
|
||||
// 一般 confirm 在最后一个
|
||||
JsonNode confirmNode = arr.get(arr.size() - 1);
|
||||
if (confirmNode.isTextual()) {
|
||||
finalBar = "1".equals(confirmNode.asText());
|
||||
} else if (confirmNode.isInt() || confirmNode.isBoolean()) {
|
||||
finalBar = confirmNode.asInt() == 1 || confirmNode.asBoolean();
|
||||
}
|
||||
} else {
|
||||
// 保留你的旧逻辑作为兜底
|
||||
finalBar = System.currentTimeMillis() >= ts + tf.millis;
|
||||
}
|
||||
|
||||
Kline k = new Kline(ts, o, h, l, c, v, ts + tf.millis - 1, finalBar);
|
||||
// log.info("[WS] DATA inst={} tf={} o={} h={} l={} c={} v={} finalBar={} size={}",
|
||||
// instId, tf, o, h, l, c, v, finalBar, arr.size());
|
||||
consumer.accept(new WsKlineEvent(instId, tf, k));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[WS] onMessage parse error text={}", text, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(WebSocket webSocket, int code, String reason) {
|
||||
log.warn("[WS] onClosing code={} reason={}", code, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
log.warn("[WS] onClosed code={} reason={}", code, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
log.error("[WS] onFailure respCode={} respMsg={}",
|
||||
response == null ? null : response.code(),
|
||||
response == null ? null : response.message(), t);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package top.baogutang.strategy.listeners;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.baogutang.strategy.commons.configs.AppProperties;
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.infra.OkxPublicClient;
|
||||
import top.baogutang.strategy.infra.OkxWebSocketClient;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.models.MultiSymbolMarketDataHub;
|
||||
import top.baogutang.strategy.strategies.MultiSymbolStrategyEngine;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class KlineMultiListener {
|
||||
private final OkxPublicClient rest;
|
||||
private final OkxWebSocketClient ws;
|
||||
private final MultiSymbolMarketDataHub hub;
|
||||
private final MultiSymbolStrategyEngine engine;
|
||||
private final AppProperties props;
|
||||
|
||||
public KlineMultiListener(OkxPublicClient rest, OkxWebSocketClient ws,
|
||||
MultiSymbolMarketDataHub hub,
|
||||
MultiSymbolStrategyEngine engine,
|
||||
AppProperties props) {
|
||||
this.rest = rest;
|
||||
this.ws = ws;
|
||||
this.hub = hub;
|
||||
this.engine = engine;
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() throws Exception {
|
||||
List<Timeframe> tfs = new ArrayList<Timeframe>();
|
||||
for (String tfStr : props.getTimeframes()) {
|
||||
tfs.add(Timeframe.valueOf(tfStr));
|
||||
}
|
||||
// 预加载
|
||||
for (AppProperties.SymbolConfig sc : props.getSymbols()) {
|
||||
if (!sc.isEnable()) continue;
|
||||
for (Timeframe tf : tfs) {
|
||||
java.util.List<Kline> list = rest.getHistory(sc.getInstId(), tf, 300);
|
||||
for (Kline k : list) {
|
||||
hub.upsert(sc.getInstId(), tf, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, List<Timeframe>> subMap = new HashMap<>();
|
||||
for (AppProperties.SymbolConfig sc : props.getSymbols()) {
|
||||
if (!sc.isEnable()) continue;
|
||||
subMap.put(sc.getInstId(), tfs);
|
||||
}
|
||||
ws.connect(subMap, ev -> {
|
||||
hub.upsert(ev.getInstId(), ev.getTimeframe(), ev.getKline());
|
||||
boolean trigger = ev.getKline().finalBar() || ev.getTimeframe() == Timeframe.M15;
|
||||
if (trigger) {
|
||||
// System.out.println("[WS -> ENGINE] call onKline " + ev.getInstId() + " " + ev.getTimeframe());
|
||||
engine.onKline(ev.getInstId(), ev.getTimeframe());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
public class Kline {
|
||||
|
||||
private final long openTime;
|
||||
private final double open;
|
||||
private final double high;
|
||||
private final double low;
|
||||
private final double close;
|
||||
private final double volume;
|
||||
private final long closeTime;
|
||||
private final boolean finalBar;
|
||||
|
||||
public Kline(long openTime, double open, double high, double low, double close,
|
||||
double volume, long closeTime, boolean finalBar) {
|
||||
this.openTime = openTime;
|
||||
this.open = open;
|
||||
this.high = high;
|
||||
this.low = low;
|
||||
this.close = close;
|
||||
this.volume = volume;
|
||||
this.closeTime = closeTime;
|
||||
this.finalBar = finalBar;
|
||||
}
|
||||
|
||||
public long openTime() {
|
||||
return openTime;
|
||||
}
|
||||
|
||||
public double open() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public double high() {
|
||||
return high;
|
||||
}
|
||||
|
||||
public double low() {
|
||||
return low;
|
||||
}
|
||||
|
||||
public double close() {
|
||||
return close;
|
||||
}
|
||||
|
||||
public double volume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
public long closeTime() {
|
||||
return closeTime;
|
||||
}
|
||||
|
||||
public boolean finalBar() {
|
||||
return finalBar;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
|
||||
public class MarketDataCache {
|
||||
private final Map<Timeframe, NavigableMap<Long, Kline>> map = new ConcurrentHashMap<>();
|
||||
private final int maxBars;
|
||||
|
||||
public MarketDataCache(int maxBars) {
|
||||
this.maxBars = maxBars;
|
||||
}
|
||||
|
||||
public void upsert(Timeframe tf, Kline k) {
|
||||
if (!map.containsKey(tf)) {
|
||||
map.put(tf, new ConcurrentSkipListMap<Long, Kline>());
|
||||
}
|
||||
NavigableMap<Long, Kline> m = map.get(tf);
|
||||
m.put(k.openTime(), k);
|
||||
while (m.size() > maxBars) {
|
||||
Map.Entry<Long, Kline> e = m.pollFirstEntry();
|
||||
if (e == null) break;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Kline> lastN(Timeframe tf, int n) {
|
||||
NavigableMap<Long, Kline> m = map.get(tf);
|
||||
if (m == null || m.isEmpty()) return Collections.emptyList();
|
||||
List<Kline> tmp = new ArrayList<Kline>(m.values());
|
||||
int size = tmp.size();
|
||||
if (n >= size) return tmp;
|
||||
return tmp.subList(size - n, size);
|
||||
}
|
||||
|
||||
public Kline last(Timeframe tf) {
|
||||
NavigableMap<Long, Kline> m = map.get(tf);
|
||||
if (m == null || m.isEmpty()) return null;
|
||||
return m.lastEntry().getValue();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class MultiSymbolMarketDataHub {
|
||||
private final Map<String, MarketDataCache> caches = new ConcurrentHashMap<String, MarketDataCache>();
|
||||
private final int maxBars;
|
||||
|
||||
public MultiSymbolMarketDataHub(int maxBars) {
|
||||
this.maxBars = maxBars;
|
||||
}
|
||||
|
||||
private MarketDataCache ensure(String symbol) {
|
||||
MarketDataCache c = caches.get(symbol);
|
||||
if (c == null) {
|
||||
c = new MarketDataCache(maxBars);
|
||||
MarketDataCache old = caches.putIfAbsent(symbol, c);
|
||||
if (old != null) c = old;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
public void upsert(String symbol, Timeframe tf, Kline k) {
|
||||
ensure(symbol).upsert(tf, k);
|
||||
}
|
||||
|
||||
public MarketDataCache raw(String symbol) {
|
||||
return ensure(symbol);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PortfolioRiskManager {
|
||||
private final double equity;
|
||||
private final double maxOpenRiskPercent;
|
||||
private final Map<String, Double> openRisk = new ConcurrentHashMap<>();
|
||||
|
||||
public PortfolioRiskManager(double equity, double maxOpenRiskPercent) {
|
||||
this.equity = equity;
|
||||
this.maxOpenRiskPercent = maxOpenRiskPercent;
|
||||
}
|
||||
|
||||
public synchronized boolean canAccept(double riskAmt) {
|
||||
double curr = currentOpenRisk();
|
||||
return (curr + riskAmt) <= equity * maxOpenRiskPercent;
|
||||
}
|
||||
|
||||
public double currentOpenRisk() {
|
||||
double sum = 0;
|
||||
for (Double d : openRisk.values()) sum += d;
|
||||
return sum;
|
||||
}
|
||||
|
||||
public void register(String signalId, double amt) {
|
||||
openRisk.put(signalId, amt);
|
||||
}
|
||||
|
||||
public void close(String signalId) {
|
||||
openRisk.remove(signalId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
public class PositionPlan {
|
||||
private final double quantity;
|
||||
private final double notional;
|
||||
private final double leverage;
|
||||
private final double riskAmount;
|
||||
|
||||
public PositionPlan(double quantity, double notional, double leverage, double riskAmount) {
|
||||
this.quantity = quantity;
|
||||
this.notional = notional;
|
||||
this.leverage = leverage;
|
||||
this.riskAmount = riskAmount;
|
||||
}
|
||||
|
||||
public double getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public double getNotional() {
|
||||
return notional;
|
||||
}
|
||||
|
||||
public double getLeverage() {
|
||||
return leverage;
|
||||
}
|
||||
|
||||
public double getRiskAmount() {
|
||||
return riskAmount;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
|
||||
public class RiskManager {
|
||||
private final double maxLeverage;
|
||||
|
||||
public RiskManager(double maxLeverage) {
|
||||
this.maxLeverage = maxLeverage;
|
||||
}
|
||||
|
||||
public PositionPlan plan(double equity, double riskPercent, double entry, double stop, double minQtyStep) {
|
||||
double riskAmt = equity * riskPercent;
|
||||
double dist = Math.abs(entry - stop);
|
||||
if (dist <= 0) throw new IllegalArgumentException("Stop=Entry");
|
||||
double qty = riskAmt / dist;
|
||||
if (minQtyStep > 0) {
|
||||
qty = Math.floor(qty / minQtyStep) * minQtyStep;
|
||||
}
|
||||
double notional = qty * entry;
|
||||
double lev = notional / equity;
|
||||
if (lev > maxLeverage) {
|
||||
double scale = maxLeverage / lev;
|
||||
qty *= scale;
|
||||
notional = qty * entry;
|
||||
lev = notional / equity;
|
||||
}
|
||||
return new PositionPlan(qty, notional, lev, riskAmt);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
public class Signal {
|
||||
private final String id;
|
||||
private final long timestamp;
|
||||
private final String symbol;
|
||||
private final String strategy;
|
||||
private final String direction;
|
||||
private final double entryRef;
|
||||
private final double stopRef;
|
||||
private final double[] targets;
|
||||
private final String grade;
|
||||
private final String comment;
|
||||
|
||||
public Signal(String id, long timestamp, String symbol, String strategy, String direction,
|
||||
double entryRef, double stopRef, double[] targets, String grade, String comment) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.symbol = symbol;
|
||||
this.strategy = strategy;
|
||||
this.direction = direction;
|
||||
this.entryRef = entryRef;
|
||||
this.stopRef = stopRef;
|
||||
this.targets = targets;
|
||||
this.grade = grade;
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
public String getStrategy() {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
public String getDirection() {
|
||||
return direction;
|
||||
}
|
||||
|
||||
public double getEntryRef() {
|
||||
return entryRef;
|
||||
}
|
||||
|
||||
public double getStopRef() {
|
||||
return stopRef;
|
||||
}
|
||||
|
||||
public double[] getTargets() {
|
||||
return targets;
|
||||
}
|
||||
|
||||
public String getGrade() {
|
||||
return grade;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package top.baogutang.strategy.models;
|
||||
|
||||
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
public class StrategyContext {
|
||||
private final String symbol;
|
||||
private final MarketDataCache cache;
|
||||
private final IndicatorService indicators;
|
||||
private final double accountEquity;
|
||||
private final double riskPercent;
|
||||
private final long now;
|
||||
|
||||
public StrategyContext(String symbol, MarketDataCache cache, IndicatorService indicators,
|
||||
double accountEquity, double riskPercent, long now) {
|
||||
this.symbol = symbol;
|
||||
this.cache = cache;
|
||||
this.indicators = indicators;
|
||||
this.accountEquity = accountEquity;
|
||||
this.riskPercent = riskPercent;
|
||||
this.now = now;
|
||||
}
|
||||
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
public MarketDataCache getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
public IndicatorService getIndicators() {
|
||||
return indicators;
|
||||
}
|
||||
|
||||
public double getAccountEquity() {
|
||||
return accountEquity;
|
||||
}
|
||||
|
||||
public double getRiskPercent() {
|
||||
return riskPercent;
|
||||
}
|
||||
|
||||
public long getNow() {
|
||||
return now;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package top.baogutang.strategy.runners;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.baogutang.strategy.backtests.BacktestEngine;
|
||||
import top.baogutang.strategy.backtests.BacktestResult;
|
||||
import top.baogutang.strategy.backtests.CsvLoader;
|
||||
import top.baogutang.strategy.commons.configs.AppProperties;
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.strategies.Strategy;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BacktestCommandLineRunner implements CommandLineRunner {
|
||||
private final AppProperties props;
|
||||
private final Map<String, List<Strategy>> strategiesMap;
|
||||
|
||||
public BacktestCommandLineRunner(AppProperties props,
|
||||
Map<String, List<Strategy>> strategiesMap) {
|
||||
this.props = props;
|
||||
this.strategiesMap = strategiesMap;
|
||||
}
|
||||
|
||||
public void run(String... args) throws Exception {
|
||||
boolean backtest = false;
|
||||
for (String a : args) {
|
||||
if ("--backtest=true".equalsIgnoreCase(a)) {
|
||||
backtest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!backtest) return;
|
||||
|
||||
AppProperties.SymbolConfig cfg = props.getSymbols().get(0);
|
||||
Map<Timeframe, List<Kline>> data = new HashMap<Timeframe, List<Kline>>();
|
||||
data.put(Timeframe.M15, CsvLoader.load("data/" + cfg.getInstId() + "_M15.csv", Timeframe.M15.millis));
|
||||
data.put(Timeframe.H1, CsvLoader.load("data/" + cfg.getInstId() + "_H1.csv", Timeframe.H1.millis));
|
||||
data.put(Timeframe.H4, CsvLoader.load("data/" + cfg.getInstId() + "_H4.csv", Timeframe.H4.millis));
|
||||
|
||||
BacktestEngine engine = new BacktestEngine();
|
||||
List<Strategy> strategies = strategiesMap.get(cfg.getInstId());
|
||||
BacktestResult r = engine.run(strategies, data, props.getAccount().getEquity(), props.getAccount().getRiskPercent());
|
||||
log.info("Backtest %s Trades=%d WinRate=%.2f%% TotalR=%.2f MaxDD=%.2f%%%n",
|
||||
cfg.getInstId(), r.getTrades(), r.getWinRate() * 100, r.getTotalR(), r.getMaxDrawdown() * 100);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package top.baogutang.strategy.services;
|
||||
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IndicatorService {
|
||||
public double sma(List<Kline> bars, int p) {
|
||||
if (bars.size() < p) return Double.NaN;
|
||||
double s = 0;
|
||||
for (int i = bars.size() - p; i < bars.size(); i++) s += bars.get(i).close();
|
||||
return s / p;
|
||||
}
|
||||
|
||||
public double smaVolume(List<Kline> bars, int p) {
|
||||
if (bars.size() < p) return Double.NaN;
|
||||
double s = 0;
|
||||
for (int i = bars.size() - p; i < bars.size(); i++) s += bars.get(i).volume();
|
||||
return s / p;
|
||||
}
|
||||
|
||||
public double atr(List<Kline> bars, int p) {
|
||||
if (bars.size() < p + 1) return Double.NaN;
|
||||
double sum = 0;
|
||||
for (int i = 1; i < bars.size(); i++) {
|
||||
Kline c = bars.get(i);
|
||||
Kline prev = bars.get(i - 1);
|
||||
double tr = Math.max(c.high() - c.low(),
|
||||
Math.max(Math.abs(c.high() - prev.close()), Math.abs(c.low() - prev.close())));
|
||||
if (i >= bars.size() - p) sum += tr;
|
||||
}
|
||||
return sum / p;
|
||||
}
|
||||
|
||||
public double lowerShadowRatio(Kline k) {
|
||||
double range = k.high() - k.low();
|
||||
if (range <= 0) return 0;
|
||||
double lower = Math.min(k.open(), k.close()) - k.low();
|
||||
return lower / range;
|
||||
}
|
||||
|
||||
public double upperShadowRatio(Kline k) {
|
||||
double range = k.high() - k.low();
|
||||
if (range <= 0) return 0;
|
||||
double upper = k.high() - Math.max(k.open(), k.close());
|
||||
return upper / range;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.baogutang.strategy.alerts.AlertChannel;
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.*;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
public class MultiSymbolStrategyEngine {
|
||||
|
||||
private final MultiSymbolMarketDataHub hub;
|
||||
private final IndicatorService indicators;
|
||||
private final RiskManager riskManager;
|
||||
private final AlertChannel alertChannel;
|
||||
private final Map<String, List<Strategy>> strategiesBySymbol;
|
||||
private final double baseEquity;
|
||||
private final double baseRiskPercent;
|
||||
private final PortfolioRiskManager portfolioRisk;
|
||||
private final Map<String, Double> symbolRiskOverride;
|
||||
|
||||
public MultiSymbolStrategyEngine(MultiSymbolMarketDataHub hub,
|
||||
IndicatorService indicators,
|
||||
RiskManager riskManager,
|
||||
AlertChannel alertChannel,
|
||||
Map<String, List<Strategy>> strategiesBySymbol,
|
||||
double baseEquity,
|
||||
double baseRiskPercent,
|
||||
PortfolioRiskManager portfolioRisk,
|
||||
Map<String, Double> symbolRiskOverride) {
|
||||
this.hub = hub;
|
||||
this.indicators = indicators;
|
||||
this.riskManager = riskManager;
|
||||
this.alertChannel = alertChannel;
|
||||
this.strategiesBySymbol = strategiesBySymbol;
|
||||
this.baseEquity = baseEquity;
|
||||
this.baseRiskPercent = baseRiskPercent;
|
||||
this.portfolioRisk = portfolioRisk;
|
||||
this.symbolRiskOverride = symbolRiskOverride;
|
||||
}
|
||||
|
||||
public void onKline(String symbol, Timeframe tf) {
|
||||
// log.info("onKline {} {}", symbol, tf);
|
||||
List<Strategy> list = strategiesBySymbol.get(symbol);
|
||||
if (list == null) return;
|
||||
double riskPercent = symbolRiskOverride.getOrDefault(symbol, baseRiskPercent);
|
||||
StrategyContext ctx = new StrategyContext(symbol, hub.raw(symbol), indicators, baseEquity, riskPercent, System.currentTimeMillis());
|
||||
for (Strategy s : list) {
|
||||
try {
|
||||
java.util.Optional<Signal> maybe = s.evaluate(ctx);
|
||||
if (maybe.isPresent()) {
|
||||
Signal sig = maybe.get();
|
||||
PositionPlan plan = riskManager.plan(baseEquity, riskPercent, sig.getEntryRef(), sig.getStopRef(), 0);
|
||||
if (portfolioRisk != null) {
|
||||
if (!portfolioRisk.canAccept(plan.getRiskAmount())) {
|
||||
log.info("Portfolio risk reject {} {}", sig.getStrategy(), sig.getId());
|
||||
continue;
|
||||
}
|
||||
portfolioRisk.register(sig.getId(), plan.getRiskAmount());
|
||||
}
|
||||
alertChannel.send(sig, plan);
|
||||
} else {
|
||||
log.info("Strategy {} for coin:{} has no portfolio risk", s.name(), symbol);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Strategy {} error {}", s.name(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.models.StrategyContext;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface Strategy {
|
||||
|
||||
String name();
|
||||
|
||||
Optional<Signal> evaluate(StrategyContext ctx);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.models.StrategyContext;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StrategyBreakdownRetestShort implements Strategy {
|
||||
private final double level;
|
||||
private volatile boolean armed = false;
|
||||
private volatile long breakdownBarTime = -1;
|
||||
|
||||
public StrategyBreakdownRetestShort(double level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return "BreakdownRetestShort";
|
||||
}
|
||||
|
||||
public Optional<Signal> evaluate(StrategyContext ctx) {
|
||||
java.util.List<Kline> h4 = ctx.getCache().lastN(Timeframe.H4, 30);
|
||||
java.util.List<Kline> m15 = ctx.getCache().lastN(Timeframe.M15, 80);
|
||||
if (h4.size() < 10 || m15.size() < 20) return Optional.empty();
|
||||
IndicatorService ind = ctx.getIndicators();
|
||||
Kline lastH4 = h4.get(h4.size() - 1);
|
||||
if (lastH4.finalBar()) {
|
||||
double avgVol = ind.smaVolume(h4, 10);
|
||||
boolean breakdown = lastH4.close() < level && lastH4.volume() >= avgVol * 1.2;
|
||||
if (breakdown) {
|
||||
armed = true;
|
||||
breakdownBarTime = lastH4.openTime();
|
||||
}
|
||||
}
|
||||
if (!armed) return Optional.empty();
|
||||
int consecutive = 0;
|
||||
for (int i = m15.size() - 1; i >= 0 && consecutive < 3; i--) {
|
||||
Kline k = m15.get(i);
|
||||
if (k.openTime() < breakdownBarTime) break;
|
||||
boolean zone = k.high() <= level + Math.abs(level) * 0.000002
|
||||
&& k.high() >= level - Math.abs(level) * 0.000005;
|
||||
if (zone) consecutive++;
|
||||
else if (consecutive > 0) break;
|
||||
}
|
||||
if (consecutive >= 2) {
|
||||
armed = false;
|
||||
double entry = level - Math.abs(level) * 0.000002;
|
||||
double stop = level + Math.abs(level) * 0.00001;
|
||||
double[] targets = new double[]{
|
||||
level - Math.abs(level) * 0.00002,
|
||||
level - Math.abs(level) * 0.00005,
|
||||
level - Math.abs(level) * 0.00009
|
||||
};
|
||||
Signal sig = new Signal(java.util.UUID.randomUUID().toString(),
|
||||
ctx.getNow(), ctx.getSymbol(), name(), "SHORT",
|
||||
entry, stop, targets, "A", "Retest m15 consecutive=" + consecutive);
|
||||
return Optional.of(sig);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.models.StrategyContext;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StrategyBreakoutRetestLong implements Strategy {
|
||||
private final double breakoutConfirm;
|
||||
private final double r2Low, r2High;
|
||||
private volatile boolean armed = false;
|
||||
private volatile long breakoutTime = -1;
|
||||
|
||||
public StrategyBreakoutRetestLong(double breakoutConfirm, double r2Low, double r2High) {
|
||||
this.breakoutConfirm = breakoutConfirm;
|
||||
this.r2Low = r2Low;
|
||||
this.r2High = r2High;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return "BreakoutRetestLong";
|
||||
}
|
||||
|
||||
public Optional<Signal> evaluate(StrategyContext ctx) {
|
||||
java.util.List<Kline> h4 = ctx.getCache().lastN(Timeframe.H4, 35);
|
||||
java.util.List<Kline> m15 = ctx.getCache().lastN(Timeframe.M15, 80);
|
||||
if (h4.size() < 12 || m15.size() < 30) return Optional.empty();
|
||||
IndicatorService ind = ctx.getIndicators();
|
||||
Kline lastH4 = h4.get(h4.size() - 1);
|
||||
if (lastH4.finalBar()) {
|
||||
double avgVol = ind.smaVolume(h4, 10);
|
||||
boolean brk = lastH4.close() >= breakoutConfirm && lastH4.volume() >= avgVol * 1.3;
|
||||
if (brk) {
|
||||
armed = true;
|
||||
breakoutTime = lastH4.openTime();
|
||||
}
|
||||
}
|
||||
if (!armed) return Optional.empty();
|
||||
|
||||
int barsChecked = 0;
|
||||
int holds = 0;
|
||||
double prevLow = -1;
|
||||
for (int i = m15.size() - 1; i >= 0 && barsChecked < 12; i--) {
|
||||
Kline k = m15.get(i);
|
||||
if (k.openTime() < breakoutTime) break;
|
||||
barsChecked++;
|
||||
boolean within = k.low() >= r2Low - Math.abs(r2Low) * 0.000003
|
||||
&& k.low() <= r2High + Math.abs(r2High) * 0.000003;
|
||||
if (!within) return Optional.empty();
|
||||
if (prevLow > 0 && k.low() >= prevLow * 0.999) holds++;
|
||||
prevLow = k.low();
|
||||
}
|
||||
if (barsChecked >= 4 && holds >= 2) {
|
||||
Kline last15 = m15.get(m15.size() - 1);
|
||||
double avgVol = ind.smaVolume(m15, 20);
|
||||
if (last15.volume() < avgVol * 1.5) return Optional.empty();
|
||||
armed = false;
|
||||
double entry = Math.min(last15.close(), r2Low + (r2High - r2Low) * 0.3);
|
||||
double stop = r2Low - (r2High - r2Low) * 0.8;
|
||||
double[] targets = new double[]{
|
||||
breakoutConfirm + (r2High - r2Low) * 1.2,
|
||||
breakoutConfirm + (r2High - r2Low) * 2.0,
|
||||
breakoutConfirm + (r2High - r2Low) * 3.0
|
||||
};
|
||||
String cmt = "Pullback bars=" + barsChecked + " holds=" + holds + " volFactor=" + String.format("%.2f", last15.volume() / avgVol);
|
||||
Signal sig = new Signal(java.util.UUID.randomUUID().toString(),
|
||||
ctx.getNow(), ctx.getSymbol(), name(), "LONG",
|
||||
entry, stop, targets, "A", cmt);
|
||||
return Optional.of(sig);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.commons.configs.AppProperties;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StrategyFactory {
|
||||
public static Map<String, List<Strategy>> build(AppProperties props) {
|
||||
Map<String, List<Strategy>> map = new HashMap<String, List<Strategy>>();
|
||||
for (AppProperties.SymbolConfig sc : props.getSymbols()) {
|
||||
if (!sc.isEnable()) continue;
|
||||
AppProperties.SymbolConfig.Levels lv = sc.getLevels();
|
||||
List<Strategy> list = new ArrayList<Strategy>();
|
||||
for (String name : sc.getStrategies()) {
|
||||
if ("LowReboundLong".equals(name)) {
|
||||
list.add(new StrategyLowReboundLong(
|
||||
lv.getS1High(), lv.getS1Low(), lv.getS2High(), lv.getS2Low()));
|
||||
} else if ("MeanRevertShort".equals(name)) {
|
||||
list.add(new StrategyMeanRevertShort(lv.getR1Low(), lv.getR1High()));
|
||||
} else if ("BreakdownRetestShort".equals(name)) {
|
||||
list.add(new StrategyBreakdownRetestShort(lv.getS1Low()));
|
||||
} else if ("BreakoutRetestLong".equals(name)) {
|
||||
list.add(new StrategyBreakoutRetestLong(
|
||||
lv.getBreakoutConfirm(), lv.getR2Low(), lv.getR2High()));
|
||||
}
|
||||
}
|
||||
map.put(sc.getInstId(), list);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.models.StrategyContext;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StrategyLowReboundLong implements Strategy {
|
||||
private final double s1High, s1Low, s2High, s2Low;
|
||||
|
||||
public StrategyLowReboundLong(double s1High, double s1Low, double s2High, double s2Low) {
|
||||
this.s1High = s1High;
|
||||
this.s1Low = s1Low;
|
||||
this.s2High = s2High;
|
||||
this.s2Low = s2Low;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return "LowReboundLong";
|
||||
}
|
||||
|
||||
public Optional<Signal> evaluate(StrategyContext ctx) {
|
||||
java.util.List<Kline> h4 = ctx.getCache().lastN(Timeframe.H4, 40);
|
||||
if (h4.size() < 12) return Optional.empty();
|
||||
Kline last = h4.get(h4.size() - 1);
|
||||
if (!last.finalBar()) return Optional.empty();
|
||||
IndicatorService ind = ctx.getIndicators();
|
||||
double avgVol = ind.smaVolume(h4, 8);
|
||||
boolean inS1 = last.low() <= s1High && last.low() >= s1Low - (s1High - s1Low) * 0.5;
|
||||
boolean inS2 = last.low() <= s2High && last.low() >= s2Low - (s2High - s2Low) * 0.5;
|
||||
if (!(inS1 || inS2)) return Optional.empty();
|
||||
double lowerRatio = ind.lowerShadowRatio(last);
|
||||
if (lowerRatio < 0.55) return Optional.empty();
|
||||
if (last.volume() < avgVol * 1.1) return Optional.empty();
|
||||
double entry = last.close();
|
||||
double stop = inS1 ? s1Low - (s1High - s1Low) * 0.7 : s2Low - (s2High - s2Low) * 0.6;
|
||||
double[] targets;
|
||||
if (inS1) {
|
||||
targets = new double[]{
|
||||
s1High + (s1High - s1Low) * 2,
|
||||
s1High + (s1High - s1Low) * 3,
|
||||
s1High + (s1High - s1Low) * 5
|
||||
};
|
||||
} else {
|
||||
targets = new double[]{
|
||||
s2High + (s2High - s2Low) * 1.5,
|
||||
s1High,
|
||||
s1High + (s1High - s1Low) * 2
|
||||
};
|
||||
}
|
||||
String comment = "lowerRatio=" + String.format("%.2f", lowerRatio) + " volFactor=" + String.format("%.2f", last.volume() / avgVol);
|
||||
Signal sig = new Signal(java.util.UUID.randomUUID().toString(),
|
||||
ctx.getNow(), ctx.getSymbol(), name(), "LONG",
|
||||
entry, stop, targets, "A", comment);
|
||||
return Optional.of(sig);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package top.baogutang.strategy.strategies;
|
||||
|
||||
import top.baogutang.strategy.commons.enums.Timeframe;
|
||||
import top.baogutang.strategy.models.Kline;
|
||||
import top.baogutang.strategy.models.Signal;
|
||||
import top.baogutang.strategy.models.StrategyContext;
|
||||
import top.baogutang.strategy.services.IndicatorService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StrategyMeanRevertShort implements Strategy {
|
||||
private final double r1Low, r1High;
|
||||
|
||||
public StrategyMeanRevertShort(double r1Low, double r1High) {
|
||||
this.r1Low = r1Low;
|
||||
this.r1High = r1High;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return "MeanRevertShort";
|
||||
}
|
||||
|
||||
public Optional<Signal> evaluate(StrategyContext ctx) {
|
||||
java.util.List<Kline> h1 = ctx.getCache().lastN(Timeframe.H1, 120);
|
||||
if (h1.size() < 50) return Optional.empty();
|
||||
Kline last = h1.get(h1.size() - 1);
|
||||
if (!last.finalBar()) return Optional.empty();
|
||||
IndicatorService ind = ctx.getIndicators();
|
||||
double ma7 = ind.sma(h1, 7);
|
||||
double ma30 = ind.sma(h1, 30);
|
||||
if (!(ma7 < ma30)) return Optional.empty();
|
||||
boolean inR1 = last.high() >= r1Low && last.close() <= r1High;
|
||||
if (!inR1) return Optional.empty();
|
||||
Kline h1_1 = h1.get(h1.size() - 2);
|
||||
Kline h1_2 = h1.get(h1.size() - 3);
|
||||
double prevMax = Math.max(h1_1.high(), h1_2.high());
|
||||
if (last.high() > prevMax * 1.002) return Optional.empty();
|
||||
double avgVol20 = ind.smaVolume(h1, 20);
|
||||
double recent3 = 0;
|
||||
for (int i = h1.size() - 3; i < h1.size(); i++) recent3 += h1.get(i).volume();
|
||||
recent3 /= 3.0;
|
||||
if (recent3 > avgVol20 * 1.05) return Optional.empty();
|
||||
double entry = Math.min(last.close(), r1High - (r1High - r1Low) * 0.3);
|
||||
double stop = r1High + (r1High - r1Low) * 0.4;
|
||||
double[] targets = new double[]{
|
||||
r1Low - (r1High - r1Low) * 1.0,
|
||||
r1Low - (r1High - r1Low) * 1.8,
|
||||
r1Low - (r1High - r1Low) * 3.0
|
||||
};
|
||||
String comment = String.format("ma7=%.2f ma30=%.2f volFactor=%.2f", ma7, ma30, recent3 / avgVol20);
|
||||
Signal sig = new Signal(java.util.UUID.randomUUID().toString(),
|
||||
ctx.getNow(), ctx.getSymbol(), name(), "SHORT",
|
||||
entry, stop, targets, "A", comment);
|
||||
return Optional.of(sig);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package top.baogutang.strategy.utils;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
public class SignUtil {
|
||||
public static String sign(long timestamp, String secret) {
|
||||
try {
|
||||
String str = timestamp + "\n" + secret;
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] signData = mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
|
||||
String base64 = Base64.getEncoder().encodeToString(signData);
|
||||
return URLEncoder.encode(base64, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
baogutang-strategy/src/main/resources/application.yml
Normal file
98
baogutang-strategy/src/main/resources/application.yml
Normal file
@ -0,0 +1,98 @@
|
||||
server:
|
||||
port: 8108
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: baogutang-strategy
|
||||
datasource:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://117.72.78.133:3306/baogutang?characterEncoding=UTF-8
|
||||
username: baogutang
|
||||
password: dPy6Cwp4Fxi5AFpe
|
||||
hikari:
|
||||
max-lifetime: 60000
|
||||
|
||||
app:
|
||||
account:
|
||||
equity: 10000
|
||||
riskPercent: 0.02
|
||||
maxDailyLossPercent: 0.08
|
||||
maxConsecutiveLoss: 3
|
||||
portfolioMaxOpenRiskPercent: 0.10
|
||||
timeframes: [ M15,H1,H4 ]
|
||||
symbols:
|
||||
- instId: BTC-USDT-SWAP
|
||||
enable: true
|
||||
riskPercentOverride: 0.015
|
||||
levels:
|
||||
s1High: 64000
|
||||
s1Low: 63650
|
||||
s2High: 62800
|
||||
s2Low: 62200
|
||||
r1Low: 64850
|
||||
r1High: 65200
|
||||
r2Low: 66000
|
||||
r2High: 66650
|
||||
breakoutConfirm: 66000
|
||||
strategies: [ LowReboundLong, MeanRevertShort, BreakdownRetestShort, BreakoutRetestLong ]
|
||||
- instId: ETH-USDT-SWAP
|
||||
enable: true
|
||||
levels:
|
||||
s1High: 3150
|
||||
s1Low: 3110
|
||||
s2High: 3050
|
||||
s2Low: 3000
|
||||
r1Low: 3220
|
||||
r1High: 3255
|
||||
r2Low: 3320
|
||||
r2High: 3360
|
||||
breakoutConfirm: 3320
|
||||
strategies: [ LowReboundLong, MeanRevertShort, BreakoutRetestLong ]
|
||||
- instId: PEPE-USDT-SWAP
|
||||
enable: true
|
||||
levels:
|
||||
s1High: 0.00001105
|
||||
s1Low: 0.00001095
|
||||
s2High: 0.00001085
|
||||
s2Low: 0.00001080
|
||||
r1Low: 0.00001140
|
||||
r1High: 0.00001155
|
||||
r2Low: 0.00001180
|
||||
r2High: 0.00001190
|
||||
breakoutConfirm: 0.00001190
|
||||
strategies: [ LowReboundLong, MeanRevertShort, BreakdownRetestShort, BreakoutRetestLong ]
|
||||
- instId: SOL-USDT-SWAP
|
||||
enable: true
|
||||
levels:
|
||||
s1High: 134
|
||||
s1Low: 132
|
||||
s2High: 126
|
||||
s2Low: 123
|
||||
r1Low: 138
|
||||
r1High: 141
|
||||
r2Low: 146
|
||||
r2High: 149
|
||||
breakoutConfirm: 146
|
||||
strategies: [ MeanRevertShort, BreakoutRetestLong ]
|
||||
|
||||
alert:
|
||||
dingTalk:
|
||||
robots:
|
||||
- name: defaultRobot
|
||||
webhook: "https://oapi.dingtalk.com/robot/send?access_token=80d4401d9e155f060aae73b416a23958eb0f89f7bede9b9dbb00568bd4d611a7"
|
||||
security:
|
||||
enable: true
|
||||
secret: "SECb8d0873db6a56f3c26f98aa527af95d9ac131e65f16bd404b6f8c877af160895"
|
||||
atMobiles: [ "18010816106" ]
|
||||
atAll: true
|
||||
- name: highGradeRobot
|
||||
webhook: "https://oapi.dingtalk.com/robot/send?access_token=80d4401d9e155f060aae73b416a23958eb0f89f7bede9b9dbb00568bd4d611a7"
|
||||
security:
|
||||
enable: true
|
||||
secret: "SECb8d0873db6a56f3c26f98aa527af95d9ac131e65f16bd404b6f8c877af160895"
|
||||
atMobiles: [ ]
|
||||
atAll: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
Loading…
Reference in New Issue
Block a user