Spaces:
Sleeping
Sleeping
Commit ·
f5cd2d3
1
Parent(s): 5e92b78
init_files_added
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +5 -0
- .gitignore +36 -0
- .mvn/wrapper/maven-wrapper.properties +3 -0
- Dockerfile +30 -0
- TestYahoo.java +15 -0
- mvnw +295 -0
- mvnw.cmd +189 -0
- pom.xml +144 -0
- src/main/java/com/rods/backtestingstrategies/BacktestingStrategiesApplication.java +13 -0
- src/main/java/com/rods/backtestingstrategies/TestYahooCrumb.java +52 -0
- src/main/java/com/rods/backtestingstrategies/config/CorsConfig.java +28 -0
- src/main/java/com/rods/backtestingstrategies/controller/AuthController.java +59 -0
- src/main/java/com/rods/backtestingstrategies/controller/BacktestController.java +99 -0
- src/main/java/com/rods/backtestingstrategies/controller/MarketController.java +104 -0
- src/main/java/com/rods/backtestingstrategies/controller/SymbolController.java +90 -0
- src/main/java/com/rods/backtestingstrategies/dto/AuthRequest.java +15 -0
- src/main/java/com/rods/backtestingstrategies/dto/AuthResponse.java +14 -0
- src/main/java/com/rods/backtestingstrategies/entity/AuthenticationRequest.java +20 -0
- src/main/java/com/rods/backtestingstrategies/entity/AuthenticationResponse.java +14 -0
- src/main/java/com/rods/backtestingstrategies/entity/BacktestResult.java +52 -0
- src/main/java/com/rods/backtestingstrategies/entity/Candle.java +37 -0
- src/main/java/com/rods/backtestingstrategies/entity/CrossOver.java +60 -0
- src/main/java/com/rods/backtestingstrategies/entity/CrossOverType.java +6 -0
- src/main/java/com/rods/backtestingstrategies/entity/EquityPoint.java +67 -0
- src/main/java/com/rods/backtestingstrategies/entity/PerformanceMetrics.java +53 -0
- src/main/java/com/rods/backtestingstrategies/entity/PortfolioRequest.java +30 -0
- src/main/java/com/rods/backtestingstrategies/entity/PortfolioResult.java +33 -0
- src/main/java/com/rods/backtestingstrategies/entity/QuoteSummary.java +48 -0
- src/main/java/com/rods/backtestingstrategies/entity/RegisterRequest.java +20 -0
- src/main/java/com/rods/backtestingstrategies/entity/SignalType.java +9 -0
- src/main/java/com/rods/backtestingstrategies/entity/Stock.java +22 -0
- src/main/java/com/rods/backtestingstrategies/entity/StockSymbol.java +84 -0
- src/main/java/com/rods/backtestingstrategies/entity/StrategyComparisonResult.java +34 -0
- src/main/java/com/rods/backtestingstrategies/entity/TradeSignal.java +85 -0
- src/main/java/com/rods/backtestingstrategies/entity/Transaction.java +90 -0
- src/main/java/com/rods/backtestingstrategies/entity/User.java +29 -0
- src/main/java/com/rods/backtestingstrategies/repository/CandleRepository.java +22 -0
- src/main/java/com/rods/backtestingstrategies/repository/StockSymbolRepository.java +90 -0
- src/main/java/com/rods/backtestingstrategies/repository/UserRepository.java +10 -0
- src/main/java/com/rods/backtestingstrategies/security/CustomUserDetailsService.java +30 -0
- src/main/java/com/rods/backtestingstrategies/security/JwtAuthenticationFilter.java +59 -0
- src/main/java/com/rods/backtestingstrategies/security/JwtUtils.java +97 -0
- src/main/java/com/rods/backtestingstrategies/security/SecurityConfig.java +77 -0
- src/main/java/com/rods/backtestingstrategies/service/BacktestService.java +419 -0
- src/main/java/com/rods/backtestingstrategies/service/MarketDataService.java +147 -0
- src/main/java/com/rods/backtestingstrategies/service/TickerSeederService.java +111 -0
- src/main/java/com/rods/backtestingstrategies/service/YahooFinanceService.java +156 -0
- src/main/java/com/rods/backtestingstrategies/strategy/BuyAndHoldStrategy.java +41 -0
- src/main/java/com/rods/backtestingstrategies/strategy/MacdStrategy.java +141 -0
- src/main/java/com/rods/backtestingstrategies/strategy/RsiStrategy.java +78 -0
.dockerignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
target
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
node_modules
|
| 5 |
+
.env
|
.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
HELP.md
|
| 2 |
+
target/
|
| 3 |
+
.mvn/wrapper/maven-wrapper.jar
|
| 4 |
+
!**/src/main/**/target/
|
| 5 |
+
!**/src/test/**/target/
|
| 6 |
+
|
| 7 |
+
### STS ###
|
| 8 |
+
.apt_generated
|
| 9 |
+
.classpath
|
| 10 |
+
.factorypath
|
| 11 |
+
.project
|
| 12 |
+
.settings
|
| 13 |
+
.springBeans
|
| 14 |
+
.sts4-cache
|
| 15 |
+
|
| 16 |
+
### IntelliJ IDEA ###
|
| 17 |
+
.idea
|
| 18 |
+
*.iws
|
| 19 |
+
*.iml
|
| 20 |
+
*.ipr
|
| 21 |
+
|
| 22 |
+
### NetBeans ###
|
| 23 |
+
/nbproject/private/
|
| 24 |
+
/nbbuild/
|
| 25 |
+
/dist/
|
| 26 |
+
/nbdist/
|
| 27 |
+
/.nb-gradle/
|
| 28 |
+
build/
|
| 29 |
+
!**/src/main/**/build/
|
| 30 |
+
!**/src/test/**/build/
|
| 31 |
+
|
| 32 |
+
### VS Code ###
|
| 33 |
+
.vscode/
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
docs/
|
.mvn/wrapper/maven-wrapper.properties
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
wrapperVersion=3.3.4
|
| 2 |
+
distributionType=only-script
|
| 3 |
+
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ===============================
|
| 2 |
+
# Build stage (Java 21)
|
| 3 |
+
# ===============================
|
| 4 |
+
FROM maven:3.9.6-eclipse-temurin-21 AS build
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY pom.xml .
|
| 9 |
+
RUN mvn dependency:go-offline
|
| 10 |
+
|
| 11 |
+
COPY src ./src
|
| 12 |
+
RUN mvn clean package -DskipTests
|
| 13 |
+
|
| 14 |
+
# ===============================
|
| 15 |
+
# Runtime stage (Java 21)
|
| 16 |
+
# ===============================
|
| 17 |
+
FROM eclipse-temurin:21-jre-alpine
|
| 18 |
+
|
| 19 |
+
# Hugging Face requires running as a non-root user (user ID 1000)
|
| 20 |
+
RUN addgroup -S appgroup && adduser -S -u 1000 appuser -G appgroup
|
| 21 |
+
USER 1000
|
| 22 |
+
|
| 23 |
+
WORKDIR /app
|
| 24 |
+
|
| 25 |
+
COPY --from=build --chown=appuser:appgroup /app/target/*.jar app.jar
|
| 26 |
+
|
| 27 |
+
# Hugging Face Spaces exposes port 7860
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
ENTRYPOINT ["java","-Dserver.port=7860","-jar","app.jar"]
|
TestYahoo.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yahoofinance.Stock;
|
| 2 |
+
import yahoofinance.YahooFinance;
|
| 3 |
+
import java.io.IOException;
|
| 4 |
+
|
| 5 |
+
public class TestYahoo {
|
| 6 |
+
public static void main(String[] args) throws IOException {
|
| 7 |
+
System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36");
|
| 8 |
+
try {
|
| 9 |
+
Stock stock = YahooFinance.get("AAPL");
|
| 10 |
+
System.out.println("Success! Price: " + stock.getQuote().getPrice());
|
| 11 |
+
} catch (Exception e) {
|
| 12 |
+
e.printStackTrace();
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
mvnw
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
# ----------------------------------------------------------------------------
|
| 3 |
+
# Licensed to the Apache Software Foundation (ASF) under one
|
| 4 |
+
# or more contributor license agreements. See the NOTICE file
|
| 5 |
+
# distributed with this work for additional information
|
| 6 |
+
# regarding copyright ownership. The ASF licenses this file
|
| 7 |
+
# to you under the Apache License, Version 2.0 (the
|
| 8 |
+
# "License"); you may not use this file except in compliance
|
| 9 |
+
# with the License. You may obtain a copy of the License at
|
| 10 |
+
#
|
| 11 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 12 |
+
#
|
| 13 |
+
# Unless required by applicable law or agreed to in writing,
|
| 14 |
+
# software distributed under the License is distributed on an
|
| 15 |
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
| 16 |
+
# KIND, either express or implied. See the License for the
|
| 17 |
+
# specific language governing permissions and limitations
|
| 18 |
+
# under the License.
|
| 19 |
+
# ----------------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
# ----------------------------------------------------------------------------
|
| 22 |
+
# Apache Maven Wrapper startup batch script, version 3.3.4
|
| 23 |
+
#
|
| 24 |
+
# Optional ENV vars
|
| 25 |
+
# -----------------
|
| 26 |
+
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
| 27 |
+
# MVNW_REPOURL - repo url base for downloading maven distribution
|
| 28 |
+
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
| 29 |
+
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
| 30 |
+
# ----------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
set -euf
|
| 33 |
+
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
| 34 |
+
|
| 35 |
+
# OS specific support.
|
| 36 |
+
native_path() { printf %s\\n "$1"; }
|
| 37 |
+
case "$(uname)" in
|
| 38 |
+
CYGWIN* | MINGW*)
|
| 39 |
+
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
| 40 |
+
native_path() { cygpath --path --windows "$1"; }
|
| 41 |
+
;;
|
| 42 |
+
esac
|
| 43 |
+
|
| 44 |
+
# set JAVACMD and JAVACCMD
|
| 45 |
+
set_java_home() {
|
| 46 |
+
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
| 47 |
+
if [ -n "${JAVA_HOME-}" ]; then
|
| 48 |
+
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
| 49 |
+
# IBM's JDK on AIX uses strange locations for the executables
|
| 50 |
+
JAVACMD="$JAVA_HOME/jre/sh/java"
|
| 51 |
+
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
| 52 |
+
else
|
| 53 |
+
JAVACMD="$JAVA_HOME/bin/java"
|
| 54 |
+
JAVACCMD="$JAVA_HOME/bin/javac"
|
| 55 |
+
|
| 56 |
+
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
| 57 |
+
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
| 58 |
+
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
| 59 |
+
return 1
|
| 60 |
+
fi
|
| 61 |
+
fi
|
| 62 |
+
else
|
| 63 |
+
JAVACMD="$(
|
| 64 |
+
'set' +e
|
| 65 |
+
'unset' -f command 2>/dev/null
|
| 66 |
+
'command' -v java
|
| 67 |
+
)" || :
|
| 68 |
+
JAVACCMD="$(
|
| 69 |
+
'set' +e
|
| 70 |
+
'unset' -f command 2>/dev/null
|
| 71 |
+
'command' -v javac
|
| 72 |
+
)" || :
|
| 73 |
+
|
| 74 |
+
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
| 75 |
+
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
| 76 |
+
return 1
|
| 77 |
+
fi
|
| 78 |
+
fi
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# hash string like Java String::hashCode
|
| 82 |
+
hash_string() {
|
| 83 |
+
str="${1:-}" h=0
|
| 84 |
+
while [ -n "$str" ]; do
|
| 85 |
+
char="${str%"${str#?}"}"
|
| 86 |
+
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
| 87 |
+
str="${str#?}"
|
| 88 |
+
done
|
| 89 |
+
printf %x\\n $h
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
verbose() { :; }
|
| 93 |
+
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
| 94 |
+
|
| 95 |
+
die() {
|
| 96 |
+
printf %s\\n "$1" >&2
|
| 97 |
+
exit 1
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
trim() {
|
| 101 |
+
# MWRAPPER-139:
|
| 102 |
+
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
| 103 |
+
# Needed for removing poorly interpreted newline sequences when running in more
|
| 104 |
+
# exotic environments such as mingw bash on Windows.
|
| 105 |
+
printf "%s" "${1}" | tr -d '[:space:]'
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
scriptDir="$(dirname "$0")"
|
| 109 |
+
scriptName="$(basename "$0")"
|
| 110 |
+
|
| 111 |
+
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
| 112 |
+
while IFS="=" read -r key value; do
|
| 113 |
+
case "${key-}" in
|
| 114 |
+
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
| 115 |
+
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
| 116 |
+
esac
|
| 117 |
+
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
| 118 |
+
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
| 119 |
+
|
| 120 |
+
case "${distributionUrl##*/}" in
|
| 121 |
+
maven-mvnd-*bin.*)
|
| 122 |
+
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
| 123 |
+
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
| 124 |
+
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
| 125 |
+
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
| 126 |
+
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
| 127 |
+
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
| 128 |
+
*)
|
| 129 |
+
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
| 130 |
+
distributionPlatform=linux-amd64
|
| 131 |
+
;;
|
| 132 |
+
esac
|
| 133 |
+
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
| 134 |
+
;;
|
| 135 |
+
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
| 136 |
+
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
| 137 |
+
esac
|
| 138 |
+
|
| 139 |
+
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
| 140 |
+
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
| 141 |
+
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
| 142 |
+
distributionUrlName="${distributionUrl##*/}"
|
| 143 |
+
distributionUrlNameMain="${distributionUrlName%.*}"
|
| 144 |
+
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
| 145 |
+
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
| 146 |
+
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
| 147 |
+
|
| 148 |
+
exec_maven() {
|
| 149 |
+
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
| 150 |
+
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
if [ -d "$MAVEN_HOME" ]; then
|
| 154 |
+
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
| 155 |
+
exec_maven "$@"
|
| 156 |
+
fi
|
| 157 |
+
|
| 158 |
+
case "${distributionUrl-}" in
|
| 159 |
+
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
| 160 |
+
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
| 161 |
+
esac
|
| 162 |
+
|
| 163 |
+
# prepare tmp dir
|
| 164 |
+
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
| 165 |
+
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
| 166 |
+
trap clean HUP INT TERM EXIT
|
| 167 |
+
else
|
| 168 |
+
die "cannot create temp dir"
|
| 169 |
+
fi
|
| 170 |
+
|
| 171 |
+
mkdir -p -- "${MAVEN_HOME%/*}"
|
| 172 |
+
|
| 173 |
+
# Download and Install Apache Maven
|
| 174 |
+
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
| 175 |
+
verbose "Downloading from: $distributionUrl"
|
| 176 |
+
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
| 177 |
+
|
| 178 |
+
# select .zip or .tar.gz
|
| 179 |
+
if ! command -v unzip >/dev/null; then
|
| 180 |
+
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
| 181 |
+
distributionUrlName="${distributionUrl##*/}"
|
| 182 |
+
fi
|
| 183 |
+
|
| 184 |
+
# verbose opt
|
| 185 |
+
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
| 186 |
+
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
| 187 |
+
|
| 188 |
+
# normalize http auth
|
| 189 |
+
case "${MVNW_PASSWORD:+has-password}" in
|
| 190 |
+
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
| 191 |
+
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
| 192 |
+
esac
|
| 193 |
+
|
| 194 |
+
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
| 195 |
+
verbose "Found wget ... using wget"
|
| 196 |
+
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
| 197 |
+
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
| 198 |
+
verbose "Found curl ... using curl"
|
| 199 |
+
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
| 200 |
+
elif set_java_home; then
|
| 201 |
+
verbose "Falling back to use Java to download"
|
| 202 |
+
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
| 203 |
+
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
| 204 |
+
cat >"$javaSource" <<-END
|
| 205 |
+
public class Downloader extends java.net.Authenticator
|
| 206 |
+
{
|
| 207 |
+
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
| 208 |
+
{
|
| 209 |
+
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
| 210 |
+
}
|
| 211 |
+
public static void main( String[] args ) throws Exception
|
| 212 |
+
{
|
| 213 |
+
setDefault( new Downloader() );
|
| 214 |
+
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
END
|
| 218 |
+
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
| 219 |
+
verbose " - Compiling Downloader.java ..."
|
| 220 |
+
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
| 221 |
+
verbose " - Running Downloader.java ..."
|
| 222 |
+
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
| 223 |
+
fi
|
| 224 |
+
|
| 225 |
+
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
| 226 |
+
if [ -n "${distributionSha256Sum-}" ]; then
|
| 227 |
+
distributionSha256Result=false
|
| 228 |
+
if [ "$MVN_CMD" = mvnd.sh ]; then
|
| 229 |
+
echo "Checksum validation is not supported for maven-mvnd." >&2
|
| 230 |
+
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
| 231 |
+
exit 1
|
| 232 |
+
elif command -v sha256sum >/dev/null; then
|
| 233 |
+
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
| 234 |
+
distributionSha256Result=true
|
| 235 |
+
fi
|
| 236 |
+
elif command -v shasum >/dev/null; then
|
| 237 |
+
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
| 238 |
+
distributionSha256Result=true
|
| 239 |
+
fi
|
| 240 |
+
else
|
| 241 |
+
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
| 242 |
+
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
| 243 |
+
exit 1
|
| 244 |
+
fi
|
| 245 |
+
if [ $distributionSha256Result = false ]; then
|
| 246 |
+
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
| 247 |
+
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
| 248 |
+
exit 1
|
| 249 |
+
fi
|
| 250 |
+
fi
|
| 251 |
+
|
| 252 |
+
# unzip and move
|
| 253 |
+
if command -v unzip >/dev/null; then
|
| 254 |
+
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
| 255 |
+
else
|
| 256 |
+
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
| 257 |
+
fi
|
| 258 |
+
|
| 259 |
+
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
| 260 |
+
actualDistributionDir=""
|
| 261 |
+
|
| 262 |
+
# First try the expected directory name (for regular distributions)
|
| 263 |
+
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
| 264 |
+
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
| 265 |
+
actualDistributionDir="$distributionUrlNameMain"
|
| 266 |
+
fi
|
| 267 |
+
fi
|
| 268 |
+
|
| 269 |
+
# If not found, search for any directory with the Maven executable (for snapshots)
|
| 270 |
+
if [ -z "$actualDistributionDir" ]; then
|
| 271 |
+
# enable globbing to iterate over items
|
| 272 |
+
set +f
|
| 273 |
+
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
| 274 |
+
if [ -d "$dir" ]; then
|
| 275 |
+
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
| 276 |
+
actualDistributionDir="$(basename "$dir")"
|
| 277 |
+
break
|
| 278 |
+
fi
|
| 279 |
+
fi
|
| 280 |
+
done
|
| 281 |
+
set -f
|
| 282 |
+
fi
|
| 283 |
+
|
| 284 |
+
if [ -z "$actualDistributionDir" ]; then
|
| 285 |
+
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
| 286 |
+
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
| 287 |
+
die "Could not find Maven distribution directory in extracted archive"
|
| 288 |
+
fi
|
| 289 |
+
|
| 290 |
+
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
| 291 |
+
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
| 292 |
+
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
| 293 |
+
|
| 294 |
+
clean || :
|
| 295 |
+
exec_maven "$@"
|
mvnw.cmd
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<# : batch portion
|
| 2 |
+
@REM ----------------------------------------------------------------------------
|
| 3 |
+
@REM Licensed to the Apache Software Foundation (ASF) under one
|
| 4 |
+
@REM or more contributor license agreements. See the NOTICE file
|
| 5 |
+
@REM distributed with this work for additional information
|
| 6 |
+
@REM regarding copyright ownership. The ASF licenses this file
|
| 7 |
+
@REM to you under the Apache License, Version 2.0 (the
|
| 8 |
+
@REM "License"); you may not use this file except in compliance
|
| 9 |
+
@REM with the License. You may obtain a copy of the License at
|
| 10 |
+
@REM
|
| 11 |
+
@REM http://www.apache.org/licenses/LICENSE-2.0
|
| 12 |
+
@REM
|
| 13 |
+
@REM Unless required by applicable law or agreed to in writing,
|
| 14 |
+
@REM software distributed under the License is distributed on an
|
| 15 |
+
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
| 16 |
+
@REM KIND, either express or implied. See the License for the
|
| 17 |
+
@REM specific language governing permissions and limitations
|
| 18 |
+
@REM under the License.
|
| 19 |
+
@REM ----------------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
@REM ----------------------------------------------------------------------------
|
| 22 |
+
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
| 23 |
+
@REM
|
| 24 |
+
@REM Optional ENV vars
|
| 25 |
+
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
| 26 |
+
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
| 27 |
+
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
| 28 |
+
@REM ----------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
+
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
| 31 |
+
@SET __MVNW_CMD__=
|
| 32 |
+
@SET __MVNW_ERROR__=
|
| 33 |
+
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
| 34 |
+
@SET PSModulePath=
|
| 35 |
+
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
| 36 |
+
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
| 37 |
+
)
|
| 38 |
+
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
| 39 |
+
@SET __MVNW_PSMODULEP_SAVE=
|
| 40 |
+
@SET __MVNW_ARG0_NAME__=
|
| 41 |
+
@SET MVNW_USERNAME=
|
| 42 |
+
@SET MVNW_PASSWORD=
|
| 43 |
+
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
| 44 |
+
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
| 45 |
+
@GOTO :EOF
|
| 46 |
+
: end batch / begin powershell #>
|
| 47 |
+
|
| 48 |
+
$ErrorActionPreference = "Stop"
|
| 49 |
+
if ($env:MVNW_VERBOSE -eq "true") {
|
| 50 |
+
$VerbosePreference = "Continue"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
| 54 |
+
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
| 55 |
+
if (!$distributionUrl) {
|
| 56 |
+
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
| 60 |
+
"maven-mvnd-*" {
|
| 61 |
+
$USE_MVND = $true
|
| 62 |
+
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
| 63 |
+
$MVN_CMD = "mvnd.cmd"
|
| 64 |
+
break
|
| 65 |
+
}
|
| 66 |
+
default {
|
| 67 |
+
$USE_MVND = $false
|
| 68 |
+
$MVN_CMD = $script -replace '^mvnw','mvn'
|
| 69 |
+
break
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
| 74 |
+
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
| 75 |
+
if ($env:MVNW_REPOURL) {
|
| 76 |
+
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
| 77 |
+
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
| 78 |
+
}
|
| 79 |
+
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
| 80 |
+
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
| 81 |
+
|
| 82 |
+
$MAVEN_M2_PATH = "$HOME/.m2"
|
| 83 |
+
if ($env:MAVEN_USER_HOME) {
|
| 84 |
+
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
| 88 |
+
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
$MAVEN_WRAPPER_DISTS = $null
|
| 92 |
+
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
| 93 |
+
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
| 94 |
+
} else {
|
| 95 |
+
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
| 99 |
+
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
| 100 |
+
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
| 101 |
+
|
| 102 |
+
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
| 103 |
+
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
| 104 |
+
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
| 105 |
+
exit $?
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
| 109 |
+
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# prepare tmp dir
|
| 113 |
+
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
| 114 |
+
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
| 115 |
+
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
| 116 |
+
trap {
|
| 117 |
+
if ($TMP_DOWNLOAD_DIR.Exists) {
|
| 118 |
+
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
| 119 |
+
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
| 124 |
+
|
| 125 |
+
# Download and Install Apache Maven
|
| 126 |
+
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
| 127 |
+
Write-Verbose "Downloading from: $distributionUrl"
|
| 128 |
+
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
| 129 |
+
|
| 130 |
+
$webclient = New-Object System.Net.WebClient
|
| 131 |
+
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
| 132 |
+
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
| 133 |
+
}
|
| 134 |
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
| 135 |
+
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
| 136 |
+
|
| 137 |
+
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
| 138 |
+
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
| 139 |
+
if ($distributionSha256Sum) {
|
| 140 |
+
if ($USE_MVND) {
|
| 141 |
+
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
| 142 |
+
}
|
| 143 |
+
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
| 144 |
+
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
| 145 |
+
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
# unzip and move
|
| 150 |
+
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
| 151 |
+
|
| 152 |
+
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
| 153 |
+
$actualDistributionDir = ""
|
| 154 |
+
|
| 155 |
+
# First try the expected directory name (for regular distributions)
|
| 156 |
+
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
| 157 |
+
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
| 158 |
+
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
| 159 |
+
$actualDistributionDir = $distributionUrlNameMain
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
# If not found, search for any directory with the Maven executable (for snapshots)
|
| 163 |
+
if (!$actualDistributionDir) {
|
| 164 |
+
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
| 165 |
+
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
| 166 |
+
if (Test-Path -Path $testPath -PathType Leaf) {
|
| 167 |
+
$actualDistributionDir = $_.Name
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if (!$actualDistributionDir) {
|
| 173 |
+
Write-Error "Could not find Maven distribution directory in extracted archive"
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
| 177 |
+
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
| 178 |
+
try {
|
| 179 |
+
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
| 180 |
+
} catch {
|
| 181 |
+
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
| 182 |
+
Write-Error "fail to move MAVEN_HOME"
|
| 183 |
+
}
|
| 184 |
+
} finally {
|
| 185 |
+
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
| 186 |
+
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
pom.xml
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
| 3 |
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
| 4 |
+
<modelVersion>4.0.0</modelVersion>
|
| 5 |
+
<parent>
|
| 6 |
+
<groupId>org.springframework.boot</groupId>
|
| 7 |
+
<artifactId>spring-boot-starter-parent</artifactId>
|
| 8 |
+
<version>3.5.7</version>
|
| 9 |
+
<relativePath/> <!-- lookup parent from repository -->
|
| 10 |
+
</parent>
|
| 11 |
+
<groupId>com.rods</groupId>
|
| 12 |
+
<artifactId>BacktestingStrategies</artifactId>
|
| 13 |
+
<version>0.0.1-SNAPSHOT</version>
|
| 14 |
+
<name>BacktestingStrategies</name>
|
| 15 |
+
<description>BacktestingStrategies</description>
|
| 16 |
+
<url/>
|
| 17 |
+
<licenses>
|
| 18 |
+
<license/>
|
| 19 |
+
</licenses>
|
| 20 |
+
<developers>
|
| 21 |
+
<developer/>
|
| 22 |
+
</developers>
|
| 23 |
+
<scm>
|
| 24 |
+
<connection/>
|
| 25 |
+
<developerConnection/>
|
| 26 |
+
<tag/>
|
| 27 |
+
<url/>
|
| 28 |
+
</scm>
|
| 29 |
+
<properties>
|
| 30 |
+
<java.version>21</java.version>
|
| 31 |
+
</properties>
|
| 32 |
+
<!-- adding the jit repo for discovering the java implementation repository for alpha-vantage -->
|
| 33 |
+
<!-- Replaced AlphaVantage with Yahoo Finance -->
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
<dependencies>
|
| 37 |
+
<dependency>
|
| 38 |
+
<groupId>org.springframework.boot</groupId>
|
| 39 |
+
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
| 40 |
+
</dependency>
|
| 41 |
+
<dependency>
|
| 42 |
+
<groupId>org.springframework.boot</groupId>
|
| 43 |
+
<artifactId>spring-boot-starter-web</artifactId>
|
| 44 |
+
|
| 45 |
+
</dependency>
|
| 46 |
+
|
| 47 |
+
<dependency>
|
| 48 |
+
<groupId>org.springframework.boot</groupId>
|
| 49 |
+
<artifactId>spring-boot-starter-test</artifactId>
|
| 50 |
+
<scope>test</scope>
|
| 51 |
+
</dependency>
|
| 52 |
+
|
| 53 |
+
<dependency>
|
| 54 |
+
<groupId>org.projectlombok</groupId>
|
| 55 |
+
<artifactId>lombok</artifactId>
|
| 56 |
+
<optional>true</optional>
|
| 57 |
+
</dependency>
|
| 58 |
+
<dependency>
|
| 59 |
+
<groupId>com.mysql</groupId>
|
| 60 |
+
<artifactId>mysql-connector-j</artifactId>
|
| 61 |
+
<scope>runtime</scope>
|
| 62 |
+
</dependency>
|
| 63 |
+
|
| 64 |
+
<!-- Yahoo Finance API - no API key required, no rate limits -->
|
| 65 |
+
<dependency>
|
| 66 |
+
<groupId>com.yahoofinance-api</groupId>
|
| 67 |
+
<artifactId>YahooFinanceAPI</artifactId>
|
| 68 |
+
<version>3.17.0</version>
|
| 69 |
+
</dependency>
|
| 70 |
+
|
| 71 |
+
<!-- Jackson for JSON parsing of ticker data -->
|
| 72 |
+
<dependency>
|
| 73 |
+
<groupId>com.fasterxml.jackson.core</groupId>
|
| 74 |
+
<artifactId>jackson-databind</artifactId>
|
| 75 |
+
</dependency>
|
| 76 |
+
|
| 77 |
+
<!-- Security & JWT Dependencies -->
|
| 78 |
+
<dependency>
|
| 79 |
+
<groupId>org.springframework.boot</groupId>
|
| 80 |
+
<artifactId>spring-boot-starter-security</artifactId>
|
| 81 |
+
</dependency>
|
| 82 |
+
<dependency>
|
| 83 |
+
<groupId>org.springframework.boot</groupId>
|
| 84 |
+
<artifactId>spring-boot-starter-validation</artifactId>
|
| 85 |
+
</dependency>
|
| 86 |
+
<dependency>
|
| 87 |
+
<groupId>io.jsonwebtoken</groupId>
|
| 88 |
+
<artifactId>jjwt-api</artifactId>
|
| 89 |
+
<version>0.11.5</version>
|
| 90 |
+
</dependency>
|
| 91 |
+
<dependency>
|
| 92 |
+
<groupId>io.jsonwebtoken</groupId>
|
| 93 |
+
<artifactId>jjwt-impl</artifactId>
|
| 94 |
+
<version>0.11.5</version>
|
| 95 |
+
<scope>runtime</scope>
|
| 96 |
+
</dependency>
|
| 97 |
+
<dependency>
|
| 98 |
+
<groupId>io.jsonwebtoken</groupId>
|
| 99 |
+
<artifactId>jjwt-jackson</artifactId>
|
| 100 |
+
<version>0.11.5</version>
|
| 101 |
+
<scope>runtime</scope>
|
| 102 |
+
</dependency>
|
| 103 |
+
<!-- Adding postgres dependency for deployment -->
|
| 104 |
+
<dependency>
|
| 105 |
+
<groupId>org.postgresql</groupId>
|
| 106 |
+
<artifactId>postgresql</artifactId>
|
| 107 |
+
<scope>runtime</scope>
|
| 108 |
+
</dependency>
|
| 109 |
+
|
| 110 |
+
</dependencies>
|
| 111 |
+
|
| 112 |
+
<build>
|
| 113 |
+
<plugins>
|
| 114 |
+
<plugin>
|
| 115 |
+
<groupId>org.springframework.boot</groupId>
|
| 116 |
+
<artifactId>spring-boot-maven-plugin</artifactId>
|
| 117 |
+
<configuration>
|
| 118 |
+
<excludes>
|
| 119 |
+
<exclude>
|
| 120 |
+
<groupId>org.projectlombok</groupId>
|
| 121 |
+
<artifactId>lombok</artifactId>
|
| 122 |
+
</exclude>
|
| 123 |
+
</excludes>
|
| 124 |
+
</configuration>
|
| 125 |
+
</plugin>
|
| 126 |
+
<plugin>
|
| 127 |
+
<groupId>org.apache.maven.plugins</groupId>
|
| 128 |
+
<artifactId>maven-compiler-plugin</artifactId>
|
| 129 |
+
<configuration>
|
| 130 |
+
<source>21</source>
|
| 131 |
+
<target>21</target>
|
| 132 |
+
<annotationProcessorPaths>
|
| 133 |
+
<path>
|
| 134 |
+
<groupId>org.projectlombok</groupId>
|
| 135 |
+
<artifactId>lombok</artifactId>
|
| 136 |
+
<version>1.18.42</version>
|
| 137 |
+
</path>
|
| 138 |
+
</annotationProcessorPaths>
|
| 139 |
+
</configuration>
|
| 140 |
+
</plugin>
|
| 141 |
+
</plugins>
|
| 142 |
+
</build>
|
| 143 |
+
|
| 144 |
+
</project>
|
src/main/java/com/rods/backtestingstrategies/BacktestingStrategiesApplication.java
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies;
|
| 2 |
+
|
| 3 |
+
import org.springframework.boot.SpringApplication;
|
| 4 |
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
| 5 |
+
|
| 6 |
+
@SpringBootApplication
|
| 7 |
+
public class BacktestingStrategiesApplication {
|
| 8 |
+
|
| 9 |
+
public static void main(String[] args) {
|
| 10 |
+
SpringApplication.run(BacktestingStrategiesApplication.class, args);
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
}
|
src/main/java/com/rods/backtestingstrategies/TestYahooCrumb.java
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import java.net.CookieManager;
|
| 2 |
+
import java.net.CookiePolicy;
|
| 3 |
+
import java.net.URI;
|
| 4 |
+
import java.net.http.HttpClient;
|
| 5 |
+
import java.net.http.HttpRequest;
|
| 6 |
+
import java.net.http.HttpResponse;
|
| 7 |
+
import yahoofinance.YahooFinance;
|
| 8 |
+
import yahoofinance.Stock;
|
| 9 |
+
|
| 10 |
+
public class TestYahooCrumb {
|
| 11 |
+
public static void main(String[] args) {
|
| 12 |
+
try {
|
| 13 |
+
System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36");
|
| 14 |
+
|
| 15 |
+
CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
|
| 16 |
+
java.net.CookieHandler.setDefault(cookieManager);
|
| 17 |
+
|
| 18 |
+
HttpClient client = HttpClient.newBuilder()
|
| 19 |
+
.cookieHandler(cookieManager)
|
| 20 |
+
.followRedirects(HttpClient.Redirect.NORMAL)
|
| 21 |
+
.build();
|
| 22 |
+
|
| 23 |
+
// 1. Get Cookie
|
| 24 |
+
HttpRequest req1 = HttpRequest.newBuilder()
|
| 25 |
+
.uri(URI.create("https://fc.yahoo.com"))
|
| 26 |
+
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
| 27 |
+
.GET()
|
| 28 |
+
.build();
|
| 29 |
+
client.send(req1, HttpResponse.BodyHandlers.discarding());
|
| 30 |
+
System.out.println("Got cookies: " + cookieManager.getCookieStore().getCookies());
|
| 31 |
+
|
| 32 |
+
// 2. Get Crumb
|
| 33 |
+
HttpRequest req2 = HttpRequest.newBuilder()
|
| 34 |
+
.uri(URI.create("https://query1.finance.yahoo.com/v1/test/getcrumb"))
|
| 35 |
+
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
| 36 |
+
.GET()
|
| 37 |
+
.build();
|
| 38 |
+
HttpResponse<String> res2 = client.send(req2, HttpResponse.BodyHandlers.ofString());
|
| 39 |
+
String crumb = res2.body();
|
| 40 |
+
System.out.println("Got crumb: " + crumb);
|
| 41 |
+
|
| 42 |
+
// Set crumb internally for yahoofinance-api if possible, using reflection if needed
|
| 43 |
+
Class.forName("yahoofinance.histquotes2.CrumbManager").getDeclaredMethod("setCrumb", String.class).invoke(null, crumb);
|
| 44 |
+
|
| 45 |
+
Stock stock = YahooFinance.get("AAPL");
|
| 46 |
+
System.out.println("Success! Price: " + stock.getQuote().getPrice());
|
| 47 |
+
|
| 48 |
+
} catch (Exception e) {
|
| 49 |
+
e.printStackTrace();
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
src/main/java/com/rods/backtestingstrategies/config/CorsConfig.java
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.config;
|
| 2 |
+
|
| 3 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 4 |
+
import org.springframework.context.annotation.Bean;
|
| 5 |
+
import org.springframework.context.annotation.Configuration;
|
| 6 |
+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
| 7 |
+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
| 8 |
+
|
| 9 |
+
@Configuration
|
| 10 |
+
public class CorsConfig {
|
| 11 |
+
|
| 12 |
+
@Bean
|
| 13 |
+
public WebMvcConfigurer corsConfigurer() {
|
| 14 |
+
return new WebMvcConfigurer() {
|
| 15 |
+
@Override
|
| 16 |
+
public void addCorsMappings(CorsRegistry registry) {
|
| 17 |
+
registry.addMapping("/**")
|
| 18 |
+
.allowedOrigins(
|
| 19 |
+
"https://backtest-livid.vercel.app",
|
| 20 |
+
"http://localhost:3000",
|
| 21 |
+
"http://localhost:5173")
|
| 22 |
+
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
| 23 |
+
.allowedHeaders("*")
|
| 24 |
+
.allowCredentials(true);
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
}
|
| 28 |
+
}
|
src/main/java/com/rods/backtestingstrategies/controller/AuthController.java
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.controller;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.dto.AuthRequest;
|
| 4 |
+
import com.rods.backtestingstrategies.dto.AuthResponse;
|
| 5 |
+
import com.rods.backtestingstrategies.entity.User;
|
| 6 |
+
import com.rods.backtestingstrategies.repository.UserRepository;
|
| 7 |
+
import com.rods.backtestingstrategies.security.JwtUtils;
|
| 8 |
+
import lombok.RequiredArgsConstructor;
|
| 9 |
+
import org.springframework.http.ResponseEntity;
|
| 10 |
+
import org.springframework.security.authentication.AuthenticationManager;
|
| 11 |
+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
| 12 |
+
import org.springframework.security.core.userdetails.UserDetails;
|
| 13 |
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
| 14 |
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
| 15 |
+
import org.springframework.web.bind.annotation.*;
|
| 16 |
+
|
| 17 |
+
@RestController
|
| 18 |
+
@RequestMapping("/api/auth")
|
| 19 |
+
@RequiredArgsConstructor
|
| 20 |
+
public class AuthController {
|
| 21 |
+
|
| 22 |
+
private final AuthenticationManager authenticationManager;
|
| 23 |
+
private final UserRepository userRepository;
|
| 24 |
+
private final PasswordEncoder passwordEncoder;
|
| 25 |
+
private final JwtUtils jwtUtils;
|
| 26 |
+
private final UserDetailsService userDetailsService;
|
| 27 |
+
|
| 28 |
+
@PostMapping("/register")
|
| 29 |
+
public ResponseEntity<?> register(@RequestBody AuthRequest request) {
|
| 30 |
+
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
|
| 31 |
+
return ResponseEntity.badRequest().body("Username already exists");
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
User user = User.builder()
|
| 35 |
+
.username(request.getUsername())
|
| 36 |
+
.password(passwordEncoder.encode(request.getPassword()))
|
| 37 |
+
.role("USER")
|
| 38 |
+
.build();
|
| 39 |
+
|
| 40 |
+
userRepository.save(user);
|
| 41 |
+
|
| 42 |
+
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
|
| 43 |
+
String jwtToken = jwtUtils.generateToken(userDetails);
|
| 44 |
+
|
| 45 |
+
return ResponseEntity.ok(AuthResponse.builder().token(jwtToken).build());
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@PostMapping("/login")
|
| 49 |
+
public ResponseEntity<?> login(@RequestBody AuthRequest request) {
|
| 50 |
+
authenticationManager.authenticate(
|
| 51 |
+
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
| 52 |
+
);
|
| 53 |
+
|
| 54 |
+
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
|
| 55 |
+
String jwtToken = jwtUtils.generateToken(userDetails);
|
| 56 |
+
|
| 57 |
+
return ResponseEntity.ok(AuthResponse.builder().token(jwtToken).build());
|
| 58 |
+
}
|
| 59 |
+
}
|
src/main/java/com/rods/backtestingstrategies/controller/BacktestController.java
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.controller;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.BacktestResult;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.PortfolioRequest;
|
| 5 |
+
import com.rods.backtestingstrategies.entity.PortfolioResult;
|
| 6 |
+
import com.rods.backtestingstrategies.entity.StrategyComparisonResult;
|
| 7 |
+
import com.rods.backtestingstrategies.service.BacktestService;
|
| 8 |
+
import com.rods.backtestingstrategies.strategy.StrategyType;
|
| 9 |
+
import lombok.RequiredArgsConstructor;
|
| 10 |
+
import org.springframework.http.ResponseEntity;
|
| 11 |
+
import org.springframework.web.bind.annotation.*;
|
| 12 |
+
|
| 13 |
+
import java.util.HashMap;
|
| 14 |
+
import java.util.Map;
|
| 15 |
+
|
| 16 |
+
@RestController
|
| 17 |
+
@RequestMapping("/api/backtest")
|
| 18 |
+
@CrossOrigin
|
| 19 |
+
@RequiredArgsConstructor
|
| 20 |
+
public class BacktestController {
|
| 21 |
+
|
| 22 |
+
private final BacktestService backtestService;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Run a backtest for a given symbol using a strategy.
|
| 26 |
+
* Supports optional custom strategy parameters.
|
| 27 |
+
*
|
| 28 |
+
* Examples:
|
| 29 |
+
* POST /api/backtest/AAPL?strategy=SMA&capital=100000
|
| 30 |
+
* POST /api/backtest/AAPL?strategy=SMA&capital=100000&shortPeriod=10&longPeriod=30
|
| 31 |
+
* POST /api/backtest/AAPL?strategy=MACD&fastPeriod=8&slowPeriod=21&signalPeriod=5
|
| 32 |
+
*/
|
| 33 |
+
@PostMapping("/{symbol}")
|
| 34 |
+
public ResponseEntity<BacktestResult> runBacktest(
|
| 35 |
+
@PathVariable String symbol,
|
| 36 |
+
@RequestParam(defaultValue = "SMA") StrategyType strategy,
|
| 37 |
+
@RequestParam(defaultValue = "100000") double capital,
|
| 38 |
+
@RequestParam(required = false) Integer shortPeriod,
|
| 39 |
+
@RequestParam(required = false) Integer longPeriod,
|
| 40 |
+
@RequestParam(required = false) Integer fastPeriod,
|
| 41 |
+
@RequestParam(required = false) Integer slowPeriod,
|
| 42 |
+
@RequestParam(required = false) Integer signalPeriod
|
| 43 |
+
) {
|
| 44 |
+
// Check if any custom params were provided
|
| 45 |
+
Map<String, String> params = new HashMap<>();
|
| 46 |
+
if (shortPeriod != null) params.put("shortPeriod", String.valueOf(shortPeriod));
|
| 47 |
+
if (longPeriod != null) params.put("longPeriod", String.valueOf(longPeriod));
|
| 48 |
+
if (fastPeriod != null) params.put("fastPeriod", String.valueOf(fastPeriod));
|
| 49 |
+
if (slowPeriod != null) params.put("slowPeriod", String.valueOf(slowPeriod));
|
| 50 |
+
if (signalPeriod != null) params.put("signalPeriod", String.valueOf(signalPeriod));
|
| 51 |
+
|
| 52 |
+
BacktestResult result;
|
| 53 |
+
if (params.isEmpty()) {
|
| 54 |
+
result = backtestService.backtest(symbol, strategy, capital);
|
| 55 |
+
} else {
|
| 56 |
+
result = backtestService.backtestWithParams(symbol, strategy, capital, params);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return ResponseEntity.ok(result);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Compare ALL available strategies on the same stock data.
|
| 64 |
+
* Returns rankings by return % and Sharpe Ratio.
|
| 65 |
+
*
|
| 66 |
+
* POST /api/backtest/compare/AAPL?capital=100000
|
| 67 |
+
*/
|
| 68 |
+
@PostMapping("/compare/{symbol}")
|
| 69 |
+
public ResponseEntity<StrategyComparisonResult> compareStrategies(
|
| 70 |
+
@PathVariable String symbol,
|
| 71 |
+
@RequestParam(defaultValue = "100000") double capital
|
| 72 |
+
) {
|
| 73 |
+
StrategyComparisonResult result = backtestService.compareStrategies(symbol, capital);
|
| 74 |
+
return ResponseEntity.ok(result);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Run a portfolio-level backtest across multiple stocks.
|
| 79 |
+
*
|
| 80 |
+
* POST /api/backtest/portfolio
|
| 81 |
+
* Body:
|
| 82 |
+
* {
|
| 83 |
+
* "entries": [
|
| 84 |
+
* {"symbol": "AAPL", "weight": 0.4},
|
| 85 |
+
* {"symbol": "MSFT", "weight": 0.3},
|
| 86 |
+
* {"symbol": "GOOGL", "weight": 0.3}
|
| 87 |
+
* ],
|
| 88 |
+
* "totalCapital": 100000,
|
| 89 |
+
* "strategy": "SMA"
|
| 90 |
+
* }
|
| 91 |
+
*/
|
| 92 |
+
@PostMapping("/portfolio")
|
| 93 |
+
public ResponseEntity<PortfolioResult> runPortfolioBacktest(
|
| 94 |
+
@RequestBody PortfolioRequest request
|
| 95 |
+
) {
|
| 96 |
+
PortfolioResult result = backtestService.backtestPortfolio(request);
|
| 97 |
+
return ResponseEntity.ok(result);
|
| 98 |
+
}
|
| 99 |
+
}
|
src/main/java/com/rods/backtestingstrategies/controller/MarketController.java
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.controller;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.QuoteSummary;
|
| 5 |
+
import com.rods.backtestingstrategies.service.MarketDataService;
|
| 6 |
+
import com.rods.backtestingstrategies.service.TickerSeederService;
|
| 7 |
+
import com.rods.backtestingstrategies.service.YahooFinanceService;
|
| 8 |
+
import org.springframework.http.ResponseEntity;
|
| 9 |
+
import org.springframework.web.bind.annotation.*;
|
| 10 |
+
import yahoofinance.Stock;
|
| 11 |
+
import yahoofinance.quotes.stock.StockQuote;
|
| 12 |
+
import yahoofinance.quotes.stock.StockStats;
|
| 13 |
+
|
| 14 |
+
import java.io.IOException;
|
| 15 |
+
import java.math.BigDecimal;
|
| 16 |
+
import java.util.List;
|
| 17 |
+
import java.util.Map;
|
| 18 |
+
|
| 19 |
+
@RestController
|
| 20 |
+
@RequestMapping("/api/market")
|
| 21 |
+
@CrossOrigin
|
| 22 |
+
public class MarketController {
|
| 23 |
+
|
| 24 |
+
private final YahooFinanceService yahooFinanceService;
|
| 25 |
+
private final MarketDataService marketDataService;
|
| 26 |
+
private final TickerSeederService tickerSeederService;
|
| 27 |
+
|
| 28 |
+
public MarketController(YahooFinanceService yahooFinanceService,
|
| 29 |
+
MarketDataService marketDataService,
|
| 30 |
+
TickerSeederService tickerSeederService) {
|
| 31 |
+
this.yahooFinanceService = yahooFinanceService;
|
| 32 |
+
this.marketDataService = marketDataService;
|
| 33 |
+
this.tickerSeederService = tickerSeederService;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Fetch & cache candle data for a stock symbol.
|
| 38 |
+
* Uses DB-first approach with Yahoo Finance sync.
|
| 39 |
+
*/
|
| 40 |
+
@GetMapping("/stock/{symbol}")
|
| 41 |
+
public ResponseEntity<List<Candle>> getDailyStockData(@PathVariable String symbol) {
|
| 42 |
+
System.out.println("Request Received for: " + symbol);
|
| 43 |
+
return ResponseEntity.ok(marketDataService.getCandles(symbol));
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Get real-time quote summary from Yahoo Finance.
|
| 48 |
+
* Includes price, change, volume, 52W range, fundamentals.
|
| 49 |
+
*/
|
| 50 |
+
@GetMapping("/quote/{symbol}")
|
| 51 |
+
public ResponseEntity<?> getQuote(@PathVariable String symbol) {
|
| 52 |
+
try {
|
| 53 |
+
Stock stock = yahooFinanceService.getStock(symbol);
|
| 54 |
+
if (stock == null || stock.getQuote() == null) {
|
| 55 |
+
return ResponseEntity.notFound().build();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
StockQuote quote = stock.getQuote();
|
| 59 |
+
StockStats stats = stock.getStats();
|
| 60 |
+
|
| 61 |
+
QuoteSummary summary = QuoteSummary.builder()
|
| 62 |
+
.symbol(stock.getSymbol())
|
| 63 |
+
.name(stock.getName())
|
| 64 |
+
.exchange(stock.getStockExchange())
|
| 65 |
+
.currency(stock.getCurrency())
|
| 66 |
+
.price(quote.getPrice())
|
| 67 |
+
.change(quote.getChange())
|
| 68 |
+
.changePercent(quote.getChangeInPercent())
|
| 69 |
+
.previousClose(quote.getPreviousClose())
|
| 70 |
+
.open(quote.getOpen())
|
| 71 |
+
.dayHigh(quote.getDayHigh())
|
| 72 |
+
.dayLow(quote.getDayLow())
|
| 73 |
+
.volume(quote.getVolume())
|
| 74 |
+
.avgVolume(quote.getAvgVolume())
|
| 75 |
+
.yearHigh(quote.getYearHigh())
|
| 76 |
+
.yearLow(quote.getYearLow())
|
| 77 |
+
.marketCap(stats != null ? stats.getMarketCap() : null)
|
| 78 |
+
.pe(stats != null ? stats.getPe() : null)
|
| 79 |
+
.eps(stats != null ? stats.getEps() : null)
|
| 80 |
+
.priceToBook(stats != null ? stats.getPriceBook() : null)
|
| 81 |
+
.bookValue(stats != null ? stats.getBookValuePerShare() : null)
|
| 82 |
+
.dividendYield(stock.getDividend() != null ?
|
| 83 |
+
stock.getDividend().getAnnualYieldPercent() : null)
|
| 84 |
+
.build();
|
| 85 |
+
|
| 86 |
+
return ResponseEntity.ok(summary);
|
| 87 |
+
} catch (IOException e) {
|
| 88 |
+
return ResponseEntity.internalServerError()
|
| 89 |
+
.body(Map.of("error", "Failed to fetch quote: " + e.getMessage()));
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Manually trigger a re-seed of the ticker database.
|
| 95 |
+
*/
|
| 96 |
+
@PostMapping("/reseed-tickers")
|
| 97 |
+
public ResponseEntity<Map<String, Object>> reseedTickers() {
|
| 98 |
+
int count = tickerSeederService.reseedTickers();
|
| 99 |
+
return ResponseEntity.ok(Map.of(
|
| 100 |
+
"message", "Ticker database re-seeded successfully",
|
| 101 |
+
"totalSymbols", count
|
| 102 |
+
));
|
| 103 |
+
}
|
| 104 |
+
}
|
src/main/java/com/rods/backtestingstrategies/controller/SymbolController.java
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.controller;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.StockSymbol;
|
| 4 |
+
import com.rods.backtestingstrategies.repository.StockSymbolRepository;
|
| 5 |
+
import com.rods.backtestingstrategies.service.MarketDataService;
|
| 6 |
+
import org.springframework.web.bind.annotation.*;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
import java.util.Map;
|
| 10 |
+
|
| 11 |
+
@RestController
|
| 12 |
+
@RequestMapping("/api/symbols")
|
| 13 |
+
@CrossOrigin
|
| 14 |
+
public class SymbolController {
|
| 15 |
+
|
| 16 |
+
private final MarketDataService marketDataService;
|
| 17 |
+
private final StockSymbolRepository symbolRepository;
|
| 18 |
+
|
| 19 |
+
public SymbolController(MarketDataService marketDataService,
|
| 20 |
+
StockSymbolRepository symbolRepository) {
|
| 21 |
+
this.marketDataService = marketDataService;
|
| 22 |
+
this.symbolRepository = symbolRepository;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Search symbols by keyword (matches symbol prefix or company name).
|
| 27 |
+
* Optionally filter by exchange.
|
| 28 |
+
*/
|
| 29 |
+
@GetMapping("/search")
|
| 30 |
+
public List<StockSymbol> searchSymbols(
|
| 31 |
+
@RequestParam String query,
|
| 32 |
+
@RequestParam(required = false) String exchange
|
| 33 |
+
) {
|
| 34 |
+
System.out.println("Symbol search request: query=" + query + ", exchange=" + exchange);
|
| 35 |
+
|
| 36 |
+
if (query == null || query.length() < 1) {
|
| 37 |
+
return List.of();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
if (exchange != null && !exchange.isBlank()) {
|
| 41 |
+
return marketDataService.searchSymbolsByExchange(query.trim(), exchange.trim());
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return marketDataService.searchSymbols(query.trim());
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Get all symbols for a specific exchange.
|
| 49 |
+
*/
|
| 50 |
+
@GetMapping("/exchange/{exchange}")
|
| 51 |
+
public List<StockSymbol> getByExchange(@PathVariable String exchange) {
|
| 52 |
+
return marketDataService.getSymbolsByExchange(exchange);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Get symbols filtered by sector.
|
| 57 |
+
*/
|
| 58 |
+
@GetMapping("/sector/{sector}")
|
| 59 |
+
public List<StockSymbol> getBySector(@PathVariable String sector) {
|
| 60 |
+
return symbolRepository.findBySector(sector);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* Get all available exchanges.
|
| 65 |
+
*/
|
| 66 |
+
@GetMapping("/exchanges")
|
| 67 |
+
public List<String> getExchanges() {
|
| 68 |
+
return symbolRepository.findAllExchanges();
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Get all available sectors.
|
| 73 |
+
*/
|
| 74 |
+
@GetMapping("/sectors")
|
| 75 |
+
public List<String> getSectors() {
|
| 76 |
+
return symbolRepository.findAllSectors();
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Get summary stats about the ticker database.
|
| 81 |
+
*/
|
| 82 |
+
@GetMapping("/stats")
|
| 83 |
+
public Map<String, Object> getStats() {
|
| 84 |
+
return Map.of(
|
| 85 |
+
"totalSymbols", symbolRepository.count(),
|
| 86 |
+
"exchanges", symbolRepository.findAllExchanges(),
|
| 87 |
+
"sectors", symbolRepository.findAllSectors()
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
}
|
src/main/java/com/rods/backtestingstrategies/dto/AuthRequest.java
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
@Data
|
| 9 |
+
@Builder
|
| 10 |
+
@NoArgsConstructor
|
| 11 |
+
@AllArgsConstructor
|
| 12 |
+
public class AuthRequest {
|
| 13 |
+
private String username;
|
| 14 |
+
private String password;
|
| 15 |
+
}
|
src/main/java/com/rods/backtestingstrategies/dto/AuthResponse.java
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.dto;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
@Data
|
| 9 |
+
@Builder
|
| 10 |
+
@NoArgsConstructor
|
| 11 |
+
@AllArgsConstructor
|
| 12 |
+
public class AuthResponse {
|
| 13 |
+
private String token;
|
| 14 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/AuthenticationRequest.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.validation.constraints.NotBlank;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
@Data
|
| 10 |
+
@Builder
|
| 11 |
+
@AllArgsConstructor
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
public class AuthenticationRequest {
|
| 14 |
+
|
| 15 |
+
@NotBlank(message = "Username is required")
|
| 16 |
+
private String username;
|
| 17 |
+
|
| 18 |
+
@NotBlank(message = "Password is required")
|
| 19 |
+
private String password;
|
| 20 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/AuthenticationResponse.java
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
@Data
|
| 9 |
+
@Builder
|
| 10 |
+
@AllArgsConstructor
|
| 11 |
+
@NoArgsConstructor
|
| 12 |
+
public class AuthenticationResponse {
|
| 13 |
+
private String token;
|
| 14 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/BacktestResult.java
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.Builder;
|
| 4 |
+
import lombok.Getter;
|
| 5 |
+
import lombok.ToString;
|
| 6 |
+
|
| 7 |
+
import java.util.List;
|
| 8 |
+
|
| 9 |
+
@Getter
|
| 10 |
+
@Builder
|
| 11 |
+
@ToString
|
| 12 |
+
public class BacktestResult {
|
| 13 |
+
|
| 14 |
+
// Capital metrics
|
| 15 |
+
private final double startCapital;
|
| 16 |
+
private final double finalCapital;
|
| 17 |
+
private final double profitLoss;
|
| 18 |
+
private final double returnPct;
|
| 19 |
+
|
| 20 |
+
// Advanced performance metrics
|
| 21 |
+
private final PerformanceMetrics metrics;
|
| 22 |
+
|
| 23 |
+
// Strategy name used
|
| 24 |
+
private final String strategyName;
|
| 25 |
+
|
| 26 |
+
// Time-series equity curve
|
| 27 |
+
private final List<EquityPoint> equityCurve;
|
| 28 |
+
|
| 29 |
+
// Executed trades
|
| 30 |
+
private final List<Transaction> transactions;
|
| 31 |
+
|
| 32 |
+
// Bullish / Bearish crossover events
|
| 33 |
+
private final List<CrossOver> crossovers;
|
| 34 |
+
|
| 35 |
+
/* ==========================
|
| 36 |
+
Factory Helpers
|
| 37 |
+
========================== */
|
| 38 |
+
|
| 39 |
+
public static BacktestResult empty(double capital) {
|
| 40 |
+
return BacktestResult.builder()
|
| 41 |
+
.startCapital(capital)
|
| 42 |
+
.finalCapital(capital)
|
| 43 |
+
.profitLoss(0.0)
|
| 44 |
+
.returnPct(0.0)
|
| 45 |
+
.strategyName("N/A")
|
| 46 |
+
.metrics(PerformanceMetrics.builder().build())
|
| 47 |
+
.equityCurve(List.of())
|
| 48 |
+
.transactions(List.of())
|
| 49 |
+
.crossovers(List.of())
|
| 50 |
+
.build();
|
| 51 |
+
}
|
| 52 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/Candle.java
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDate;
|
| 9 |
+
|
| 10 |
+
@Entity
|
| 11 |
+
@Data
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
@AllArgsConstructor
|
| 14 |
+
@Table(
|
| 15 |
+
name = "candles",
|
| 16 |
+
uniqueConstraints = {
|
| 17 |
+
@UniqueConstraint(columnNames = {"symbol", "date"})
|
| 18 |
+
}
|
| 19 |
+
)
|
| 20 |
+
public class Candle {
|
| 21 |
+
|
| 22 |
+
@Id
|
| 23 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 24 |
+
private Long id;
|
| 25 |
+
// We are using this Candle Entity to store the data related to a Stock in the database "OHLVC"
|
| 26 |
+
private String symbol;
|
| 27 |
+
|
| 28 |
+
// Enforcing that the Symbol and the Dates are always unique
|
| 29 |
+
private LocalDate date;
|
| 30 |
+
private double openPrice;
|
| 31 |
+
private double highPrice;
|
| 32 |
+
private double lowPrice;
|
| 33 |
+
private double closePrice;
|
| 34 |
+
private long volume;
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/CrossOver.java
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.*;
|
| 5 |
+
|
| 6 |
+
import java.time.LocalDate;
|
| 7 |
+
|
| 8 |
+
@Entity
|
| 9 |
+
@Table(
|
| 10 |
+
name = "crossovers",
|
| 11 |
+
indexes = {
|
| 12 |
+
@Index(name = "idx_crossover_date", columnList = "date"),
|
| 13 |
+
@Index(name = "idx_crossover_type", columnList = "type")
|
| 14 |
+
}
|
| 15 |
+
)
|
| 16 |
+
@Getter
|
| 17 |
+
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requirement
|
| 18 |
+
@AllArgsConstructor(access = AccessLevel.PRIVATE) // force factory usage
|
| 19 |
+
@ToString
|
| 20 |
+
@EqualsAndHashCode(of = {"date", "type", "price"})
|
| 21 |
+
public class CrossOver {
|
| 22 |
+
|
| 23 |
+
@Id
|
| 24 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 25 |
+
private Long id;
|
| 26 |
+
|
| 27 |
+
// Date when crossover occurred
|
| 28 |
+
@Column(nullable = false)
|
| 29 |
+
private LocalDate date;
|
| 30 |
+
|
| 31 |
+
@Enumerated(EnumType.STRING)
|
| 32 |
+
@Column(nullable = false, length = 10)
|
| 33 |
+
private CrossOverType type; // BULLISH or BEARISH
|
| 34 |
+
|
| 35 |
+
// Price at crossover
|
| 36 |
+
@Column(nullable = false)
|
| 37 |
+
private double price;
|
| 38 |
+
|
| 39 |
+
/* ==========================
|
| 40 |
+
Factory Methods
|
| 41 |
+
========================== */
|
| 42 |
+
|
| 43 |
+
public static CrossOver bullish(Candle candle) {
|
| 44 |
+
return new CrossOver(
|
| 45 |
+
null,
|
| 46 |
+
candle.getDate(),
|
| 47 |
+
CrossOverType.BULLISH,
|
| 48 |
+
candle.getClosePrice()
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
public static CrossOver bearish(Candle candle) {
|
| 53 |
+
return new CrossOver(
|
| 54 |
+
null,
|
| 55 |
+
candle.getDate(),
|
| 56 |
+
CrossOverType.BEARISH,
|
| 57 |
+
candle.getClosePrice()
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/CrossOverType.java
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
public enum CrossOverType {
|
| 4 |
+
BULLISH,
|
| 5 |
+
BEARISH
|
| 6 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/EquityPoint.java
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.*;
|
| 5 |
+
|
| 6 |
+
import java.time.LocalDate;
|
| 7 |
+
|
| 8 |
+
@Entity
|
| 9 |
+
@Table(
|
| 10 |
+
name = "equity_points",
|
| 11 |
+
indexes = {
|
| 12 |
+
@Index(name = "idx_equity_point_date", columnList = "date")
|
| 13 |
+
}
|
| 14 |
+
)
|
| 15 |
+
@Getter
|
| 16 |
+
@NoArgsConstructor // JPA requirement
|
| 17 |
+
@AllArgsConstructor // force factory usage
|
| 18 |
+
@ToString
|
| 19 |
+
@EqualsAndHashCode(of = {"date", "equity", "shares", "cash"})
|
| 20 |
+
public class EquityPoint {
|
| 21 |
+
|
| 22 |
+
@Id
|
| 23 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 24 |
+
private Long id;
|
| 25 |
+
|
| 26 |
+
// Date of this equity snapshot
|
| 27 |
+
@Column(nullable = false)
|
| 28 |
+
private LocalDate date;
|
| 29 |
+
|
| 30 |
+
// Market price at this candle
|
| 31 |
+
@Column(nullable = false)
|
| 32 |
+
private double price;
|
| 33 |
+
|
| 34 |
+
// Total portfolio equity (cash + holdings)
|
| 35 |
+
@Column(nullable = false)
|
| 36 |
+
private double equity;
|
| 37 |
+
|
| 38 |
+
// Number of shares held
|
| 39 |
+
@Column(nullable = false)
|
| 40 |
+
private long shares;
|
| 41 |
+
|
| 42 |
+
// Cash balance
|
| 43 |
+
@Column(nullable = false)
|
| 44 |
+
private double cash;
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
/* ==========================
|
| 49 |
+
Factory Method
|
| 50 |
+
========================== */
|
| 51 |
+
|
| 52 |
+
public static EquityPoint of(
|
| 53 |
+
Candle candle,
|
| 54 |
+
double equity,
|
| 55 |
+
long shares,
|
| 56 |
+
double cash
|
| 57 |
+
) {
|
| 58 |
+
return new EquityPoint(
|
| 59 |
+
null,
|
| 60 |
+
candle.getDate(),
|
| 61 |
+
candle.getClosePrice(),
|
| 62 |
+
equity,
|
| 63 |
+
shares,
|
| 64 |
+
cash
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/PerformanceMetrics.java
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Advanced performance metrics for a backtest result.
|
| 10 |
+
* Calculated from equity curve and transaction history.
|
| 11 |
+
*/
|
| 12 |
+
@Data
|
| 13 |
+
@Builder
|
| 14 |
+
@AllArgsConstructor
|
| 15 |
+
@NoArgsConstructor
|
| 16 |
+
public class PerformanceMetrics {
|
| 17 |
+
|
| 18 |
+
// Risk-adjusted return (annualized)
|
| 19 |
+
private double sharpeRatio;
|
| 20 |
+
|
| 21 |
+
// Worst peak-to-trough decline (as negative %)
|
| 22 |
+
private double maxDrawdown;
|
| 23 |
+
|
| 24 |
+
// Percentage of profitable trades
|
| 25 |
+
private double winRate;
|
| 26 |
+
|
| 27 |
+
// Average profit on winning trades
|
| 28 |
+
private double avgWin;
|
| 29 |
+
|
| 30 |
+
// Average loss on losing trades
|
| 31 |
+
private double avgLoss;
|
| 32 |
+
|
| 33 |
+
// Ratio of average win to average loss
|
| 34 |
+
private double winLossRatio;
|
| 35 |
+
|
| 36 |
+
// Total number of trades executed
|
| 37 |
+
private int totalTrades;
|
| 38 |
+
|
| 39 |
+
// Number of winning trades
|
| 40 |
+
private int winningTrades;
|
| 41 |
+
|
| 42 |
+
// Number of losing trades
|
| 43 |
+
private int losingTrades;
|
| 44 |
+
|
| 45 |
+
// Annualized return percentage
|
| 46 |
+
private double annualizedReturn;
|
| 47 |
+
|
| 48 |
+
// Profit factor: gross profit / gross loss
|
| 49 |
+
private double profitFactor;
|
| 50 |
+
|
| 51 |
+
// Average holding period in days
|
| 52 |
+
private double avgHoldingPeriodDays;
|
| 53 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/PortfolioRequest.java
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Request body for portfolio-level backtesting.
|
| 12 |
+
*/
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
@AllArgsConstructor
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
public class PortfolioRequest {
|
| 18 |
+
|
| 19 |
+
private List<PortfolioEntry> entries;
|
| 20 |
+
private double totalCapital;
|
| 21 |
+
private String strategy; // SMA, RSI, MACD, BUY_AND_HOLD
|
| 22 |
+
|
| 23 |
+
@Data
|
| 24 |
+
@AllArgsConstructor
|
| 25 |
+
@NoArgsConstructor
|
| 26 |
+
public static class PortfolioEntry {
|
| 27 |
+
private String symbol;
|
| 28 |
+
private double weight; // 0.0 to 1.0 (e.g., 0.4 = 40% allocation)
|
| 29 |
+
}
|
| 30 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/PortfolioResult.java
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.util.Map;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Result of portfolio-level backtesting.
|
| 12 |
+
*/
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
@AllArgsConstructor
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
public class PortfolioResult {
|
| 18 |
+
|
| 19 |
+
private double totalCapital;
|
| 20 |
+
private double finalValue;
|
| 21 |
+
private double totalPnL;
|
| 22 |
+
private double totalReturnPct;
|
| 23 |
+
private String strategyUsed;
|
| 24 |
+
|
| 25 |
+
// Aggregate performance metrics across the portfolio
|
| 26 |
+
private PerformanceMetrics aggregateMetrics;
|
| 27 |
+
|
| 28 |
+
// Per-symbol breakdown
|
| 29 |
+
private Map<String, BacktestResult> symbolResults;
|
| 30 |
+
|
| 31 |
+
// Allocation info
|
| 32 |
+
private Map<String, Double> allocations; // symbol → allocated capital
|
| 33 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/QuoteSummary.java
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.math.BigDecimal;
|
| 9 |
+
|
| 10 |
+
/**
|
| 11 |
+
* Real-time quote summary from Yahoo Finance
|
| 12 |
+
*/
|
| 13 |
+
@Data
|
| 14 |
+
@Builder
|
| 15 |
+
@AllArgsConstructor
|
| 16 |
+
@NoArgsConstructor
|
| 17 |
+
public class QuoteSummary {
|
| 18 |
+
|
| 19 |
+
private String symbol;
|
| 20 |
+
private String name;
|
| 21 |
+
private String exchange;
|
| 22 |
+
private String currency;
|
| 23 |
+
|
| 24 |
+
// Price info
|
| 25 |
+
private BigDecimal price;
|
| 26 |
+
private BigDecimal change;
|
| 27 |
+
private BigDecimal changePercent;
|
| 28 |
+
private BigDecimal previousClose;
|
| 29 |
+
private BigDecimal open;
|
| 30 |
+
private BigDecimal dayHigh;
|
| 31 |
+
private BigDecimal dayLow;
|
| 32 |
+
|
| 33 |
+
// Volume
|
| 34 |
+
private Long volume;
|
| 35 |
+
private Long avgVolume;
|
| 36 |
+
|
| 37 |
+
// 52-week range
|
| 38 |
+
private BigDecimal yearHigh;
|
| 39 |
+
private BigDecimal yearLow;
|
| 40 |
+
|
| 41 |
+
// Fundamentals
|
| 42 |
+
private BigDecimal marketCap;
|
| 43 |
+
private BigDecimal pe;
|
| 44 |
+
private BigDecimal eps;
|
| 45 |
+
private BigDecimal dividendYield;
|
| 46 |
+
private BigDecimal bookValue;
|
| 47 |
+
private BigDecimal priceToBook;
|
| 48 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/RegisterRequest.java
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.validation.constraints.NotBlank;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
@Data
|
| 10 |
+
@Builder
|
| 11 |
+
@AllArgsConstructor
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
public class RegisterRequest {
|
| 14 |
+
|
| 15 |
+
@NotBlank(message = "Username is required")
|
| 16 |
+
private String username;
|
| 17 |
+
|
| 18 |
+
@NotBlank(message = "Password is required")
|
| 19 |
+
private String password;
|
| 20 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/SignalType.java
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import org.springframework.stereotype.Component;
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
public enum SignalType
|
| 7 |
+
{
|
| 8 |
+
BUY , SELL , HOLD
|
| 9 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/Stock.java
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.Enumerated;
|
| 4 |
+
import jakarta.persistence.GeneratedValue;
|
| 5 |
+
import jakarta.persistence.GenerationType;
|
| 6 |
+
import jakarta.persistence.Id;
|
| 7 |
+
import lombok.AllArgsConstructor;
|
| 8 |
+
import lombok.Data;
|
| 9 |
+
import lombok.NoArgsConstructor;
|
| 10 |
+
|
| 11 |
+
@Data
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
@AllArgsConstructor
|
| 14 |
+
public class Stock {
|
| 15 |
+
// @Id
|
| 16 |
+
// @GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 17 |
+
// private long id;
|
| 18 |
+
@Id
|
| 19 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 20 |
+
private String symbol;
|
| 21 |
+
private String name;
|
| 22 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/StockSymbol.java
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDateTime;
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@Entity
|
| 12 |
+
@Table(
|
| 13 |
+
name = "stock_symbols",
|
| 14 |
+
uniqueConstraints = {
|
| 15 |
+
@UniqueConstraint(columnNames = {"symbol"})
|
| 16 |
+
},
|
| 17 |
+
indexes = {
|
| 18 |
+
@Index(name = "idx_symbol", columnList = "symbol"),
|
| 19 |
+
@Index(name = "idx_name", columnList = "name"),
|
| 20 |
+
@Index(name = "idx_exchange", columnList = "exchange"),
|
| 21 |
+
@Index(name = "idx_sector", columnList = "sector")
|
| 22 |
+
}
|
| 23 |
+
)
|
| 24 |
+
@Data
|
| 25 |
+
@AllArgsConstructor
|
| 26 |
+
@NoArgsConstructor
|
| 27 |
+
public class StockSymbol {
|
| 28 |
+
|
| 29 |
+
@Id
|
| 30 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 31 |
+
private Long id;
|
| 32 |
+
|
| 33 |
+
// e.g. AAPL, TSLA, RELIANCE.NS
|
| 34 |
+
@Column(nullable = false, length = 20)
|
| 35 |
+
private String symbol;
|
| 36 |
+
|
| 37 |
+
// e.g. Apple Inc, Reliance Industries
|
| 38 |
+
@Column(nullable = false, length = 255)
|
| 39 |
+
private String name;
|
| 40 |
+
|
| 41 |
+
// Equity, ETF, Index
|
| 42 |
+
@Column(length = 50)
|
| 43 |
+
private String type;
|
| 44 |
+
|
| 45 |
+
// Exchange name: NASDAQ, NYSE, BSE, NSE
|
| 46 |
+
@Column(length = 50)
|
| 47 |
+
private String exchange;
|
| 48 |
+
|
| 49 |
+
// Country / Region
|
| 50 |
+
@Column(length = 100)
|
| 51 |
+
private String region;
|
| 52 |
+
|
| 53 |
+
@Column(length = 10)
|
| 54 |
+
private String marketOpen;
|
| 55 |
+
|
| 56 |
+
@Column(length = 10)
|
| 57 |
+
private String marketClose;
|
| 58 |
+
|
| 59 |
+
@Column(length = 20)
|
| 60 |
+
private String timezone;
|
| 61 |
+
|
| 62 |
+
@Column(length = 10)
|
| 63 |
+
private String currency;
|
| 64 |
+
|
| 65 |
+
// Sector: Technology, Finance, Healthcare, etc.
|
| 66 |
+
@Column(length = 100)
|
| 67 |
+
private String sector;
|
| 68 |
+
|
| 69 |
+
// Industry sub-category
|
| 70 |
+
@Column(length = 150)
|
| 71 |
+
private String industry;
|
| 72 |
+
|
| 73 |
+
// Search relevance score (1.0 = exact match)
|
| 74 |
+
@Column
|
| 75 |
+
private Double matchScore;
|
| 76 |
+
|
| 77 |
+
// When this symbol was last fetched/refreshed
|
| 78 |
+
@Column(nullable = false)
|
| 79 |
+
private LocalDateTime lastFetched;
|
| 80 |
+
|
| 81 |
+
// Data source tracker
|
| 82 |
+
@Column(length = 50)
|
| 83 |
+
private String source = "TICKER_SEED";
|
| 84 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/StrategyComparisonResult.java
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import lombok.AllArgsConstructor;
|
| 4 |
+
import lombok.Builder;
|
| 5 |
+
import lombok.Data;
|
| 6 |
+
import lombok.NoArgsConstructor;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
import java.util.Map;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Result of comparing multiple strategies on the same stock data.
|
| 13 |
+
*/
|
| 14 |
+
@Data
|
| 15 |
+
@Builder
|
| 16 |
+
@AllArgsConstructor
|
| 17 |
+
@NoArgsConstructor
|
| 18 |
+
public class StrategyComparisonResult {
|
| 19 |
+
|
| 20 |
+
private String symbol;
|
| 21 |
+
private double initialCapital;
|
| 22 |
+
|
| 23 |
+
// Individual results keyed by strategy name
|
| 24 |
+
private Map<String, BacktestResult> results;
|
| 25 |
+
|
| 26 |
+
// Ranking: best to worst by return %
|
| 27 |
+
private List<String> rankByReturn;
|
| 28 |
+
|
| 29 |
+
// Ranking: best to worst by Sharpe Ratio
|
| 30 |
+
private List<String> rankBySharpe;
|
| 31 |
+
|
| 32 |
+
// Best overall strategy name
|
| 33 |
+
private String bestStrategy;
|
| 34 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/TradeSignal.java
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.*;
|
| 5 |
+
|
| 6 |
+
import java.time.LocalDate;
|
| 7 |
+
|
| 8 |
+
@Entity
|
| 9 |
+
@Table(
|
| 10 |
+
name = "trade_signals",
|
| 11 |
+
indexes = {
|
| 12 |
+
@Index(name = "idx_trade_signal_date", columnList = "signalDate"),
|
| 13 |
+
@Index(name = "idx_trade_signal_type", columnList = "signalType")
|
| 14 |
+
}
|
| 15 |
+
)
|
| 16 |
+
@Getter
|
| 17 |
+
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requirement
|
| 18 |
+
@AllArgsConstructor(access = AccessLevel.PRIVATE) // Force factory usage
|
| 19 |
+
@EqualsAndHashCode(of = {"signalDate", "signalType", "price"})
|
| 20 |
+
@ToString
|
| 21 |
+
public class TradeSignal {
|
| 22 |
+
|
| 23 |
+
@Id
|
| 24 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 25 |
+
private Long id;
|
| 26 |
+
|
| 27 |
+
// Date on which signal is generated
|
| 28 |
+
@Column(nullable = false)
|
| 29 |
+
private LocalDate signalDate;
|
| 30 |
+
|
| 31 |
+
@Enumerated(EnumType.STRING)
|
| 32 |
+
@Column(nullable = false, length = 10)
|
| 33 |
+
private SignalType signalType;
|
| 34 |
+
|
| 35 |
+
// Price at signal generation (usually close price)
|
| 36 |
+
@Column(nullable = false)
|
| 37 |
+
private double price;
|
| 38 |
+
|
| 39 |
+
// Optional: identify which strategy generated this signal
|
| 40 |
+
@Column(length = 100)
|
| 41 |
+
private String strategyName;
|
| 42 |
+
|
| 43 |
+
/* ==========================
|
| 44 |
+
Factory Methods
|
| 45 |
+
========================== */
|
| 46 |
+
|
| 47 |
+
public static TradeSignal buy(Candle candle) {
|
| 48 |
+
return new TradeSignal(
|
| 49 |
+
null,
|
| 50 |
+
candle.getDate(),
|
| 51 |
+
SignalType.BUY,
|
| 52 |
+
candle.getClosePrice(),
|
| 53 |
+
null
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
public static TradeSignal sell(Candle candle) {
|
| 58 |
+
return new TradeSignal(
|
| 59 |
+
null,
|
| 60 |
+
candle.getDate(),
|
| 61 |
+
SignalType.SELL,
|
| 62 |
+
candle.getClosePrice(),
|
| 63 |
+
null
|
| 64 |
+
);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
public static TradeSignal hold() {
|
| 68 |
+
return new TradeSignal(
|
| 69 |
+
null,
|
| 70 |
+
null,
|
| 71 |
+
SignalType.HOLD,
|
| 72 |
+
0.0,
|
| 73 |
+
null
|
| 74 |
+
);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* ==========================
|
| 78 |
+
Optional helpers
|
| 79 |
+
========================== */
|
| 80 |
+
|
| 81 |
+
public TradeSignal withStrategyName(String strategyName) {
|
| 82 |
+
this.strategyName = strategyName;
|
| 83 |
+
return this;
|
| 84 |
+
}
|
| 85 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/Transaction.java
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.*;
|
| 5 |
+
|
| 6 |
+
import java.time.LocalDate;
|
| 7 |
+
|
| 8 |
+
@Entity
|
| 9 |
+
@Table(
|
| 10 |
+
name = "transactions",
|
| 11 |
+
indexes = {
|
| 12 |
+
@Index(name = "idx_tx_date", columnList = "date"),
|
| 13 |
+
@Index(name = "idx_tx_type", columnList = "type")
|
| 14 |
+
}
|
| 15 |
+
)
|
| 16 |
+
@Getter
|
| 17 |
+
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requirement
|
| 18 |
+
@AllArgsConstructor(access = AccessLevel.PRIVATE) // force factory usage
|
| 19 |
+
@ToString
|
| 20 |
+
@EqualsAndHashCode(of = {"date", "type", "price", "shares"})
|
| 21 |
+
public class Transaction {
|
| 22 |
+
|
| 23 |
+
@Id
|
| 24 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 25 |
+
private Long id;
|
| 26 |
+
|
| 27 |
+
// Execution date
|
| 28 |
+
@Column(nullable = false)
|
| 29 |
+
private LocalDate date;
|
| 30 |
+
|
| 31 |
+
@Enumerated(EnumType.STRING)
|
| 32 |
+
@Column(nullable = false, length = 10)
|
| 33 |
+
private SignalType type; // BUY or SELL
|
| 34 |
+
|
| 35 |
+
// Execution price
|
| 36 |
+
@Column(nullable = false)
|
| 37 |
+
private double price;
|
| 38 |
+
|
| 39 |
+
// Number of shares traded
|
| 40 |
+
@Column(nullable = false)
|
| 41 |
+
private long shares;
|
| 42 |
+
|
| 43 |
+
// Cash remaining after execution
|
| 44 |
+
@Column(nullable = false)
|
| 45 |
+
private double cashAfter;
|
| 46 |
+
|
| 47 |
+
// Total equity after execution
|
| 48 |
+
@Column(nullable = false)
|
| 49 |
+
private double equityAfter;
|
| 50 |
+
|
| 51 |
+
/* ==========================
|
| 52 |
+
Factory Methods
|
| 53 |
+
========================== */
|
| 54 |
+
|
| 55 |
+
public static Transaction buy(
|
| 56 |
+
Candle candle,
|
| 57 |
+
double price,
|
| 58 |
+
long shares,
|
| 59 |
+
double cashAfter,
|
| 60 |
+
double equityAfter
|
| 61 |
+
) {
|
| 62 |
+
return new Transaction(
|
| 63 |
+
null,
|
| 64 |
+
candle.getDate(),
|
| 65 |
+
SignalType.BUY,
|
| 66 |
+
price,
|
| 67 |
+
shares,
|
| 68 |
+
cashAfter,
|
| 69 |
+
equityAfter
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
public static Transaction sell(
|
| 74 |
+
Candle candle,
|
| 75 |
+
double price,
|
| 76 |
+
long shares,
|
| 77 |
+
double cashAfter,
|
| 78 |
+
double equityAfter
|
| 79 |
+
) {
|
| 80 |
+
return new Transaction(
|
| 81 |
+
null,
|
| 82 |
+
candle.getDate(),
|
| 83 |
+
SignalType.SELL,
|
| 84 |
+
price,
|
| 85 |
+
shares,
|
| 86 |
+
cashAfter,
|
| 87 |
+
equityAfter
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
}
|
src/main/java/com/rods/backtestingstrategies/entity/User.java
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.entity;
|
| 2 |
+
|
| 3 |
+
import jakarta.persistence.*;
|
| 4 |
+
import lombok.AllArgsConstructor;
|
| 5 |
+
import lombok.Builder;
|
| 6 |
+
import lombok.Data;
|
| 7 |
+
import lombok.NoArgsConstructor;
|
| 8 |
+
|
| 9 |
+
@Entity
|
| 10 |
+
@Data
|
| 11 |
+
@Builder
|
| 12 |
+
@NoArgsConstructor
|
| 13 |
+
@AllArgsConstructor
|
| 14 |
+
@Table(name = "users")
|
| 15 |
+
public class User {
|
| 16 |
+
|
| 17 |
+
@Id
|
| 18 |
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
| 19 |
+
private Long id;
|
| 20 |
+
|
| 21 |
+
@Column(unique = true, nullable = false)
|
| 22 |
+
private String username;
|
| 23 |
+
|
| 24 |
+
@Column(nullable = false)
|
| 25 |
+
private String password;
|
| 26 |
+
|
| 27 |
+
@Column(nullable = false)
|
| 28 |
+
private String role;
|
| 29 |
+
}
|
src/main/java/com/rods/backtestingstrategies/repository/CandleRepository.java
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.repository;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 5 |
+
import org.springframework.data.jpa.repository.Query;
|
| 6 |
+
import org.springframework.data.repository.query.Param;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDate;
|
| 9 |
+
import java.util.List;
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
public interface CandleRepository extends JpaRepository<Candle, Long> {
|
| 13 |
+
List<Candle> findBySymbolOrderByDateAsc(String symbol);
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
// Fetching the existing date from the Database --> To check whether there is a need to syncMarket() Data
|
| 17 |
+
// Before we used to fetch the entire data first and then check the last data --> very slow (in efficient)
|
| 18 |
+
@Query("SELECT c.date FROM Candle c WHERE c.symbol = :symbol")
|
| 19 |
+
List<LocalDate> findExistingDates(@Param("symbol") String symbol);
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
}
|
src/main/java/com/rods/backtestingstrategies/repository/StockSymbolRepository.java
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.repository;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.StockSymbol;
|
| 4 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 5 |
+
import org.springframework.data.jpa.repository.Query;
|
| 6 |
+
import org.springframework.data.repository.query.Param;
|
| 7 |
+
|
| 8 |
+
import java.time.LocalDateTime;
|
| 9 |
+
import java.util.List;
|
| 10 |
+
|
| 11 |
+
public interface StockSymbolRepository extends JpaRepository<StockSymbol, Long> {
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Fuzzy search by symbol prefix or company name substring.
|
| 15 |
+
* Ordered by match score descending.
|
| 16 |
+
*/
|
| 17 |
+
@Query("""
|
| 18 |
+
SELECT s FROM StockSymbol s
|
| 19 |
+
WHERE LOWER(s.symbol) LIKE LOWER(CONCAT(:query, '%'))
|
| 20 |
+
OR LOWER(s.name) LIKE LOWER(CONCAT('%', :query, '%'))
|
| 21 |
+
ORDER BY s.matchScore DESC
|
| 22 |
+
""")
|
| 23 |
+
List<StockSymbol> searchSymbols(@Param("query") String query);
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Search symbols filtered by a specific exchange.
|
| 27 |
+
*/
|
| 28 |
+
@Query("""
|
| 29 |
+
SELECT s FROM StockSymbol s
|
| 30 |
+
WHERE (LOWER(s.symbol) LIKE LOWER(CONCAT(:query, '%'))
|
| 31 |
+
OR LOWER(s.name) LIKE LOWER(CONCAT('%', :query, '%')))
|
| 32 |
+
AND LOWER(s.exchange) = LOWER(:exchange)
|
| 33 |
+
ORDER BY s.matchScore DESC
|
| 34 |
+
""")
|
| 35 |
+
List<StockSymbol> searchSymbolsByExchange(
|
| 36 |
+
@Param("query") String query,
|
| 37 |
+
@Param("exchange") String exchange
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Find a specific symbol (case-insensitive)
|
| 42 |
+
*/
|
| 43 |
+
@Query("""
|
| 44 |
+
SELECT s FROM StockSymbol s
|
| 45 |
+
WHERE LOWER(s.symbol) = LOWER(:symbol)
|
| 46 |
+
""")
|
| 47 |
+
StockSymbol findBySymbol(@Param("symbol") String symbol);
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Get all symbols for a specific exchange
|
| 51 |
+
*/
|
| 52 |
+
@Query("""
|
| 53 |
+
SELECT s FROM StockSymbol s
|
| 54 |
+
WHERE LOWER(s.exchange) = LOWER(:exchange)
|
| 55 |
+
ORDER BY s.symbol ASC
|
| 56 |
+
""")
|
| 57 |
+
List<StockSymbol> findByExchange(@Param("exchange") String exchange);
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Get all symbols for a specific sector
|
| 61 |
+
*/
|
| 62 |
+
@Query("""
|
| 63 |
+
SELECT s FROM StockSymbol s
|
| 64 |
+
WHERE LOWER(s.sector) = LOWER(:sector)
|
| 65 |
+
ORDER BY s.symbol ASC
|
| 66 |
+
""")
|
| 67 |
+
List<StockSymbol> findBySector(@Param("sector") String sector);
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Get the last fetched timestamp for symbol search freshness check
|
| 71 |
+
*/
|
| 72 |
+
@Query("""
|
| 73 |
+
SELECT MAX(s.lastFetched)
|
| 74 |
+
FROM StockSymbol s
|
| 75 |
+
WHERE LOWER(s.symbol) LIKE LOWER(CONCAT(:query, '%'))
|
| 76 |
+
""")
|
| 77 |
+
LocalDateTime findLastFetchedForQuery(@Param("query") String query);
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Get all distinct exchange names
|
| 81 |
+
*/
|
| 82 |
+
@Query("SELECT DISTINCT s.exchange FROM StockSymbol s ORDER BY s.exchange")
|
| 83 |
+
List<String> findAllExchanges();
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Get all distinct sectors
|
| 87 |
+
*/
|
| 88 |
+
@Query("SELECT DISTINCT s.sector FROM StockSymbol s WHERE s.sector IS NOT NULL ORDER BY s.sector")
|
| 89 |
+
List<String> findAllSectors();
|
| 90 |
+
}
|
src/main/java/com/rods/backtestingstrategies/repository/UserRepository.java
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.repository;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.User;
|
| 4 |
+
import org.springframework.data.jpa.repository.JpaRepository;
|
| 5 |
+
|
| 6 |
+
import java.util.Optional;
|
| 7 |
+
|
| 8 |
+
public interface UserRepository extends JpaRepository<User, Long> {
|
| 9 |
+
Optional<User> findByUsername(String username);
|
| 10 |
+
}
|
src/main/java/com/rods/backtestingstrategies/security/CustomUserDetailsService.java
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.security;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.User;
|
| 4 |
+
import com.rods.backtestingstrategies.repository.UserRepository;
|
| 5 |
+
import lombok.RequiredArgsConstructor;
|
| 6 |
+
import org.springframework.security.core.userdetails.UserDetails;
|
| 7 |
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
| 8 |
+
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
| 9 |
+
import org.springframework.stereotype.Service;
|
| 10 |
+
|
| 11 |
+
import java.util.ArrayList;
|
| 12 |
+
|
| 13 |
+
@Service
|
| 14 |
+
@RequiredArgsConstructor
|
| 15 |
+
public class CustomUserDetailsService implements UserDetailsService {
|
| 16 |
+
|
| 17 |
+
private final UserRepository userRepository;
|
| 18 |
+
|
| 19 |
+
@Override
|
| 20 |
+
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
| 21 |
+
User user = userRepository.findByUsername(username)
|
| 22 |
+
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
| 23 |
+
|
| 24 |
+
return new org.springframework.security.core.userdetails.User(
|
| 25 |
+
user.getUsername(),
|
| 26 |
+
user.getPassword(),
|
| 27 |
+
new ArrayList<>() // Authorities/Roles can be added here
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
}
|
src/main/java/com/rods/backtestingstrategies/security/JwtAuthenticationFilter.java
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.security;
|
| 2 |
+
|
| 3 |
+
import jakarta.servlet.FilterChain;
|
| 4 |
+
import jakarta.servlet.ServletException;
|
| 5 |
+
import jakarta.servlet.http.HttpServletRequest;
|
| 6 |
+
import jakarta.servlet.http.HttpServletResponse;
|
| 7 |
+
import lombok.RequiredArgsConstructor;
|
| 8 |
+
import org.springframework.lang.NonNull;
|
| 9 |
+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
| 10 |
+
import org.springframework.security.core.context.SecurityContextHolder;
|
| 11 |
+
import org.springframework.security.core.userdetails.UserDetails;
|
| 12 |
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
| 13 |
+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
| 14 |
+
import org.springframework.stereotype.Component;
|
| 15 |
+
import org.springframework.web.filter.OncePerRequestFilter;
|
| 16 |
+
|
| 17 |
+
import java.io.IOException;
|
| 18 |
+
|
| 19 |
+
@Component
|
| 20 |
+
@RequiredArgsConstructor
|
| 21 |
+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
| 22 |
+
|
| 23 |
+
private final JwtUtils jwtService;
|
| 24 |
+
private final UserDetailsService userDetailsService;
|
| 25 |
+
|
| 26 |
+
@Override
|
| 27 |
+
protected void doFilterInternal(
|
| 28 |
+
@NonNull HttpServletRequest request,
|
| 29 |
+
@NonNull HttpServletResponse response,
|
| 30 |
+
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
| 31 |
+
|
| 32 |
+
final String authHeader = request.getHeader("Authorization");
|
| 33 |
+
final String jwt;
|
| 34 |
+
final String userEmail;
|
| 35 |
+
|
| 36 |
+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
| 37 |
+
filterChain.doFilter(request, response);
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
jwt = authHeader.substring(7);
|
| 42 |
+
userEmail = jwtService.extractUsername(jwt);
|
| 43 |
+
|
| 44 |
+
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
| 45 |
+
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
|
| 46 |
+
|
| 47 |
+
if (jwtService.isTokenValid(jwt, userDetails)) {
|
| 48 |
+
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
| 49 |
+
userDetails,
|
| 50 |
+
null,
|
| 51 |
+
userDetails.getAuthorities());
|
| 52 |
+
authToken.setDetails(
|
| 53 |
+
new WebAuthenticationDetailsSource().buildDetails(request));
|
| 54 |
+
SecurityContextHolder.getContext().setAuthentication(authToken);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
filterChain.doFilter(request, response);
|
| 58 |
+
}
|
| 59 |
+
}
|
src/main/java/com/rods/backtestingstrategies/security/JwtUtils.java
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.security;
|
| 2 |
+
|
| 3 |
+
import io.jsonwebtoken.Claims;
|
| 4 |
+
import io.jsonwebtoken.Jwts;
|
| 5 |
+
import io.jsonwebtoken.SignatureAlgorithm;
|
| 6 |
+
import io.jsonwebtoken.io.Decoders;
|
| 7 |
+
import io.jsonwebtoken.security.Keys;
|
| 8 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 9 |
+
import org.springframework.security.core.userdetails.UserDetails;
|
| 10 |
+
import org.springframework.stereotype.Component;
|
| 11 |
+
|
| 12 |
+
import java.security.Key;
|
| 13 |
+
import java.util.Date;
|
| 14 |
+
import java.util.HashMap;
|
| 15 |
+
import java.util.Map;
|
| 16 |
+
import java.util.function.Function;
|
| 17 |
+
|
| 18 |
+
@Component
|
| 19 |
+
public class JwtUtils {
|
| 20 |
+
|
| 21 |
+
@Value("${jwt.secret}")
|
| 22 |
+
private String secret;
|
| 23 |
+
|
| 24 |
+
@Value("${jwt.expiration}")
|
| 25 |
+
private long jwtExpiration;
|
| 26 |
+
|
| 27 |
+
public String extractUsername(String token) {
|
| 28 |
+
return extractClaim(token, Claims::getSubject);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
public Date extractExpiration(String token) {
|
| 32 |
+
return extractClaim(token, Claims::getExpiration);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
| 36 |
+
final Claims claims = extractAllClaims(token);
|
| 37 |
+
return claimsResolver.apply(claims);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
private Claims extractAllClaims(String token) {
|
| 41 |
+
return Jwts.parserBuilder()
|
| 42 |
+
.setSigningKey(getSigningKey())
|
| 43 |
+
.build()
|
| 44 |
+
.parseClaimsJws(token)
|
| 45 |
+
.getBody();
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
private Key getSigningKey() {
|
| 49 |
+
byte[] keyBytes = Decoders.BASE64.decode(secret); // Actually I used HEX in current example, but standard is
|
| 50 |
+
// often Base64. Let's stick to simple Bytes for now or assume
|
| 51 |
+
// secret is compatible.
|
| 52 |
+
// Wait, I put a HEX string in application.yml?
|
| 53 |
+
// Let's modify the code to parse hex if I used hex, or better, just use the
|
| 54 |
+
// string bytes if it's simpler.
|
| 55 |
+
// Actually, the standard JJWT way is to use a Base64 encoded key or just raw
|
| 56 |
+
// bytes.
|
| 57 |
+
// To be safe and consistent with the hex I generated: 5367566B5970...
|
| 58 |
+
// Hex decoding is safer for the string I put. But Decoders.BASE64 expects
|
| 59 |
+
// Base64.
|
| 60 |
+
// Let's assume I'll change the secret in YAML to a proper Base64 or just use
|
| 61 |
+
// string bytes.
|
| 62 |
+
// For simplicity, let's just use
|
| 63 |
+
// Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)) and expect the user to
|
| 64 |
+
// provide a Base64 secret, OR
|
| 65 |
+
// Use the hex decoder if I want to support that specific hex string.
|
| 66 |
+
// I'll stick to Base64 for standardness.
|
| 67 |
+
// Wait, the secret I put `5367...` is clearly hex.
|
| 68 |
+
// I will change the code to use `Decoders.BASE64` and I will update the secret
|
| 69 |
+
// in YAML to a Base64 string to be clean.
|
| 70 |
+
// Or I can just use `secret.getBytes()` if I don't care about the format.
|
| 71 |
+
// Let's go with standard Base64.
|
| 72 |
+
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
public String generateToken(UserDetails userDetails) {
|
| 76 |
+
return generateToken(new HashMap<>(), userDetails);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
|
| 80 |
+
return Jwts.builder()
|
| 81 |
+
.setClaims(extraClaims)
|
| 82 |
+
.setSubject(userDetails.getUsername())
|
| 83 |
+
.setIssuedAt(new Date(System.currentTimeMillis()))
|
| 84 |
+
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
|
| 85 |
+
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
|
| 86 |
+
.compact();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
public boolean isTokenValid(String token, UserDetails userDetails) {
|
| 90 |
+
final String username = extractUsername(token);
|
| 91 |
+
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
private boolean isTokenExpired(String token) {
|
| 95 |
+
return extractExpiration(token).before(new Date());
|
| 96 |
+
}
|
| 97 |
+
}
|
src/main/java/com/rods/backtestingstrategies/security/SecurityConfig.java
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.security;
|
| 2 |
+
|
| 3 |
+
import lombok.RequiredArgsConstructor;
|
| 4 |
+
import org.springframework.context.annotation.Bean;
|
| 5 |
+
import org.springframework.context.annotation.Configuration;
|
| 6 |
+
import org.springframework.security.authentication.AuthenticationManager;
|
| 7 |
+
import org.springframework.security.authentication.AuthenticationProvider;
|
| 8 |
+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
| 9 |
+
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
| 10 |
+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
| 11 |
+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
| 12 |
+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
| 13 |
+
import org.springframework.security.config.Customizer;
|
| 14 |
+
import org.springframework.security.config.http.SessionCreationPolicy;
|
| 15 |
+
import org.springframework.security.core.userdetails.UserDetailsService;
|
| 16 |
+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
| 17 |
+
import org.springframework.security.crypto.password.PasswordEncoder;
|
| 18 |
+
import org.springframework.security.web.SecurityFilterChain;
|
| 19 |
+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
| 20 |
+
|
| 21 |
+
@Configuration
|
| 22 |
+
@EnableWebSecurity
|
| 23 |
+
@RequiredArgsConstructor
|
| 24 |
+
public class SecurityConfig {
|
| 25 |
+
|
| 26 |
+
private final JwtAuthenticationFilter jwtAuthFilter;
|
| 27 |
+
private final UserDetailsService userDetailsService;
|
| 28 |
+
|
| 29 |
+
@Bean
|
| 30 |
+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
| 31 |
+
http
|
| 32 |
+
.csrf(AbstractHttpConfigurer::disable)
|
| 33 |
+
.cors(Customizer.withDefaults())
|
| 34 |
+
.authorizeHttpRequests(auth -> auth
|
| 35 |
+
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() // Allow preflight checks
|
| 36 |
+
.requestMatchers("/api/auth/**", "/server/**", "/error").permitAll() // Whitelist public endpoints
|
| 37 |
+
.anyRequest().authenticated() // Secure everything else
|
| 38 |
+
)
|
| 39 |
+
.sessionManagement(sess -> sess
|
| 40 |
+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
| 41 |
+
.authenticationProvider(authenticationProvider())
|
| 42 |
+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
| 43 |
+
|
| 44 |
+
return http.build();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
@Bean
|
| 48 |
+
public AuthenticationProvider authenticationProvider() {
|
| 49 |
+
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
| 50 |
+
authProvider.setUserDetailsService(userDetailsService);
|
| 51 |
+
authProvider.setPasswordEncoder(passwordEncoder());
|
| 52 |
+
return authProvider;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
@Bean
|
| 56 |
+
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
| 57 |
+
return config.getAuthenticationManager();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
@Bean
|
| 61 |
+
public PasswordEncoder passwordEncoder() {
|
| 62 |
+
return new BCryptPasswordEncoder();
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
@Bean
|
| 66 |
+
public org.springframework.web.cors.CorsConfigurationSource corsConfigurationSource() {
|
| 67 |
+
org.springframework.web.cors.CorsConfiguration configuration = new org.springframework.web.cors.CorsConfiguration();
|
| 68 |
+
configuration.setAllowedOrigins(java.util.Arrays.asList("http://localhost:5173", "http://localhost:3000", "https://backtest-livid.vercel.app"));
|
| 69 |
+
configuration
|
| 70 |
+
.setAllowedMethods(java.util.Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"));
|
| 71 |
+
configuration.setAllowedHeaders(java.util.Collections.singletonList("*"));
|
| 72 |
+
configuration.setAllowCredentials(true);
|
| 73 |
+
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new org.springframework.web.cors.UrlBasedCorsConfigurationSource();
|
| 74 |
+
source.registerCorsConfiguration("/**", configuration);
|
| 75 |
+
return source;
|
| 76 |
+
}
|
| 77 |
+
}
|
src/main/java/com/rods/backtestingstrategies/service/BacktestService.java
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.service;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.*;
|
| 4 |
+
import com.rods.backtestingstrategies.strategy.*;
|
| 5 |
+
import lombok.RequiredArgsConstructor;
|
| 6 |
+
import org.springframework.stereotype.Service;
|
| 7 |
+
|
| 8 |
+
import java.time.temporal.ChronoUnit;
|
| 9 |
+
import java.util.*;
|
| 10 |
+
import java.util.stream.Collectors;
|
| 11 |
+
|
| 12 |
+
@Service
|
| 13 |
+
@RequiredArgsConstructor
|
| 14 |
+
public class BacktestService {
|
| 15 |
+
|
| 16 |
+
private final MarketDataService marketDataService;
|
| 17 |
+
private final StrategyFactory strategyFactory;
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Run a backtest with default strategy parameters.
|
| 21 |
+
*/
|
| 22 |
+
public BacktestResult backtest(
|
| 23 |
+
String symbol,
|
| 24 |
+
StrategyType strategyType,
|
| 25 |
+
double initialCapital
|
| 26 |
+
) {
|
| 27 |
+
Strategy strategy = strategyFactory.getStrategy(strategyType);
|
| 28 |
+
return executeBacktest(symbol, strategy, initialCapital);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Run a backtest with custom strategy parameters.
|
| 33 |
+
*/
|
| 34 |
+
public BacktestResult backtestWithParams(
|
| 35 |
+
String symbol,
|
| 36 |
+
StrategyType strategyType,
|
| 37 |
+
double initialCapital,
|
| 38 |
+
Map<String, String> params
|
| 39 |
+
) {
|
| 40 |
+
Strategy strategy = createParameterizedStrategy(strategyType, params);
|
| 41 |
+
return executeBacktest(symbol, strategy, initialCapital);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Compare all available strategies on the same stock data.
|
| 46 |
+
*/
|
| 47 |
+
public StrategyComparisonResult compareStrategies(
|
| 48 |
+
String symbol,
|
| 49 |
+
double initialCapital
|
| 50 |
+
) {
|
| 51 |
+
Map<String, BacktestResult> results = new LinkedHashMap<>();
|
| 52 |
+
|
| 53 |
+
for (StrategyType type : StrategyType.values()) {
|
| 54 |
+
try {
|
| 55 |
+
Strategy strategy = strategyFactory.getStrategy(type);
|
| 56 |
+
BacktestResult result = executeBacktest(symbol, strategy, initialCapital);
|
| 57 |
+
results.put(strategy.getName(), result);
|
| 58 |
+
} catch (IllegalArgumentException e) {
|
| 59 |
+
// Strategy not implemented yet, skip
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Rank by return %
|
| 64 |
+
List<String> rankByReturn = results.entrySet().stream()
|
| 65 |
+
.sorted((a, b) -> Double.compare(b.getValue().getReturnPct(), a.getValue().getReturnPct()))
|
| 66 |
+
.map(Map.Entry::getKey)
|
| 67 |
+
.collect(Collectors.toList());
|
| 68 |
+
|
| 69 |
+
// Rank by Sharpe Ratio
|
| 70 |
+
List<String> rankBySharpe = results.entrySet().stream()
|
| 71 |
+
.sorted((a, b) -> Double.compare(
|
| 72 |
+
b.getValue().getMetrics().getSharpeRatio(),
|
| 73 |
+
a.getValue().getMetrics().getSharpeRatio()))
|
| 74 |
+
.map(Map.Entry::getKey)
|
| 75 |
+
.collect(Collectors.toList());
|
| 76 |
+
|
| 77 |
+
String best = rankByReturn.isEmpty() ? "N/A" : rankByReturn.getFirst();
|
| 78 |
+
|
| 79 |
+
return StrategyComparisonResult.builder()
|
| 80 |
+
.symbol(symbol)
|
| 81 |
+
.initialCapital(initialCapital)
|
| 82 |
+
.results(results)
|
| 83 |
+
.rankByReturn(rankByReturn)
|
| 84 |
+
.rankBySharpe(rankBySharpe)
|
| 85 |
+
.bestStrategy(best)
|
| 86 |
+
.build();
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Run portfolio-level backtest across multiple symbols.
|
| 91 |
+
*/
|
| 92 |
+
public PortfolioResult backtestPortfolio(PortfolioRequest request) {
|
| 93 |
+
StrategyType strategyType = StrategyType.valueOf(request.getStrategy().toUpperCase());
|
| 94 |
+
Strategy strategy = strategyFactory.getStrategy(strategyType);
|
| 95 |
+
|
| 96 |
+
Map<String, BacktestResult> symbolResults = new LinkedHashMap<>();
|
| 97 |
+
Map<String, Double> allocations = new LinkedHashMap<>();
|
| 98 |
+
|
| 99 |
+
double totalFinalValue = 0;
|
| 100 |
+
|
| 101 |
+
for (PortfolioRequest.PortfolioEntry entry : request.getEntries()) {
|
| 102 |
+
double allocatedCapital = request.getTotalCapital() * entry.getWeight();
|
| 103 |
+
allocations.put(entry.getSymbol(), allocatedCapital);
|
| 104 |
+
|
| 105 |
+
BacktestResult result = executeBacktest(entry.getSymbol(), strategy, allocatedCapital);
|
| 106 |
+
symbolResults.put(entry.getSymbol(), result);
|
| 107 |
+
totalFinalValue += result.getFinalCapital();
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
double totalPnL = totalFinalValue - request.getTotalCapital();
|
| 111 |
+
double totalReturnPct = request.getTotalCapital() == 0 ? 0 :
|
| 112 |
+
(totalPnL / request.getTotalCapital()) * 100.0;
|
| 113 |
+
|
| 114 |
+
// Aggregate metrics weighted by allocation
|
| 115 |
+
PerformanceMetrics aggregateMetrics = calculateAggregateMetrics(symbolResults, allocations, request.getTotalCapital());
|
| 116 |
+
|
| 117 |
+
return PortfolioResult.builder()
|
| 118 |
+
.totalCapital(request.getTotalCapital())
|
| 119 |
+
.finalValue(totalFinalValue)
|
| 120 |
+
.totalPnL(totalPnL)
|
| 121 |
+
.totalReturnPct(totalReturnPct)
|
| 122 |
+
.strategyUsed(strategy.getName())
|
| 123 |
+
.aggregateMetrics(aggregateMetrics)
|
| 124 |
+
.symbolResults(symbolResults)
|
| 125 |
+
.allocations(allocations)
|
| 126 |
+
.build();
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* ==========================
|
| 130 |
+
Core Execution Engine
|
| 131 |
+
========================== */
|
| 132 |
+
|
| 133 |
+
private BacktestResult executeBacktest(String symbol, Strategy strategy, double initialCapital) {
|
| 134 |
+
List<Candle> candles = marketDataService.getCandles(symbol);
|
| 135 |
+
|
| 136 |
+
if (candles == null || candles.isEmpty()) {
|
| 137 |
+
return BacktestResult.empty(initialCapital);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
candles.sort(Comparator.comparing(Candle::getDate));
|
| 141 |
+
|
| 142 |
+
double cash = initialCapital;
|
| 143 |
+
long shares = 0L;
|
| 144 |
+
|
| 145 |
+
List<EquityPoint> equityCurve = new ArrayList<>();
|
| 146 |
+
List<Transaction> transactions = new ArrayList<>();
|
| 147 |
+
List<CrossOver> crossovers = new ArrayList<>();
|
| 148 |
+
|
| 149 |
+
for (int i = 0; i < candles.size(); i++) {
|
| 150 |
+
Candle candle = candles.get(i);
|
| 151 |
+
double price = candle.getClosePrice();
|
| 152 |
+
|
| 153 |
+
TradeSignal signal = strategy.evaluate(candles, i)
|
| 154 |
+
.withStrategyName(strategy.getName());
|
| 155 |
+
|
| 156 |
+
switch (signal.getSignalType()) {
|
| 157 |
+
case BUY -> {
|
| 158 |
+
if (cash > 0 && shares == 0) {
|
| 159 |
+
long buyShares = (long) (cash / price);
|
| 160 |
+
if (buyShares > 0) {
|
| 161 |
+
cash -= buyShares * price;
|
| 162 |
+
shares += buyShares;
|
| 163 |
+
transactions.add(
|
| 164 |
+
Transaction.buy(candle, price, buyShares, cash, cash + shares * price)
|
| 165 |
+
);
|
| 166 |
+
crossovers.add(CrossOver.bullish(candle));
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
case SELL -> {
|
| 171 |
+
if (shares > 0) {
|
| 172 |
+
double proceeds = shares * price;
|
| 173 |
+
cash += proceeds;
|
| 174 |
+
transactions.add(
|
| 175 |
+
Transaction.sell(candle, price, shares, cash, cash)
|
| 176 |
+
);
|
| 177 |
+
crossovers.add(CrossOver.bearish(candle));
|
| 178 |
+
shares = 0;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
case HOLD -> { /* no-op */ }
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
double equity = cash + shares * price;
|
| 185 |
+
equityCurve.add(EquityPoint.of(candle, equity, shares, cash));
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
EquityPoint last = equityCurve.getLast();
|
| 189 |
+
double finalValue = last.getEquity();
|
| 190 |
+
double pnl = finalValue - initialCapital;
|
| 191 |
+
double returnPct = initialCapital == 0 ? 0 : (pnl / initialCapital) * 100.0;
|
| 192 |
+
|
| 193 |
+
// Calculate advanced metrics
|
| 194 |
+
PerformanceMetrics metrics = calculateMetrics(equityCurve, transactions, initialCapital);
|
| 195 |
+
|
| 196 |
+
return BacktestResult.builder()
|
| 197 |
+
.startCapital(initialCapital)
|
| 198 |
+
.finalCapital(finalValue)
|
| 199 |
+
.profitLoss(pnl)
|
| 200 |
+
.returnPct(returnPct)
|
| 201 |
+
.strategyName(strategy.getName())
|
| 202 |
+
.metrics(metrics)
|
| 203 |
+
.equityCurve(equityCurve)
|
| 204 |
+
.transactions(transactions)
|
| 205 |
+
.crossovers(crossovers)
|
| 206 |
+
.build();
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* ==========================
|
| 210 |
+
Advanced Metrics Calculator
|
| 211 |
+
========================== */
|
| 212 |
+
|
| 213 |
+
private PerformanceMetrics calculateMetrics(
|
| 214 |
+
List<EquityPoint> equityCurve,
|
| 215 |
+
List<Transaction> transactions,
|
| 216 |
+
double initialCapital
|
| 217 |
+
) {
|
| 218 |
+
if (equityCurve.isEmpty()) {
|
| 219 |
+
return PerformanceMetrics.builder().build();
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
// --- Max Drawdown ---
|
| 223 |
+
double maxDrawdown = calculateMaxDrawdown(equityCurve);
|
| 224 |
+
|
| 225 |
+
// --- Trade Analysis ---
|
| 226 |
+
List<Double> tradePnLs = calculateTradePnLs(transactions);
|
| 227 |
+
int totalTrades = tradePnLs.size();
|
| 228 |
+
int winningTrades = (int) tradePnLs.stream().filter(p -> p > 0).count();
|
| 229 |
+
int losingTrades = (int) tradePnLs.stream().filter(p -> p < 0).count();
|
| 230 |
+
double winRate = totalTrades == 0 ? 0 : (double) winningTrades / totalTrades * 100.0;
|
| 231 |
+
|
| 232 |
+
double avgWin = tradePnLs.stream().filter(p -> p > 0).mapToDouble(Double::doubleValue).average().orElse(0);
|
| 233 |
+
double avgLoss = tradePnLs.stream().filter(p -> p < 0).mapToDouble(Double::doubleValue).average().orElse(0);
|
| 234 |
+
double winLossRatio = avgLoss == 0 ? 0 : Math.abs(avgWin / avgLoss);
|
| 235 |
+
|
| 236 |
+
double grossProfit = tradePnLs.stream().filter(p -> p > 0).mapToDouble(Double::doubleValue).sum();
|
| 237 |
+
double grossLoss = Math.abs(tradePnLs.stream().filter(p -> p < 0).mapToDouble(Double::doubleValue).sum());
|
| 238 |
+
double profitFactor = grossLoss == 0 ? 0 : grossProfit / grossLoss;
|
| 239 |
+
|
| 240 |
+
// --- Sharpe Ratio ---
|
| 241 |
+
double sharpeRatio = calculateSharpeRatio(equityCurve);
|
| 242 |
+
|
| 243 |
+
// --- Annualized Return ---
|
| 244 |
+
double finalEquity = equityCurve.getLast().getEquity();
|
| 245 |
+
long days = ChronoUnit.DAYS.between(
|
| 246 |
+
equityCurve.getFirst().getDate(),
|
| 247 |
+
equityCurve.getLast().getDate()
|
| 248 |
+
);
|
| 249 |
+
double years = Math.max(days / 365.25, 0.01);
|
| 250 |
+
double annualizedReturn = (Math.pow(finalEquity / initialCapital, 1.0 / years) - 1.0) * 100.0;
|
| 251 |
+
|
| 252 |
+
// --- Average Holding Period ---
|
| 253 |
+
double avgHoldingDays = calculateAvgHoldingPeriod(transactions);
|
| 254 |
+
|
| 255 |
+
return PerformanceMetrics.builder()
|
| 256 |
+
.sharpeRatio(round(sharpeRatio))
|
| 257 |
+
.maxDrawdown(round(maxDrawdown))
|
| 258 |
+
.winRate(round(winRate))
|
| 259 |
+
.avgWin(round(avgWin))
|
| 260 |
+
.avgLoss(round(avgLoss))
|
| 261 |
+
.winLossRatio(round(winLossRatio))
|
| 262 |
+
.totalTrades(totalTrades)
|
| 263 |
+
.winningTrades(winningTrades)
|
| 264 |
+
.losingTrades(losingTrades)
|
| 265 |
+
.annualizedReturn(round(annualizedReturn))
|
| 266 |
+
.profitFactor(round(profitFactor))
|
| 267 |
+
.avgHoldingPeriodDays(round(avgHoldingDays))
|
| 268 |
+
.build();
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
private double calculateMaxDrawdown(List<EquityPoint> equityCurve) {
|
| 272 |
+
double peak = equityCurve.getFirst().getEquity();
|
| 273 |
+
double maxDrawdown = 0;
|
| 274 |
+
|
| 275 |
+
for (EquityPoint point : equityCurve) {
|
| 276 |
+
if (point.getEquity() > peak) {
|
| 277 |
+
peak = point.getEquity();
|
| 278 |
+
}
|
| 279 |
+
double drawdown = (peak - point.getEquity()) / peak * 100.0;
|
| 280 |
+
if (drawdown > maxDrawdown) {
|
| 281 |
+
maxDrawdown = drawdown;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
return -maxDrawdown; // Return as negative percentage
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
private double calculateSharpeRatio(List<EquityPoint> equityCurve) {
|
| 288 |
+
if (equityCurve.size() < 2) return 0;
|
| 289 |
+
|
| 290 |
+
// Daily returns
|
| 291 |
+
List<Double> dailyReturns = new ArrayList<>();
|
| 292 |
+
for (int i = 1; i < equityCurve.size(); i++) {
|
| 293 |
+
double prevEquity = equityCurve.get(i - 1).getEquity();
|
| 294 |
+
double currEquity = equityCurve.get(i).getEquity();
|
| 295 |
+
if (prevEquity != 0) {
|
| 296 |
+
dailyReturns.add((currEquity - prevEquity) / prevEquity);
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
if (dailyReturns.isEmpty()) return 0;
|
| 301 |
+
|
| 302 |
+
double avgReturn = dailyReturns.stream().mapToDouble(Double::doubleValue).average().orElse(0);
|
| 303 |
+
double stdDev = Math.sqrt(
|
| 304 |
+
dailyReturns.stream()
|
| 305 |
+
.mapToDouble(r -> Math.pow(r - avgReturn, 2))
|
| 306 |
+
.average()
|
| 307 |
+
.orElse(0)
|
| 308 |
+
);
|
| 309 |
+
|
| 310 |
+
if (stdDev == 0) return 0;
|
| 311 |
+
|
| 312 |
+
// Annualized Sharpe (assuming 252 trading days, risk-free rate = 0)
|
| 313 |
+
return (avgReturn / stdDev) * Math.sqrt(252);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
private List<Double> calculateTradePnLs(List<Transaction> transactions) {
|
| 317 |
+
List<Double> pnls = new ArrayList<>();
|
| 318 |
+
Double buyPrice = null;
|
| 319 |
+
long buyShares = 0;
|
| 320 |
+
|
| 321 |
+
for (Transaction tx : transactions) {
|
| 322 |
+
if (tx.getType() == SignalType.BUY) {
|
| 323 |
+
buyPrice = tx.getPrice();
|
| 324 |
+
buyShares = tx.getShares();
|
| 325 |
+
} else if (tx.getType() == SignalType.SELL && buyPrice != null) {
|
| 326 |
+
double pnl = (tx.getPrice() - buyPrice) * buyShares;
|
| 327 |
+
pnls.add(pnl);
|
| 328 |
+
buyPrice = null;
|
| 329 |
+
buyShares = 0;
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
return pnls;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
private double calculateAvgHoldingPeriod(List<Transaction> transactions) {
|
| 336 |
+
List<Long> holdingDays = new ArrayList<>();
|
| 337 |
+
Transaction buyTx = null;
|
| 338 |
+
|
| 339 |
+
for (Transaction tx : transactions) {
|
| 340 |
+
if (tx.getType() == SignalType.BUY) {
|
| 341 |
+
buyTx = tx;
|
| 342 |
+
} else if (tx.getType() == SignalType.SELL && buyTx != null) {
|
| 343 |
+
long days = ChronoUnit.DAYS.between(buyTx.getDate(), tx.getDate());
|
| 344 |
+
holdingDays.add(days);
|
| 345 |
+
buyTx = null;
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
return holdingDays.isEmpty() ? 0 :
|
| 350 |
+
holdingDays.stream().mapToLong(Long::longValue).average().orElse(0);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
private PerformanceMetrics calculateAggregateMetrics(
|
| 354 |
+
Map<String, BacktestResult> symbolResults,
|
| 355 |
+
Map<String, Double> allocations,
|
| 356 |
+
double totalCapital
|
| 357 |
+
) {
|
| 358 |
+
// Weighted average of individual metrics
|
| 359 |
+
double weightedSharpe = 0;
|
| 360 |
+
double weightedReturn = 0;
|
| 361 |
+
double totalMaxDrawdown = 0;
|
| 362 |
+
int totalTrades = 0;
|
| 363 |
+
int totalWins = 0;
|
| 364 |
+
int totalLosses = 0;
|
| 365 |
+
|
| 366 |
+
for (var entry : symbolResults.entrySet()) {
|
| 367 |
+
double weight = allocations.getOrDefault(entry.getKey(), 0.0) / totalCapital;
|
| 368 |
+
PerformanceMetrics m = entry.getValue().getMetrics();
|
| 369 |
+
|
| 370 |
+
weightedSharpe += m.getSharpeRatio() * weight;
|
| 371 |
+
weightedReturn += m.getAnnualizedReturn() * weight;
|
| 372 |
+
totalMaxDrawdown = Math.min(totalMaxDrawdown, m.getMaxDrawdown());
|
| 373 |
+
totalTrades += m.getTotalTrades();
|
| 374 |
+
totalWins += m.getWinningTrades();
|
| 375 |
+
totalLosses += m.getLosingTrades();
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
double winRate = totalTrades == 0 ? 0 : (double) totalWins / totalTrades * 100.0;
|
| 379 |
+
|
| 380 |
+
return PerformanceMetrics.builder()
|
| 381 |
+
.sharpeRatio(round(weightedSharpe))
|
| 382 |
+
.maxDrawdown(round(totalMaxDrawdown))
|
| 383 |
+
.winRate(round(winRate))
|
| 384 |
+
.totalTrades(totalTrades)
|
| 385 |
+
.winningTrades(totalWins)
|
| 386 |
+
.losingTrades(totalLosses)
|
| 387 |
+
.annualizedReturn(round(weightedReturn))
|
| 388 |
+
.build();
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/* ==========================
|
| 392 |
+
Parameterized Strategy Factory
|
| 393 |
+
========================== */
|
| 394 |
+
|
| 395 |
+
private Strategy createParameterizedStrategy(StrategyType type, Map<String, String> params) {
|
| 396 |
+
return switch (type) {
|
| 397 |
+
case SMA -> {
|
| 398 |
+
int shortPeriod = Integer.parseInt(params.getOrDefault("shortPeriod", "20"));
|
| 399 |
+
int longPeriod = Integer.parseInt(params.getOrDefault("longPeriod", "50"));
|
| 400 |
+
yield new SmaCrossoverStrategy(shortPeriod, longPeriod);
|
| 401 |
+
}
|
| 402 |
+
case RSI -> {
|
| 403 |
+
// RSI uses class constants — for custom params, create a new configurable version
|
| 404 |
+
yield strategyFactory.getStrategy(type);
|
| 405 |
+
}
|
| 406 |
+
case MACD -> {
|
| 407 |
+
int fast = Integer.parseInt(params.getOrDefault("fastPeriod", "12"));
|
| 408 |
+
int slow = Integer.parseInt(params.getOrDefault("slowPeriod", "26"));
|
| 409 |
+
int signal = Integer.parseInt(params.getOrDefault("signalPeriod", "9"));
|
| 410 |
+
yield new MacdStrategy(fast, slow, signal);
|
| 411 |
+
}
|
| 412 |
+
case BUY_AND_HOLD -> strategyFactory.getStrategy(type);
|
| 413 |
+
};
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
private double round(double value) {
|
| 417 |
+
return Math.round(value * 100.0) / 100.0;
|
| 418 |
+
}
|
| 419 |
+
}
|
src/main/java/com/rods/backtestingstrategies/service/MarketDataService.java
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.service;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.StockSymbol;
|
| 5 |
+
import com.rods.backtestingstrategies.repository.CandleRepository;
|
| 6 |
+
import com.rods.backtestingstrategies.repository.StockSymbolRepository;
|
| 7 |
+
import org.springframework.dao.DataIntegrityViolationException;
|
| 8 |
+
import org.springframework.stereotype.Service;
|
| 9 |
+
import yahoofinance.histquotes.HistoricalQuote;
|
| 10 |
+
|
| 11 |
+
import java.io.IOException;
|
| 12 |
+
import java.math.BigDecimal;
|
| 13 |
+
import java.time.LocalDate;
|
| 14 |
+
import java.time.ZoneId;
|
| 15 |
+
import java.util.*;
|
| 16 |
+
|
| 17 |
+
@Service
|
| 18 |
+
public class MarketDataService {
|
| 19 |
+
|
| 20 |
+
private final YahooFinanceService yahooFinanceService;
|
| 21 |
+
private final CandleRepository candleRepository;
|
| 22 |
+
private final StockSymbolRepository symbolRepository;
|
| 23 |
+
|
| 24 |
+
public MarketDataService(YahooFinanceService yahooFinanceService,
|
| 25 |
+
CandleRepository candleRepository,
|
| 26 |
+
StockSymbolRepository symbolRepository) {
|
| 27 |
+
this.yahooFinanceService = yahooFinanceService;
|
| 28 |
+
this.candleRepository = candleRepository;
|
| 29 |
+
this.symbolRepository = symbolRepository;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Sync daily candle data from Yahoo Finance into the database.
|
| 34 |
+
* Only inserts candles for dates not yet stored.
|
| 35 |
+
*/
|
| 36 |
+
public void syncDailyCandles(String symbol) {
|
| 37 |
+
System.out.println("Sync Method Called for: " + symbol);
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
// Fetch 5 years of historical data for comprehensive backtesting
|
| 41 |
+
Calendar from = Calendar.getInstance();
|
| 42 |
+
from.add(Calendar.YEAR, -5);
|
| 43 |
+
Calendar to = Calendar.getInstance();
|
| 44 |
+
|
| 45 |
+
List<HistoricalQuote> history = yahooFinanceService.getHistoricalData(symbol, from, to);
|
| 46 |
+
|
| 47 |
+
if (history == null || history.isEmpty()) {
|
| 48 |
+
System.err.println("No historical data returned for: " + symbol);
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Fetch existing candle dates for this symbol
|
| 53 |
+
Set<LocalDate> existingDates = new HashSet<>(candleRepository.findExistingDates(symbol));
|
| 54 |
+
|
| 55 |
+
List<Candle> newCandles = new ArrayList<>();
|
| 56 |
+
|
| 57 |
+
for (HistoricalQuote quote : history) {
|
| 58 |
+
if (quote.getDate() == null || quote.getClose() == null) {
|
| 59 |
+
continue;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
LocalDate date = quote.getDate().getTime().toInstant()
|
| 63 |
+
.atZone(ZoneId.systemDefault()).toLocalDate();
|
| 64 |
+
|
| 65 |
+
// Skip if candle already exists
|
| 66 |
+
if (existingDates.contains(date)) {
|
| 67 |
+
continue;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
Candle candle = new Candle();
|
| 71 |
+
candle.setSymbol(symbol.toUpperCase());
|
| 72 |
+
candle.setDate(date);
|
| 73 |
+
candle.setOpenPrice(toDouble(quote.getOpen()));
|
| 74 |
+
candle.setHighPrice(toDouble(quote.getHigh()));
|
| 75 |
+
candle.setLowPrice(toDouble(quote.getLow()));
|
| 76 |
+
candle.setClosePrice(toDouble(quote.getClose()));
|
| 77 |
+
candle.setVolume(quote.getVolume() != null ? quote.getVolume() : 0L);
|
| 78 |
+
|
| 79 |
+
newCandles.add(candle);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Bulk insert
|
| 83 |
+
if (!newCandles.isEmpty()) {
|
| 84 |
+
try {
|
| 85 |
+
candleRepository.saveAll(newCandles);
|
| 86 |
+
System.out.println("Inserted " + newCandles.size() + " new candles for " + symbol);
|
| 87 |
+
} catch (DataIntegrityViolationException e) {
|
| 88 |
+
System.err.println("Duplicate candles skipped for: " + symbol);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
} catch (IOException e) {
|
| 92 |
+
System.err.println("Failed to fetch data from Yahoo Finance for: " + symbol + " - " + e.getMessage());
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Get candles for a symbol. Fetches from DB first, syncs from API if needed.
|
| 98 |
+
*/
|
| 99 |
+
public List<Candle> getCandles(String symbol) {
|
| 100 |
+
String upperSymbol = symbol.toUpperCase();
|
| 101 |
+
List<Candle> candles = candleRepository.findBySymbolOrderByDateAsc(upperSymbol);
|
| 102 |
+
|
| 103 |
+
// CASE 1: No data in DB → sync
|
| 104 |
+
if (candles.isEmpty()) {
|
| 105 |
+
syncDailyCandles(upperSymbol);
|
| 106 |
+
return candleRepository.findBySymbolOrderByDateAsc(upperSymbol);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// CASE 2: Data exists → check if stale (last candle > 1 day old)
|
| 110 |
+
LocalDate latestDate = candles.getLast().getDate();
|
| 111 |
+
if (LocalDate.now().isAfter(latestDate.plusDays(1))) {
|
| 112 |
+
syncDailyCandles(upperSymbol);
|
| 113 |
+
candles = candleRepository.findBySymbolOrderByDateAsc(upperSymbol);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return candles;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Search stock symbols from the local pre-seeded database.
|
| 121 |
+
* Supports fuzzy matching on symbol and company name.
|
| 122 |
+
*/
|
| 123 |
+
public List<StockSymbol> searchSymbols(String query) {
|
| 124 |
+
return symbolRepository.searchSymbols(query);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/**
|
| 128 |
+
* Search symbols filtered by exchange
|
| 129 |
+
*/
|
| 130 |
+
public List<StockSymbol> searchSymbolsByExchange(String query, String exchange) {
|
| 131 |
+
return symbolRepository.searchSymbolsByExchange(query, exchange);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* Get all symbols for a specific exchange
|
| 136 |
+
*/
|
| 137 |
+
public List<StockSymbol> getSymbolsByExchange(String exchange) {
|
| 138 |
+
return symbolRepository.findByExchange(exchange);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Safe BigDecimal to double conversion
|
| 143 |
+
*/
|
| 144 |
+
private double toDouble(BigDecimal value) {
|
| 145 |
+
return value != null ? value.doubleValue() : 0.0;
|
| 146 |
+
}
|
| 147 |
+
}
|
src/main/java/com/rods/backtestingstrategies/service/TickerSeederService.java
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.service;
|
| 2 |
+
|
| 3 |
+
import com.fasterxml.jackson.databind.JsonNode;
|
| 4 |
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
| 5 |
+
import com.rods.backtestingstrategies.entity.StockSymbol;
|
| 6 |
+
import com.rods.backtestingstrategies.repository.StockSymbolRepository;
|
| 7 |
+
import jakarta.annotation.PostConstruct;
|
| 8 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 9 |
+
import org.springframework.core.io.ClassPathResource;
|
| 10 |
+
import org.springframework.stereotype.Service;
|
| 11 |
+
|
| 12 |
+
import java.io.IOException;
|
| 13 |
+
import java.io.InputStream;
|
| 14 |
+
import java.time.LocalDateTime;
|
| 15 |
+
import java.util.ArrayList;
|
| 16 |
+
import java.util.List;
|
| 17 |
+
|
| 18 |
+
/**
|
| 19 |
+
* Service to populate the stock_symbols table on startup
|
| 20 |
+
* from a curated JSON file of global exchange tickers.
|
| 21 |
+
*
|
| 22 |
+
* Covers: NASDAQ, NYSE/S&P 500, NSE (India), BSE (India), LSE (UK), TSE (Japan).
|
| 23 |
+
* Runs only when tickers are missing from the DB.
|
| 24 |
+
*/
|
| 25 |
+
@Service
|
| 26 |
+
public class TickerSeederService {
|
| 27 |
+
|
| 28 |
+
private final StockSymbolRepository symbolRepository;
|
| 29 |
+
private final ObjectMapper objectMapper;
|
| 30 |
+
|
| 31 |
+
@Value("${ticker.seeder.enabled:true}")
|
| 32 |
+
private boolean seederEnabled;
|
| 33 |
+
|
| 34 |
+
public TickerSeederService(StockSymbolRepository symbolRepository) {
|
| 35 |
+
this.symbolRepository = symbolRepository;
|
| 36 |
+
this.objectMapper = new ObjectMapper();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
@PostConstruct
|
| 40 |
+
public void seedTickers() {
|
| 41 |
+
if (!seederEnabled) {
|
| 42 |
+
System.out.println("Ticker seeder is disabled.");
|
| 43 |
+
return;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
long existingCount = symbolRepository.count();
|
| 47 |
+
if (existingCount > 0) {
|
| 48 |
+
System.out.println("Ticker database already populated with " + existingCount + " symbols. Skipping seed.");
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
System.out.println("Seeding ticker database from ticker_data.json...");
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
ClassPathResource resource = new ClassPathResource("ticker_data.json");
|
| 56 |
+
InputStream inputStream = resource.getInputStream();
|
| 57 |
+
JsonNode root = objectMapper.readTree(inputStream);
|
| 58 |
+
JsonNode exchanges = root.get("exchanges");
|
| 59 |
+
|
| 60 |
+
List<StockSymbol> allSymbols = new ArrayList<>();
|
| 61 |
+
LocalDateTime now = LocalDateTime.now();
|
| 62 |
+
|
| 63 |
+
for (JsonNode exchange : exchanges) {
|
| 64 |
+
String exchangeName = exchange.get("name").asText();
|
| 65 |
+
String region = exchange.get("region").asText();
|
| 66 |
+
String currency = exchange.get("currency").asText();
|
| 67 |
+
String timezone = exchange.get("timezone").asText();
|
| 68 |
+
String marketOpen = exchange.get("marketOpen").asText();
|
| 69 |
+
String marketClose = exchange.get("marketClose").asText();
|
| 70 |
+
|
| 71 |
+
JsonNode tickers = exchange.get("tickers");
|
| 72 |
+
for (JsonNode ticker : tickers) {
|
| 73 |
+
StockSymbol symbol = new StockSymbol();
|
| 74 |
+
symbol.setSymbol(ticker.get("symbol").asText());
|
| 75 |
+
symbol.setName(ticker.get("name").asText());
|
| 76 |
+
symbol.setType("Equity");
|
| 77 |
+
symbol.setExchange(exchangeName);
|
| 78 |
+
symbol.setRegion(region);
|
| 79 |
+
symbol.setCurrency(currency);
|
| 80 |
+
symbol.setTimezone(timezone);
|
| 81 |
+
symbol.setMarketOpen(marketOpen);
|
| 82 |
+
symbol.setMarketClose(marketClose);
|
| 83 |
+
symbol.setSector(ticker.has("sector") ? ticker.get("sector").asText() : null);
|
| 84 |
+
symbol.setIndustry(ticker.has("industry") ? ticker.get("industry").asText() : null);
|
| 85 |
+
symbol.setMatchScore(1.0);
|
| 86 |
+
symbol.setLastFetched(now);
|
| 87 |
+
symbol.setSource("TICKER_SEED");
|
| 88 |
+
|
| 89 |
+
allSymbols.add(symbol);
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
symbolRepository.saveAll(allSymbols);
|
| 94 |
+
System.out.println("Successfully seeded " + allSymbols.size() + " tickers across " +
|
| 95 |
+
exchanges.size() + " exchanges.");
|
| 96 |
+
|
| 97 |
+
} catch (IOException e) {
|
| 98 |
+
System.err.println("Failed to seed ticker data: " + e.getMessage());
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Manual re-seed: clears all existing symbols and re-imports from JSON.
|
| 104 |
+
* Use this endpoint for manual updates.
|
| 105 |
+
*/
|
| 106 |
+
public int reseedTickers() {
|
| 107 |
+
symbolRepository.deleteAll();
|
| 108 |
+
seedTickers();
|
| 109 |
+
return (int) symbolRepository.count();
|
| 110 |
+
}
|
| 111 |
+
}
|
src/main/java/com/rods/backtestingstrategies/service/YahooFinanceService.java
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.service;
|
| 2 |
+
|
| 3 |
+
import org.springframework.stereotype.Service;
|
| 4 |
+
import yahoofinance.Stock;
|
| 5 |
+
import yahoofinance.YahooFinance;
|
| 6 |
+
import yahoofinance.histquotes.HistoricalQuote;
|
| 7 |
+
import yahoofinance.histquotes.Interval;
|
| 8 |
+
import yahoofinance.quotes.stock.StockQuote;
|
| 9 |
+
import yahoofinance.quotes.stock.StockStats;
|
| 10 |
+
|
| 11 |
+
import java.io.IOException;
|
| 12 |
+
import java.util.Calendar;
|
| 13 |
+
import java.util.List;
|
| 14 |
+
import java.util.Map;
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Service wrapping the Yahoo Finance API.
|
| 18 |
+
* No API key required. No enforced rate limits.
|
| 19 |
+
*/
|
| 20 |
+
@Service
|
| 21 |
+
public class YahooFinanceService {
|
| 22 |
+
|
| 23 |
+
static {
|
| 24 |
+
// Set standard user-agent so Yahoo doesn't reject as bot with 429 Error
|
| 25 |
+
System.setProperty("http.agent",
|
| 26 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36");
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* Fetch full stock info (quote, stats, dividend)
|
| 31 |
+
*/
|
| 32 |
+
public Stock getStock(String symbol) throws IOException {
|
| 33 |
+
return YahooFinance.get(symbol);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Fetch stock with full historical data (default: 1 year, daily)
|
| 38 |
+
*/
|
| 39 |
+
public Stock getStockWithHistory(String symbol) throws IOException {
|
| 40 |
+
return YahooFinance.get(symbol, true);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/**
|
| 44 |
+
* Fetch historical daily data for a custom date range using v8 API (Bypasses
|
| 45 |
+
* 429)
|
| 46 |
+
*/
|
| 47 |
+
public List<HistoricalQuote> getHistoricalData(String symbol, Calendar from, Calendar to) throws IOException {
|
| 48 |
+
String urlString = String.format(
|
| 49 |
+
"https://query1.finance.yahoo.com/v8/finance/chart/%s?period1=%d&period2=%d&interval=1d",
|
| 50 |
+
symbol, from.getTimeInMillis() / 1000, to.getTimeInMillis() / 1000);
|
| 51 |
+
|
| 52 |
+
return fetchHistoryFromV8(symbol, urlString);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Fetch historical daily data with default lookback (1 year)
|
| 57 |
+
*/
|
| 58 |
+
public List<HistoricalQuote> getHistoricalData(String symbol) throws IOException {
|
| 59 |
+
Calendar from = Calendar.getInstance();
|
| 60 |
+
from.add(Calendar.YEAR, -1);
|
| 61 |
+
Calendar to = Calendar.getInstance();
|
| 62 |
+
return getHistoricalData(symbol, from, to);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
private List<HistoricalQuote> fetchHistoryFromV8(String symbol, String urlString) throws IOException {
|
| 66 |
+
java.util.List<HistoricalQuote> history = new java.util.ArrayList<>();
|
| 67 |
+
try {
|
| 68 |
+
java.net.URL url = new java.net.URL(urlString);
|
| 69 |
+
java.net.HttpURLConnection request = (java.net.HttpURLConnection) url.openConnection();
|
| 70 |
+
request.setRequestMethod("GET");
|
| 71 |
+
request.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
| 72 |
+
request.connect();
|
| 73 |
+
|
| 74 |
+
int responseCode = request.getResponseCode();
|
| 75 |
+
if (responseCode == 404)
|
| 76 |
+
return history; // Stock not found
|
| 77 |
+
|
| 78 |
+
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
| 79 |
+
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(request.getInputStream());
|
| 80 |
+
com.fasterxml.jackson.databind.JsonNode result = root.path("chart").path("result").get(0);
|
| 81 |
+
|
| 82 |
+
if (result == null || result.isMissingNode())
|
| 83 |
+
return history;
|
| 84 |
+
|
| 85 |
+
com.fasterxml.jackson.databind.JsonNode timestampNode = result.path("timestamp");
|
| 86 |
+
com.fasterxml.jackson.databind.JsonNode quoteNode = result.path("indicators").path("quote").get(0);
|
| 87 |
+
com.fasterxml.jackson.databind.JsonNode adjCloseNode = result.path("indicators").path("adjclose").get(0);
|
| 88 |
+
|
| 89 |
+
if (timestampNode.isMissingNode() || quoteNode.isMissingNode())
|
| 90 |
+
return history;
|
| 91 |
+
|
| 92 |
+
for (int i = 0; i < timestampNode.size(); i++) {
|
| 93 |
+
long timestamp = timestampNode.get(i).asLong();
|
| 94 |
+
java.util.Calendar date = java.util.Calendar.getInstance();
|
| 95 |
+
date.setTimeInMillis(timestamp * 1000);
|
| 96 |
+
|
| 97 |
+
java.math.BigDecimal open = getBigDecimal(quoteNode.path("open").get(i));
|
| 98 |
+
java.math.BigDecimal high = getBigDecimal(quoteNode.path("high").get(i));
|
| 99 |
+
java.math.BigDecimal low = getBigDecimal(quoteNode.path("low").get(i));
|
| 100 |
+
java.math.BigDecimal close = getBigDecimal(quoteNode.path("close").get(i));
|
| 101 |
+
java.math.BigDecimal adjClose = adjCloseNode != null && !adjCloseNode.isMissingNode()
|
| 102 |
+
? getBigDecimal(adjCloseNode.path("adjclose").get(i))
|
| 103 |
+
: close;
|
| 104 |
+
long volume = quoteNode.path("volume").get(i) != null ? quoteNode.path("volume").get(i).asLong() : 0L;
|
| 105 |
+
|
| 106 |
+
if (close != null) {
|
| 107 |
+
history.add(new HistoricalQuote(symbol, date, open, low, high, close, adjClose, volume));
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
} catch (Exception e) {
|
| 111 |
+
throw new IOException("Failed to fetch custom Yahoo v8 API for " + symbol, e);
|
| 112 |
+
}
|
| 113 |
+
return history;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
private java.math.BigDecimal getBigDecimal(com.fasterxml.jackson.databind.JsonNode node) {
|
| 117 |
+
if (node == null || node.isNull() || node.isMissingNode())
|
| 118 |
+
return null;
|
| 119 |
+
return new java.math.BigDecimal(node.asText());
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Fetch multiple stocks at once (single batch request)
|
| 124 |
+
*/
|
| 125 |
+
public Map<String, Stock> getMultipleStocks(String[] symbols) throws IOException {
|
| 126 |
+
return YahooFinance.get(symbols);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* Validate if a symbol exists on Yahoo Finance
|
| 131 |
+
*/
|
| 132 |
+
public boolean isValidSymbol(String symbol) {
|
| 133 |
+
try {
|
| 134 |
+
Stock stock = YahooFinance.get(symbol);
|
| 135 |
+
return stock != null && stock.getQuote() != null && stock.getQuote().getPrice() != null;
|
| 136 |
+
} catch (IOException e) {
|
| 137 |
+
return false;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/**
|
| 142 |
+
* Get real-time quote data
|
| 143 |
+
*/
|
| 144 |
+
public StockQuote getQuote(String symbol) throws IOException {
|
| 145 |
+
Stock stock = YahooFinance.get(symbol);
|
| 146 |
+
return stock.getQuote();
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Get stock statistics (PE, EPS, market cap, etc.)
|
| 151 |
+
*/
|
| 152 |
+
public StockStats getStats(String symbol) throws IOException {
|
| 153 |
+
Stock stock = YahooFinance.get(symbol);
|
| 154 |
+
return stock.getStats();
|
| 155 |
+
}
|
| 156 |
+
}
|
src/main/java/com/rods/backtestingstrategies/strategy/BuyAndHoldStrategy.java
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.strategy;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.SignalType;
|
| 5 |
+
import com.rods.backtestingstrategies.entity.TradeSignal;
|
| 6 |
+
import org.springframework.stereotype.Component;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
|
| 10 |
+
@Component
|
| 11 |
+
public class BuyAndHoldStrategy implements Strategy {
|
| 12 |
+
|
| 13 |
+
@Override
|
| 14 |
+
public TradeSignal evaluate(List<Candle> candles, int index) {
|
| 15 |
+
|
| 16 |
+
Candle candle = candles.get(index);
|
| 17 |
+
|
| 18 |
+
// BUY on first candle
|
| 19 |
+
if (index == 0) {
|
| 20 |
+
return TradeSignal.buy(candle);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// SELL on last candle
|
| 24 |
+
if (index == candles.size() - 1) {
|
| 25 |
+
return TradeSignal.sell(candle);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Otherwise HOLD
|
| 29 |
+
return TradeSignal.hold();
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
@Override
|
| 33 |
+
public StrategyType getType() {
|
| 34 |
+
return StrategyType.BUY_AND_HOLD;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
@Override
|
| 38 |
+
public String getName() {
|
| 39 |
+
return "Buy & Hold";
|
| 40 |
+
}
|
| 41 |
+
}
|
src/main/java/com/rods/backtestingstrategies/strategy/MacdStrategy.java
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.strategy;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.TradeSignal;
|
| 5 |
+
import org.springframework.stereotype.Component;
|
| 6 |
+
|
| 7 |
+
import java.util.List;
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* MACD (Moving Average Convergence Divergence) Strategy.
|
| 11 |
+
*
|
| 12 |
+
* MACD Line = EMA(fastPeriod) - EMA(slowPeriod)
|
| 13 |
+
* Signal Line = EMA(signalPeriod) of MACD Line
|
| 14 |
+
*
|
| 15 |
+
* BUY → MACD crosses above Signal Line
|
| 16 |
+
* SELL → MACD crosses below Signal Line
|
| 17 |
+
*/
|
| 18 |
+
@Component
|
| 19 |
+
public class MacdStrategy implements Strategy {
|
| 20 |
+
|
| 21 |
+
private final int fastPeriod;
|
| 22 |
+
private final int slowPeriod;
|
| 23 |
+
private final int signalPeriod;
|
| 24 |
+
|
| 25 |
+
public MacdStrategy() {
|
| 26 |
+
// Standard MACD(12, 26, 9)
|
| 27 |
+
this.fastPeriod = 12;
|
| 28 |
+
this.slowPeriod = 26;
|
| 29 |
+
this.signalPeriod = 9;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
public MacdStrategy(int fastPeriod, int slowPeriod, int signalPeriod) {
|
| 33 |
+
this.fastPeriod = fastPeriod;
|
| 34 |
+
this.slowPeriod = slowPeriod;
|
| 35 |
+
this.signalPeriod = signalPeriod;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
@Override
|
| 39 |
+
public TradeSignal evaluate(List<Candle> candles, int index) {
|
| 40 |
+
|
| 41 |
+
// Need enough data: slowPeriod + signalPeriod candles minimum
|
| 42 |
+
int minRequired = slowPeriod + signalPeriod;
|
| 43 |
+
if (index < minRequired) {
|
| 44 |
+
return TradeSignal.hold();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
Candle candle = candles.get(index);
|
| 48 |
+
|
| 49 |
+
// Calculate MACD and Signal for current and previous index
|
| 50 |
+
double currMacd = calculateMacd(candles, index);
|
| 51 |
+
double prevMacd = calculateMacd(candles, index - 1);
|
| 52 |
+
|
| 53 |
+
double currSignal = calculateSignalLine(candles, index);
|
| 54 |
+
double prevSignal = calculateSignalLine(candles, index - 1);
|
| 55 |
+
|
| 56 |
+
// MACD crosses above Signal → BUY
|
| 57 |
+
if (prevMacd <= prevSignal && currMacd > currSignal) {
|
| 58 |
+
return TradeSignal.buy(candle);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// MACD crosses below Signal → SELL
|
| 62 |
+
if (prevMacd >= prevSignal && currMacd < currSignal) {
|
| 63 |
+
return TradeSignal.sell(candle);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return TradeSignal.hold();
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Calculate MACD line = EMA(fast) - EMA(slow)
|
| 71 |
+
*/
|
| 72 |
+
private double calculateMacd(List<Candle> candles, int index) {
|
| 73 |
+
double fastEma = calculateEma(candles, index, fastPeriod);
|
| 74 |
+
double slowEma = calculateEma(candles, index, slowPeriod);
|
| 75 |
+
return fastEma - slowEma;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Calculate Signal line = EMA(signalPeriod) of MACD values
|
| 80 |
+
*/
|
| 81 |
+
private double calculateSignalLine(List<Candle> candles, int index) {
|
| 82 |
+
// We need 'signalPeriod' MACD values ending at 'index'
|
| 83 |
+
double multiplier = 2.0 / (signalPeriod + 1);
|
| 84 |
+
|
| 85 |
+
// Seed with the oldest MACD value in the window
|
| 86 |
+
double signalEma = calculateMacd(candles, index - signalPeriod + 1);
|
| 87 |
+
|
| 88 |
+
for (int i = index - signalPeriod + 2; i <= index; i++) {
|
| 89 |
+
double macdValue = calculateMacd(candles, i);
|
| 90 |
+
signalEma = (macdValue - signalEma) * multiplier + signalEma;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
return signalEma;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Calculate Exponential Moving Average at a given index.
|
| 98 |
+
* Uses the standard EMA formula with SMA as the seed value.
|
| 99 |
+
*/
|
| 100 |
+
private double calculateEma(List<Candle> candles, int index, int period) {
|
| 101 |
+
if (index < period - 1) {
|
| 102 |
+
// Not enough data, return SMA
|
| 103 |
+
return sma(candles, index, Math.min(period, index + 1));
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
double multiplier = 2.0 / (period + 1);
|
| 107 |
+
|
| 108 |
+
// Seed EMA with SMA of the first 'period' candles
|
| 109 |
+
double ema = sma(candles, period - 1, period);
|
| 110 |
+
|
| 111 |
+
// Calculate EMA from period to index
|
| 112 |
+
for (int i = period; i <= index; i++) {
|
| 113 |
+
double price = candles.get(i).getClosePrice();
|
| 114 |
+
ema = (price - ema) * multiplier + ema;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return ema;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Simple Moving Average helper
|
| 122 |
+
*/
|
| 123 |
+
private double sma(List<Candle> candles, int endIndex, int period) {
|
| 124 |
+
double sum = 0.0;
|
| 125 |
+
int start = Math.max(0, endIndex - period + 1);
|
| 126 |
+
for (int i = start; i <= endIndex; i++) {
|
| 127 |
+
sum += candles.get(i).getClosePrice();
|
| 128 |
+
}
|
| 129 |
+
return sum / (endIndex - start + 1);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
@Override
|
| 133 |
+
public String getName() {
|
| 134 |
+
return "MACD (" + fastPeriod + ", " + slowPeriod + ", " + signalPeriod + ")";
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
@Override
|
| 138 |
+
public StrategyType getType() {
|
| 139 |
+
return StrategyType.MACD;
|
| 140 |
+
}
|
| 141 |
+
}
|
src/main/java/com/rods/backtestingstrategies/strategy/RsiStrategy.java
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.rods.backtestingstrategies.strategy;
|
| 2 |
+
|
| 3 |
+
import com.rods.backtestingstrategies.entity.Candle;
|
| 4 |
+
import com.rods.backtestingstrategies.entity.TradeSignal;
|
| 5 |
+
import com.rods.backtestingstrategies.entity.SignalType;
|
| 6 |
+
import org.springframework.stereotype.Component;
|
| 7 |
+
|
| 8 |
+
import java.util.List;
|
| 9 |
+
|
| 10 |
+
@Component
|
| 11 |
+
public class RsiStrategy implements Strategy {
|
| 12 |
+
|
| 13 |
+
private static final int PERIOD = 14;
|
| 14 |
+
private static final double OVERSOLD = 30.0;
|
| 15 |
+
private static final double OVERBOUGHT = 70.0;
|
| 16 |
+
|
| 17 |
+
@Override
|
| 18 |
+
public TradeSignal evaluate(List<Candle> candles, int index) {
|
| 19 |
+
|
| 20 |
+
// Not enough data
|
| 21 |
+
if (index < PERIOD) {
|
| 22 |
+
return TradeSignal.hold();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
double rsi = calculateRsi(candles, index);
|
| 26 |
+
|
| 27 |
+
Candle candle = candles.get(index);
|
| 28 |
+
|
| 29 |
+
if (rsi < OVERSOLD) {
|
| 30 |
+
return TradeSignal.buy(candle);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if (rsi > OVERBOUGHT) {
|
| 34 |
+
return TradeSignal.sell(candle);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return TradeSignal.hold();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
@Override
|
| 41 |
+
public StrategyType getType() {
|
| 42 |
+
return StrategyType.RSI;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
@Override
|
| 46 |
+
public String getName() {
|
| 47 |
+
return "RSI Mean Reversion (14)";
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* ==========================
|
| 51 |
+
RSI Calculation
|
| 52 |
+
========================== */
|
| 53 |
+
|
| 54 |
+
private double calculateRsi(List<Candle> candles, int index) {
|
| 55 |
+
|
| 56 |
+
double gain = 0.0;
|
| 57 |
+
double loss = 0.0;
|
| 58 |
+
|
| 59 |
+
for (int i = index - PERIOD + 1; i <= index; i++) {
|
| 60 |
+
double change =
|
| 61 |
+
candles.get(i).getClosePrice()
|
| 62 |
+
- candles.get(i - 1).getClosePrice();
|
| 63 |
+
|
| 64 |
+
if (change > 0) {
|
| 65 |
+
gain += change;
|
| 66 |
+
} else {
|
| 67 |
+
loss -= change;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
if (loss == 0) {
|
| 72 |
+
return 100.0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
double rs = gain / loss;
|
| 76 |
+
return 100.0 - (100.0 / (1.0 + rs));
|
| 77 |
+
}
|
| 78 |
+
}
|