This commit is contained in:
N1KO 2025-12-17 19:50:08 +08:00
parent 19872409c5
commit 0d73531bb1
38 changed files with 2197 additions and 4 deletions

View File

View File

@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import top.baogutang.admin.domain.IphoneProductDto; import top.baogutang.admin.domain.IphoneProductDto;
import top.baogutang.admin.utils.DingTalkMsgPushUtils; import top.baogutang.admin.utils.DingTalkMsgPushUtils;
@ -41,7 +42,7 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
@Value("${baogutang.apple.country_code:cn}") @Value("${baogutang.apple.country_code:cn}")
private String countryCode; private String countryCode;
@Value("${baogutang.apple.device_code:15-pro}") @Value("${baogutang.apple.device_code:17-pro}")
private String deviceCode; private String deviceCode;
@Value("${baogutang.apple.location:'上海 上海 闵行区'}") @Value("${baogutang.apple.location:'上海 上海 闵行区'}")
@ -59,6 +60,25 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
@Resource @Resource
private DingTalkMsgPushUtils dingTalkMsgPushUtils; 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) { private void doMonitor(IphoneProductDto product) {
@ -83,7 +103,7 @@ public class AppleInventoryScheduleHandler extends IJobHandler {
JSONObject pickupMessage = responseJsonObject.getJSONObject("body").getJSONObject("content").getJSONObject("pickupMessage"); JSONObject pickupMessage = responseJsonObject.getJSONObject("body").getJSONObject("content").getJSONObject("pickupMessage");
JSONArray stores = pickupMessage.getJSONArray("stores"); JSONArray stores = pickupMessage.getJSONArray("stores");
if (stores == null) { if (stores == null) {
log.debug(pickupMessage.toString()); // log.info(pickupMessage.toString());
return; return;
} }
if (stores.isEmpty()) { if (stores.isEmpty()) {

View File

@ -66,6 +66,7 @@
<div class="col-md-7" style="padding:0;"> <div class="col-md-7" style="padding:0;">
<div style="padding:10px;font-size:16px;border-bottom:solid 1px #ddd;" class="navi"> <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 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 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 <a href="#" class="tip shown" title="显示行号" data-placement="bottom"><i
class="glyphicon glyphicon-sort-by-order"></i></a> class="glyphicon glyphicon-sort-by-order"></i></a>
@ -264,9 +265,80 @@
var timestamp = new Date().getTime(); var timestamp = new Date().getTime();
saveAs(blob, "format." + timestamp + ".json"); 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(); $('#json-src').keyup();
</script> </script>
<footer style="padding:30px;text-align:center;font-family:'JetBrains Mono NL', Monaco, monospace;position:relative;"> <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. href="http://beian.miit.gov.cn">皖ICP备2024050571号</a>All Rights Reserved.
<p></p> <p></p>
</footer> </footer>
<button id="hiddenCopyBtn" style="display:none;" data-clipboard-text=""></button>
</body> </body>
</html> </html>

View 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>

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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) {
}
}
}
}

View File

@ -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());
}
}

View File

@ -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 + "&timestamp=" + 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();
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
});
}
}

View File

@ -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());
}
});
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View 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

View File

@ -17,6 +17,7 @@
<module>baogutang-business</module> <module>baogutang-business</module>
<module>baogutang-common</module> <module>baogutang-common</module>
<module>baogutang-generate</module> <module>baogutang-generate</module>
<module>baogutang-strategy</module>
</modules> </modules>
<properties> <properties>