uncertainrods commited on
Commit
f5cd2d3
·
1 Parent(s): 5e92b78

init_files_added

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +5 -0
  2. .gitignore +36 -0
  3. .mvn/wrapper/maven-wrapper.properties +3 -0
  4. Dockerfile +30 -0
  5. TestYahoo.java +15 -0
  6. mvnw +295 -0
  7. mvnw.cmd +189 -0
  8. pom.xml +144 -0
  9. src/main/java/com/rods/backtestingstrategies/BacktestingStrategiesApplication.java +13 -0
  10. src/main/java/com/rods/backtestingstrategies/TestYahooCrumb.java +52 -0
  11. src/main/java/com/rods/backtestingstrategies/config/CorsConfig.java +28 -0
  12. src/main/java/com/rods/backtestingstrategies/controller/AuthController.java +59 -0
  13. src/main/java/com/rods/backtestingstrategies/controller/BacktestController.java +99 -0
  14. src/main/java/com/rods/backtestingstrategies/controller/MarketController.java +104 -0
  15. src/main/java/com/rods/backtestingstrategies/controller/SymbolController.java +90 -0
  16. src/main/java/com/rods/backtestingstrategies/dto/AuthRequest.java +15 -0
  17. src/main/java/com/rods/backtestingstrategies/dto/AuthResponse.java +14 -0
  18. src/main/java/com/rods/backtestingstrategies/entity/AuthenticationRequest.java +20 -0
  19. src/main/java/com/rods/backtestingstrategies/entity/AuthenticationResponse.java +14 -0
  20. src/main/java/com/rods/backtestingstrategies/entity/BacktestResult.java +52 -0
  21. src/main/java/com/rods/backtestingstrategies/entity/Candle.java +37 -0
  22. src/main/java/com/rods/backtestingstrategies/entity/CrossOver.java +60 -0
  23. src/main/java/com/rods/backtestingstrategies/entity/CrossOverType.java +6 -0
  24. src/main/java/com/rods/backtestingstrategies/entity/EquityPoint.java +67 -0
  25. src/main/java/com/rods/backtestingstrategies/entity/PerformanceMetrics.java +53 -0
  26. src/main/java/com/rods/backtestingstrategies/entity/PortfolioRequest.java +30 -0
  27. src/main/java/com/rods/backtestingstrategies/entity/PortfolioResult.java +33 -0
  28. src/main/java/com/rods/backtestingstrategies/entity/QuoteSummary.java +48 -0
  29. src/main/java/com/rods/backtestingstrategies/entity/RegisterRequest.java +20 -0
  30. src/main/java/com/rods/backtestingstrategies/entity/SignalType.java +9 -0
  31. src/main/java/com/rods/backtestingstrategies/entity/Stock.java +22 -0
  32. src/main/java/com/rods/backtestingstrategies/entity/StockSymbol.java +84 -0
  33. src/main/java/com/rods/backtestingstrategies/entity/StrategyComparisonResult.java +34 -0
  34. src/main/java/com/rods/backtestingstrategies/entity/TradeSignal.java +85 -0
  35. src/main/java/com/rods/backtestingstrategies/entity/Transaction.java +90 -0
  36. src/main/java/com/rods/backtestingstrategies/entity/User.java +29 -0
  37. src/main/java/com/rods/backtestingstrategies/repository/CandleRepository.java +22 -0
  38. src/main/java/com/rods/backtestingstrategies/repository/StockSymbolRepository.java +90 -0
  39. src/main/java/com/rods/backtestingstrategies/repository/UserRepository.java +10 -0
  40. src/main/java/com/rods/backtestingstrategies/security/CustomUserDetailsService.java +30 -0
  41. src/main/java/com/rods/backtestingstrategies/security/JwtAuthenticationFilter.java +59 -0
  42. src/main/java/com/rods/backtestingstrategies/security/JwtUtils.java +97 -0
  43. src/main/java/com/rods/backtestingstrategies/security/SecurityConfig.java +77 -0
  44. src/main/java/com/rods/backtestingstrategies/service/BacktestService.java +419 -0
  45. src/main/java/com/rods/backtestingstrategies/service/MarketDataService.java +147 -0
  46. src/main/java/com/rods/backtestingstrategies/service/TickerSeederService.java +111 -0
  47. src/main/java/com/rods/backtestingstrategies/service/YahooFinanceService.java +156 -0
  48. src/main/java/com/rods/backtestingstrategies/strategy/BuyAndHoldStrategy.java +41 -0
  49. src/main/java/com/rods/backtestingstrategies/strategy/MacdStrategy.java +141 -0
  50. 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
+ }