From 0f0cbcbd3bf2136bc6ba23ece17e37a2a910dae3 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:57:16 -0400 Subject: [PATCH 1/9] Fix CSI300 tutorial data loading, benchmark and default workload --- docs/csi300-tutorial.md | 34 +++++++ .../csi300/CSI300SimulationDemo.java | 58 ++++++----- .../csi300/LocalCSI300DataProvider.java | 97 +++++++++++++++++++ .../csi300/LocalCSI300DataProviderTest.java | 53 ++++++++++ 4 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 docs/csi300-tutorial.md create mode 100644 src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProvider.java create mode 100644 src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md new file mode 100644 index 0000000..a3ef23b --- /dev/null +++ b/docs/csi300-tutorial.md @@ -0,0 +1,34 @@ +# CSI300 simulation tutorial + +`CSI300SimulationDemo` demonstrates AlgoQuant's historical-universe and simulation APIs. Its outputs teach the workflow; they are not evidence of investment performance. + +## Run + +Supply genuine CSI300 index observations in a UTF-8 CSV with exactly two columns: + +```csv +date,close +2009-12-01,3500.00 +2009-12-02,3510.00 +``` + +Dates must be ascending and unique, prices positive and finite. Supply the whole calibration and simulation history, not just the two illustrative rows above. Constituent membership files (including `indices/hs300.xlsx`) are not index prices. The default simulation is January 2010 through January 2017; the baseline calibration requires three preceding weeks. The full optimizer grid needs one preceding year plus two weeks. Endpoint validation allows ten calendar days for holidays; benchmark lookups reject observations older than ten days. + +```sh +mvn -Dexec.mainClass=tech.nmfin.algoquant.demo.model.portfoliooptimization.csi300.CSI300SimulationDemo \ + -Dcsi300.indexCsv=/data/csi300-index.csv \ + -Dcsi300.constituents=/data/constituent_index_300.csv \ + -Dcsi300.prices=/data/adjust_prices compile exec:java +``` + +Constituent prices retain the existing per-stock ZIP format and filenames (for example `000001.SZ.csv.zip`). This provider never falls back to Yahoo. The report uses the supplied CSI300 index as its benchmark and explicitly assumes a zero risk-free rate. + +By default, two simulations run: buy-and-hold index and monthly equal-weight constituents. Add `-Dcsi300.fullGrid=true` for the original 990 optimizer simulations plus the baselines. The grid runs sequentially to bound concurrent simulation memory and keep result ordering reproducible; it remains an expensive research run. Progress identifies each simulation as it starts and finishes. + +CSV ZIP reports and the PNG chart are saved under `log/`. GUI windows are disabled by default; add `-Dcsi300.showCharts=true` to display the chart. + +## Repair validation + +Compiled both modified classes with Java 8 against AlgoQuant 3.3.0-SNAPSHOT. Seven regression tests passed, covering valid CSVs, malformed/empty data, duplicate dates, missing files, index product value equality, and past-only/stale benchmark lookup. An end-to-end run with explicitly synthetic fixture prices and two constituents completed both baseline simulations, exported CSV ZIPs and a PNG headlessly. Synthetic data is solely a software test, not a substitute historical benchmark. + +The real 2010–2017 index history is not bundled. That historical run and the 990-optimizer grid have not been validated by this repair. Missing genuine history now produces an actionable error before loading constituents or starting simulations. diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java index 02917c6..0aa90e3 100644 --- a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java @@ -28,6 +28,7 @@ import dev.nm.nmutils.plot.api.Chart; import dev.nm.solver.multivariate.constrained.general.penaltymethod.PenaltyMethodMinimizer; import java.io.File; +import java.nio.file.Paths; import java.io.IOException; import java.time.LocalDateTime; import java.time.Period; @@ -37,7 +38,6 @@ import org.slf4j.Logger; import tech.nmfin.algoquant.data.cache.SequentialCache; import tech.nmfin.algoquant.data.cache.TimedEntry; -import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CSI300DataProvider; import tech.nmfin.algoquant.execution.component.chart.ChartUtils; import tech.nmfin.algoquant.execution.report.SimulationResultsExporter; import tech.nmfin.algoquant.execution.strategy.NamedParam; @@ -57,7 +57,13 @@ *

Using this example. * Configures CSI 300 constituents and local adjusted-price files for a portfolio simulation. * The constituent CSV and price directory are independent inputs and must cover compatible - * dates. Paths in main are examples to adapt to your own dataset. + * dates. Set {@code -Dcsi300.indexCsv=/path/to/index.csv} to a genuine CSI300 + * history with header {@code date,close} and ISO dates. Optional properties + * {@code csi300.constituents} and {@code csi300.prices} override local paths. + * The default runs buy-and-hold and equal-weight baselines only; opt into the + * expensive 990-optimizer sweep with {@code -Dcsi300.fullGrid=true}. + * No network data is downloaded. Reported risk-free rates are assumed zero. + * Set {@code csi300.showCharts=true} to open chart windows; PNG export is automatic. *

* *

Model and publication. @@ -88,33 +94,28 @@ public static void main(String[] args) throws IOException { } private final LocalDateTimeInterval interval; - private final CSI300DataProvider dataProvider; + private final LocalCSI300DataProvider dataProvider; public CSI300SimulationDemo(LocalDateTimeInterval interval) throws IOException { this.interval = interval; - this.dataProvider - = new CSI300DataProvider( - "./test-data/china/constituent_index_300.csv", - // "D:\\Data\\ZQF\\Daily\\1991_20180727", // TODO: change this to your own data folder - "./test-data/china/20070104-20171027/adjust_prices", // TODO: change this to your own data folder - true, - LocalDateTimeInterval.of( - interval.start().minusYears(5), - interval.end()), - false // offline - ); + this.dataProvider = new LocalCSI300DataProvider( + System.getProperty("csi300.constituents", "./test-data/china/constituent_index_300.csv"), + System.getProperty("csi300.prices", "./test-data/china/20070104-20171027/adjust_prices"), + Paths.get(System.getProperty("csi300.indexCsv", "./test-data/china/csi300-index.csv")), + interval + ); } public int run() throws IOException { - // A constituent workbook is not the official index price history. - // Validate the benchmark before launching the expensive parameter grid; - // never publish an empty-history buy-and-hold account as a CSI300 result. - dataProvider.getCache(new tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock( - "000300.SS", tech.nmfin.algoquant.execution.datatype.product.stock.Exchange.SS), interval); + LOGGER.info("CSI300 tutorial: local data only; full research grid enabled={}", + Boolean.getBoolean("csi300.fullGrid")); Map results = new LinkedHashMap<>(); results.putAll(simulateBaseline(interval)); - results.putAll(simulateAlgorithms(interval)); + if (Boolean.getBoolean("csi300.fullGrid")) { + LOGGER.info("Starting full research grid: 990 optimizer simulations"); + results.putAll(simulateAlgorithms(interval)); + } LOGGER.info(String.format("simulating %d sets of parameters/strategies", results.size())); saveResults(results); @@ -167,7 +168,7 @@ private Map simulateEqual(LocalDateTimeInterval interval) { Map results = new ConcurrentHashMap<>(); - params.parallelStream().forEach(param + params.stream().forEach(param -> results.putAll(simulateEqual(interval, param)) ); return results; @@ -200,7 +201,7 @@ private Map simulateAlgorithms( .params(); Map results = new ConcurrentHashMap<>(); - params.parallelStream().forEach(param -> { + params.stream().forEach(param -> { results.putAll(simulateAlgorithms(interval, param)); }); @@ -451,6 +452,10 @@ private Map simulateAlgorithm( param.value("riskAverse"), false ); + // Use the actual CSI300 benchmark rather than the simulation's SP500/Yahoo defaults. + sim.setBenchmarkLookupFactory("CSI300", ignored -> dataProvider::indexValueAt); + // Explicit tutorial assumption: zero risk-free rate (not a downloaded US bill yield). + sim.setRiskFreeRateLookup(time -> 0.0); String algoName = param.value("algorithm"); sim.addAlgorithm(algoName, algorithm); LOGGER.info("simulating {} for interval {}...", algoName, interval); @@ -491,8 +496,12 @@ private void plotCurves( new ArrayList<>(curves.values()), true ); - chart.show(); - chart.saveAsPNG(new File("./log/" + title + ".png"), 1200, 800); + if (Boolean.getBoolean("csi300.showCharts")) { + chart.show(); + } + File output = new File("./log/" + title + ".png"); + java.nio.file.Files.createDirectories(output.toPath().getParent()); + chart.saveAsPNG(output, 1200, 800); } private LinkedHashMap> extractProfitCurves( @@ -526,3 +535,4 @@ private void saveResults( } } + diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProvider.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProvider.java new file mode 100644 index 0000000..624a037 --- /dev/null +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProvider.java @@ -0,0 +1,97 @@ +package tech.nmfin.algoquant.demo.model.portfoliooptimization.csi300; + +import dev.nm.misc.datastructure.time.LocalDateTimeInterval; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; +import tech.nmfin.algoquant.data.cache.BufferCache; +import tech.nmfin.algoquant.data.cache.SequentialCache; +import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CSI300DataProvider; +import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CsvzipDataCacheProvider; +import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; +import tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock; +import tech.nmfin.algoquant.execution.datatype.product.stock.Stock; + +/** Local historical inputs for the CSI300 tutorial; never falls back to a web service. */ +final class LocalCSI300DataProvider extends CSI300DataProvider { + private static final Stock INDEX = new SimpleStock("000300.SS", Exchange.SS); + private final CsvzipDataCacheProvider prices; + private final NavigableMap index; + + LocalCSI300DataProvider(String constituents, String priceFolder, Path indexCsv, + LocalDateTimeInterval interval) throws IOException { + this(constituents, priceFolder, interval, readIndex(indexCsv)); + } + + private LocalCSI300DataProvider(String constituents, String priceFolder, + LocalDateTimeInterval interval, + NavigableMap index) throws IOException { + super(constituents, priceFolder, true, interval, false); + this.prices = new CsvzipDataCacheProvider(priceFolder); + this.index = index; + LocalDateTime requiredStart = Boolean.getBoolean("csi300.fullGrid") + ? interval.start().minusYears(1).minusWeeks(2) + : interval.start().minusWeeks(3); + if (index.firstKey().isAfter(requiredStart.plusDays(10)) + || index.lastKey().isBefore(interval.end().minusDays(10))) { + throw new IOException("CSI300 index CSV must cover calibration and simulation: " + + requiredStart + " to " + interval.end() + "; actual " + + index.firstKey() + " to " + index.lastKey()); + } + } + + static NavigableMap readIndex(Path file) throws IOException { + if (!Files.isRegularFile(file)) { + throw new IOException("Missing genuine CSI300 index history: " + file + + ". Supply -Dcsi300.indexCsv=/path/to/index.csv (date,close; ISO dates)." + + " Constituent membership is not index price data."); + } + List lines = Files.readAllLines(file, StandardCharsets.UTF_8); + if (lines.isEmpty() || !"date,close".equals(lines.get(0).replace("\uFEFF", "").trim())) { + throw new IOException("CSI300 index CSV requires header date,close: " + file); + } + NavigableMap result = new TreeMap<>(); + for (int i = 1; i < lines.size(); i++) { + if (lines.get(i).trim().isEmpty()) continue; + try { + String[] cells = lines.get(i).split(",", -1); + if (cells.length != 2) throw new IllegalArgumentException("expected two columns"); + LocalDateTime time = LocalDate.parse(cells[0].trim()).atTime(Exchange.SS.closeTime()); + double close = Double.parseDouble(cells[1].trim()); + if (!Double.isFinite(close) || close <= 0) throw new IllegalArgumentException("invalid close"); + if (!result.isEmpty() && !time.isAfter(result.lastKey())) { + throw new IllegalArgumentException("dates must be strictly increasing"); + } + result.put(time, close); + } catch (RuntimeException ex) { + throw new IOException("Invalid CSI300 index CSV row " + (i + 1) + " in " + file, ex); + } + } + if (result.size() < 2) throw new IOException("CSI300 index CSV needs at least two observations: " + file); + return result; + } + + double indexValueAt(LocalDateTime time) { + Map.Entry entry = index.floorEntry(time); + if (entry == null || entry.getKey().isBefore(time.minusDays(10))) { + throw new IllegalStateException("Missing or stale CSI300 index observation at " + time); + } + return entry.getValue(); + } + + @Override + public SequentialCache getCache(Stock product, LocalDateTimeInterval interval) { + if (!INDEX.equals(product)) return prices.getCache(product, interval); + BufferCache cache = new BufferCache<>(); + index.subMap(interval.start(), true, interval.end(), true).forEach(cache::add); + if (cache.size() < 2) throw new IllegalStateException("Insufficient CSI300 index history for " + interval); + return cache; + } +} diff --git a/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java b/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java new file mode 100644 index 0000000..65c594f --- /dev/null +++ b/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java @@ -0,0 +1,53 @@ +package tech.nmfin.algoquant.demo.model.portfoliooptimization.csi300; + +import dev.nm.misc.datastructure.time.LocalDateTimeInterval; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import org.junit.Test; +import static org.junit.Assert.*; +import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; +import tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock; + +public class LocalCSI300DataProviderTest { + private Path csv(String value) throws IOException { + Path p = Files.createTempFile("csi-index-test", ".csv"); + Files.write(p, value.getBytes(StandardCharsets.UTF_8)); + p.toFile().deleteOnExit(); + return p; + } + @Test public void readsPrices() throws Exception { + assertEquals(2, LocalCSI300DataProvider.readIndex(csv("date,close\n2009-12-01,3000\n2010-04-01,3300\n")).size()); + } + @Test(expected=IOException.class) public void rejectsEmptyHistory() throws Exception { + LocalCSI300DataProvider.readIndex(csv("date,close\n")); + } + @Test(expected=IOException.class) public void rejectsNonfinitePrice() throws Exception { + LocalCSI300DataProvider.readIndex(csv("date,close\n2010-01-01,NaN\n")); + } + @Test(expected=IOException.class) public void rejectsDuplicateDates() throws Exception { + LocalCSI300DataProvider.readIndex(csv("date,close\n2010-01-01,1\n2010-01-01,2\n")); + } + @Test(expected=IOException.class) public void rejectsWrongHeader() throws Exception { + LocalCSI300DataProvider.readIndex(csv("date,members\n2010-01-01,1\n")); + } + @Test public void missingIndexFailsBeforeConstituents() throws Exception { + try { + new LocalCSI300DataProvider("absent-members", "absent-prices", java.nio.file.Paths.get("absent-index.csv"), + LocalDateTimeInterval.of(LocalDateTime.of(2010,1,1,0,0), LocalDateTime.of(2010,4,1,0,0))); + fail(); + } catch (IOException ex) { assertTrue(ex.getMessage().contains("csi300.indexCsv")); } + } + @Test public void usesValueEqualityAndNeverLooksAhead() throws Exception { + Path members=csv("2009-12-01,000001.SZ,000002.SZ\n"); + LocalDateTimeInterval interval=LocalDateTimeInterval.of(LocalDateTime.of(2010,1,1,0,0),LocalDateTime.of(2010,4,1,0,0)); + LocalCSI300DataProvider provider=new LocalCSI300DataProvider(members.toString(), "absent-prices", + csv("date,close\n2009-12-01,3000\n2010-01-04,3100\n2010-01-05,3200\n2010-03-31,3300\n"),interval); + assertEquals(3100,provider.indexValueAt(LocalDateTime.of(2010,1,5,9,0)),0); + assertNotNull(provider.getCache(new SimpleStock("000300.SS",Exchange.SS),interval)); + try { provider.indexValueAt(LocalDateTime.of(2010,2,1,0,0)); fail(); } + catch(IllegalStateException expected) { assertTrue(expected.getMessage().contains("stale")); } + } +} From c2095b49a2ed9f3dc09e730675ed6769c5baca11 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:36:27 -0400 Subject: [PATCH 2/9] Use verified Eastmoney and Sina index feeds in CSI300 tutorial --- docs/csi300-tutorial.md | 37 ++++---- pom.xml | 2 +- .../csi300/CSI300SimulationDemo.java | 91 ++++++++++++++++--- 3 files changed, 97 insertions(+), 33 deletions(-) diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md index a3ef23b..af362b1 100644 --- a/docs/csi300-tutorial.md +++ b/docs/csi300-tutorial.md @@ -1,34 +1,31 @@ # CSI300 simulation tutorial -`CSI300SimulationDemo` demonstrates AlgoQuant's historical-universe and simulation APIs. Its outputs teach the workflow; they are not evidence of investment performance. +This tutorial demonstrates AlgoQuant's historical-universe and simulation APIs. Its outputs illustrate the workflow and do not establish investment performance. -## Run +## Index sources and verified coverage -Supply genuine CSI300 index observations in a UTF-8 CSV with exactly two columns: +The demo uses `EastmoneyIndexEODWebDownloader` by default. Select `SinaIndexEODWebDownloader` with `-Dcsi300.indexSource=sina`. Both are Java HTTPS clients provided by the matching AlgoQuant library change. There is no Yahoo index dependency, Python process, cookie or API token in this path. -```csv -date,close -2009-12-01,3500.00 -2009-12-02,3510.00 -``` +Before implementation, live requests returned **1,944** CSI300 sessions from **2010-01-04** through **2017-12-29** from each provider, with identical date sets and at most **0.005** index-point close difference. The 2008–2017 Eastmoney response contained 2,434 observations for pre-simulation calibration. Evidence: https://github.com/nmltd/algoquant/actions/runs/39127258230 . The library diagnostic workflow additionally checks the actual Java HTTP clients. + +The original demo dates remain 2010-01-01 to 2017-01-01 (exclusive). The separate coverage check includes all of calendar 2017. The demo downloads calibration history starting one year and two weeks before the simulation and validates range endpoints before running any strategy. + +Constituent membership and adjusted-price ZIPs still come from `test-data/china/constituent_index_300.csv` and `test-data/china/20070104-20171027/adjust_prices/`. Provider symbols are mapped from the canonical `000300.SS`; the index remains the first product, as required by `IndexAwareOptimizer`. The report benchmark uses these same CSI300 levels. Risk-free rate is explicitly assumed zero. Index close is a price-index level, not a total-return series. -Dates must be ascending and unique, prices positive and finite. Supply the whole calibration and simulation history, not just the two illustrative rows above. Constituent membership files (including `indices/hs300.xlsx`) are not index prices. The default simulation is January 2010 through January 2017; the baseline calibration requires three preceding weeks. The full optimizer grid needs one preceding year plus two weeks. Endpoint validation allows ten calendar days for holidays; benchmark lookups reject observations older than ten days. +## Run ```sh +mvn -Dexec.mainClass=tech.nmfin.algoquant.demo.model.portfoliooptimization.csi300.CSI300SimulationDemo compile exec:java + +# Alternative index source: mvn -Dexec.mainClass=tech.nmfin.algoquant.demo.model.portfoliooptimization.csi300.CSI300SimulationDemo \ - -Dcsi300.indexCsv=/data/csi300-index.csv \ - -Dcsi300.constituents=/data/constituent_index_300.csv \ - -Dcsi300.prices=/data/adjust_prices compile exec:java + -Dcsi300.indexSource=sina compile exec:java ``` -Constituent prices retain the existing per-stock ZIP format and filenames (for example `000001.SZ.csv.zip`). This provider never falls back to Yahoo. The report uses the supplied CSI300 index as its benchmark and explicitly assumes a zero risk-free rate. - -By default, two simulations run: buy-and-hold index and monthly equal-weight constituents. Add `-Dcsi300.fullGrid=true` for the original 990 optimizer simulations plus the baselines. The grid runs sequentially to bound concurrent simulation memory and keep result ordering reproducible; it remains an expensive research run. Progress identifies each simulation as it starts and finishes. - -CSV ZIP reports and the PNG chart are saved under `log/`. GUI windows are disabled by default; add `-Dcsi300.showCharts=true` to display the chart. +Override local constituent paths with `-Dcsi300.constituents=/data/membership.csv` and `-Dcsi300.prices=/data/adjust_prices`. The default performs two simulations: buy-and-hold index and monthly equal-weight constituents. Opt into the original **990 optimizer simulations** with `-Dcsi300.fullGrid=true`. The grid runs sequentially to bound memory and preserve ordering. Reports and a PNG are saved under `log/`; add `-Dcsi300.showCharts=true` to open a window. -## Repair validation +The two-argument Java constructor accepts a caller-supplied `SequentialCacheFactory` to replay a saved authentic index response or use another feed for reproducible tests. -Compiled both modified classes with Java 8 against AlgoQuant 3.3.0-SNAPSHOT. Seven regression tests passed, covering valid CSVs, malformed/empty data, duplicate dates, missing files, index product value equality, and past-only/stale benchmark lookup. An end-to-end run with explicitly synthetic fixture prices and two constituents completed both baseline simulations, exported CSV ZIPs and a PNG headlessly. Synthetic data is solely a software test, not a substitute historical benchmark. +## Dependency -The real 2010–2017 index history is not bundled. That historical run and the 990-optimizer grid have not been validated by this repair. Missing genuine history now produces an actionable error before loading constituents or starting simulations. +This branch targets the new `tech.nmfin:algoquant-dist:3.3.2-SNAPSHOT` library development version. The new downloaders are introduced by https://github.com/nmltd/algoquant/pull/7 and are not present in 3.3.1. Publish that library build before merging this consumer change. Local verification compiles against the changed library classes plus the existing combined runtime; it does not claim that this new development artifact is already published. diff --git a/pom.xml b/pom.xml index 136ae6b..3b63776 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ github-nm-packageshttps://maven.pkg.github.com/nmltd/maven-packages true - junitjunit4.13.2testcom.google.code.gsongson2.13.2tech.nmfinalgoquant-dist3.3.0-SNAPSHOT + junitjunit4.13.2testcom.google.code.gsongson2.13.2tech.nmfinalgoquant-dist3.3.2-SNAPSHOT org.apache.maven.pluginsmaven-compiler-plugin3.14.1 org.apache.maven.pluginsmaven-jar-plugin3.4.2 diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java index 0aa90e3..15dd544 100644 --- a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java @@ -28,7 +28,15 @@ import dev.nm.nmutils.plot.api.Chart; import dev.nm.solver.multivariate.constrained.general.penaltymethod.PenaltyMethodMinimizer; import java.io.File; -import java.nio.file.Paths; +import tech.nmfin.algoquant.data.cache.BufferCache; +import tech.nmfin.algoquant.data.cache.SequentialCacheFactory; +import tech.nmfin.algoquant.execution.datatype.StockEOD; +import tech.nmfin.algoquant.execution.datatype.product.ListedProduct; +import tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock; +import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; +import tech.nmfin.algoquant.data.historicaldata.china.index.EastmoneyIndexEODWebDownloader; +import tech.nmfin.algoquant.data.historicaldata.china.index.SinaIndexEODWebDownloader; +import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CSI300DataProvider; import java.io.IOException; import java.time.LocalDateTime; import java.time.Period; @@ -57,13 +65,14 @@ *

Using this example. * Configures CSI 300 constituents and local adjusted-price files for a portfolio simulation. * The constituent CSV and price directory are independent inputs and must cover compatible - * dates. Set {@code -Dcsi300.indexCsv=/path/to/index.csv} to a genuine CSI300 - * history with header {@code date,close} and ISO dates. Optional properties - * {@code csi300.constituents} and {@code csi300.prices} override local paths. + * dates. The index is downloaded from Eastmoney by default; select Sina with + * {@code -Dcsi300.indexSource=sina}. Optional properties {@code csi300.constituents} + * and {@code csi300.prices} override the local constituent-data paths. The index + * history is validated for calibration coverage before any simulation begins. * The default runs buy-and-hold and equal-weight baselines only; opt into the * expensive 990-optimizer sweep with {@code -Dcsi300.fullGrid=true}. - * No network data is downloaded. Reported risk-free rates are assumed zero. - * Set {@code csi300.showCharts=true} to open chart windows; PNG export is automatic. + * Reported risk-free rates are assumed zero. Set {@code csi300.showCharts=true} + * to open chart windows; PNG export is automatic. *

* *

Model and publication. @@ -94,20 +103,50 @@ public static void main(String[] args) throws IOException { } private final LocalDateTimeInterval interval; - private final LocalCSI300DataProvider dataProvider; + private final CSI300DataProvider dataProvider; + private static final SimpleStock INDEX = new SimpleStock("000300.SS", Exchange.SS); + private BufferCache indexHistory; public CSI300SimulationDemo(LocalDateTimeInterval interval) throws IOException { + this(interval, selectIndexSource()); + } + + private static SequentialCacheFactory selectIndexSource() { + String sourceName = System.getProperty("csi300.indexSource", "eastmoney"); + SequentialCacheFactory source; + if ("eastmoney".equalsIgnoreCase(sourceName)) { + source = new EastmoneyIndexEODWebDownloader(); + } else if ("sina".equalsIgnoreCase(sourceName)) { + source = new SinaIndexEODWebDownloader(); + } else { + throw new IllegalArgumentException("csi300.indexSource must be eastmoney or sina"); + } + LOGGER.info("CSI300 index source: {}; report risk-free rate assumed zero", sourceName); + return source; + } + + /** + * Uses a caller-supplied index feed, for reproducible offline tests or another provider. + * @param interval simulation dates + * @param source daily CSI300 index observations including calibration history + * @throws IOException if constituent membership cannot be read + */ + public CSI300SimulationDemo(LocalDateTimeInterval interval, + SequentialCacheFactory source) throws IOException { this.interval = interval; - this.dataProvider = new LocalCSI300DataProvider( + this.dataProvider = new CSI300DataProvider( System.getProperty("csi300.constituents", "./test-data/china/constituent_index_300.csv"), System.getProperty("csi300.prices", "./test-data/china/20070104-20171027/adjust_prices"), - Paths.get(System.getProperty("csi300.indexCsv", "./test-data/china/csi300-index.csv")), - interval + true, + LocalDateTimeInterval.of(interval.start().minusYears(1).minusWeeks(2), interval.end()), + false, + source ); } public int run() throws IOException { - LOGGER.info("CSI300 tutorial: local data only; full research grid enabled={}", + validateIndexHistory(); + LOGGER.info("CSI300 tutorial: full research grid enabled={}", Boolean.getBoolean("csi300.fullGrid")); Map results = new LinkedHashMap<>(); @@ -132,6 +171,33 @@ public int run() throws IOException { return results.size(); } + private void validateIndexHistory() throws IOException { + LocalDateTime requiredStart = interval.start().minusYears(1).minusWeeks(2); + indexHistory = BufferCache.fromSequentialCache(dataProvider.getCache(INDEX, + LocalDateTimeInterval.of(requiredStart, interval.end()))); + if (indexHistory.size() < 2 + || indexHistory.firstEntry().time().isAfter(requiredStart.plusDays(14)) + || indexHistory.lastEntry().time().isBefore(interval.end().minusDays(14))) { + throw new IOException("CSI300 index history does not cover calibration/simulation: " + + requiredStart + " to " + interval.end()); + } + for (TimedEntry observation : indexHistory) { + if (!Double.isFinite(observation.data()) || observation.data() <= 0) { + throw new IOException("Invalid CSI300 index value at " + observation.time()); + } + } + LOGGER.info("Loaded {} CSI300 index observations: {} to {}", indexHistory.size(), + indexHistory.firstEntry().time(), indexHistory.lastEntry().time()); + } + + private double indexValueAt(LocalDateTime time) { + TimedEntry observation = indexHistory.get(time); + if (observation == null || observation.time().isBefore(time.minusDays(14))) { + throw new IllegalStateException("Missing or stale CSI300 index observation at " + time); + } + return observation.data(); + } + private Map simulateBaseline(LocalDateTimeInterval interval) { Map results = new LinkedHashMap<>(); NamedParam param @@ -453,7 +519,7 @@ private Map simulateAlgorithm( false ); // Use the actual CSI300 benchmark rather than the simulation's SP500/Yahoo defaults. - sim.setBenchmarkLookupFactory("CSI300", ignored -> dataProvider::indexValueAt); + sim.setBenchmarkLookupFactory("CSI300", ignored -> this::indexValueAt); // Explicit tutorial assumption: zero risk-free rate (not a downloaded US bill yield). sim.setRiskFreeRateLookup(time -> 0.0); String algoName = param.value("algorithm"); @@ -536,3 +602,4 @@ private void saveResults( } + From e5af81762833487c2c71da5e74e0bb87361ff50e Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:39:18 -0400 Subject: [PATCH 3/9] Replay only index events in CSI300 buy-and-hold baseline --- .../csi300/CSI300SimulationDemo.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java index 15dd544..4190aef 100644 --- a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java @@ -33,6 +33,8 @@ import tech.nmfin.algoquant.execution.datatype.StockEOD; import tech.nmfin.algoquant.execution.datatype.product.ListedProduct; import tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock; +import tech.nmfin.algoquant.execution.datatype.product.stock.Stock; +import tech.nmfin.algoquant.model.strategy.rebalance.RebalancePortfolio.ProductChooser; import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; import tech.nmfin.algoquant.data.historicaldata.china.index.EastmoneyIndexEODWebDownloader; import tech.nmfin.algoquant.data.historicaldata.china.index.SinaIndexEODWebDownloader; @@ -163,7 +165,7 @@ public int run() throws IOException { Collection> set = profitCurves.values(); for (SequentialCache cache : set) { - LOGGER.info(print(cache, true)); + LOGGER.info("Profit curve contains {} observations", SequentialCache.size(cache)); } plotCurves(profitCurves); @@ -211,10 +213,7 @@ private Map simulateBaseline(LocalDateTimeInterval interval interval, new NamedParam(param) .set("algorithm", "buy-and-hold"), - new IndexAwareOptimizer( - (returns, r, s, i) -> new DenseVector(returns.nCols()), // put 0% capital in stocks - (a, b, c) -> 1. // put 100% capital in the index - ) + (returns, weights, symbols, dates) -> new DenseVector(new double[]{1.0}) ) ); @@ -508,10 +507,23 @@ private Map simulateAlgorithm( PortfolioOptimizationAlgorithm algorithm ) { try { + String algoName = param.value("algorithm"); + boolean indexOnly = "buy-and-hold".equals(algoName); + Collection products = indexOnly + ? Collections.singleton(INDEX) : dataProvider.allProducts(interval); + ProductChooser chooser = indexOnly + ? new ProductChooser() { + @Override public Set allProducts(LocalDateTimeInterval dates) { + return Collections.singleton(INDEX); + } + @Override public List chooseProducts(LocalDateTime time) { + return Collections.singletonList(INDEX); + } + } : dataProvider; OptimizationAlgorithmComparisonSimulation sim = new OptimizationAlgorithmComparisonSimulation( - dataProvider.allProducts(interval), - dataProvider, + products, + chooser, dataProvider, param.value("rebalance"), param.value("window"), @@ -522,7 +534,6 @@ private Map simulateAlgorithm( sim.setBenchmarkLookupFactory("CSI300", ignored -> this::indexValueAt); // Explicit tutorial assumption: zero risk-free rate (not a downloaded US bill yield). sim.setRiskFreeRateLookup(time -> 0.0); - String algoName = param.value("algorithm"); sim.addAlgorithm(algoName, algorithm); LOGGER.info("simulating {} for interval {}...", algoName, interval); return Collections.singletonMap( From 58bae3cd5ce222e08c282721a97c27c4e9905736 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:45:51 -0400 Subject: [PATCH 4/9] Update CSI300 validation notes for verified alternative providers --- docs/demo-repair-validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/demo-repair-validation.md b/docs/demo-repair-validation.md index df2238a..fd6ce13 100644 --- a/docs/demo-repair-validation.md +++ b/docs/demo-repair-validation.md @@ -25,6 +25,6 @@ The [targeted run](https://github.com/nmltd/algoquant/actions/runs/39122217560) Yahoo returned no historical observations for **2010–2017** under either `000300.SS` or `399300.SZ` when checked on 18 October 2026. The repository's `indices/hs300.xlsx` contains constituent membership, not official index prices. -CSI300SimulationDemo now validates the index history before its large research sweep. An empty or unavailable benchmark produces a clear data-prerequisite error, rather than endless retries, a null-pointer exception or an apparently successful flat index account. A genuine index history covering the calibration/simulation period is still required; an equal-weight constituent portfolio is not a substitute for that index. +CSI300SimulationDemo now uses the new Eastmoney Java downloader by default, with Sina selectable via csi300.indexSource=sina. Both providers were tested before implementation and each returned 1,944 identical CSI300 dates for calendar 2010–2017, with at most 0.005 index-point close difference. The actual Java HTTP clients passed the same live cross-check. The demo validates calibration coverage, uses CSI300 as its report benchmark, and defaults to two baselines. The index-only baseline replays index events only. See [the CSI300 tutorial](csi300-tutorial.md) for source selection, run instructions and the library-publication dependency. Genuine index levels are used; an equal-weight constituent portfolio is not substituted for the official index. Generated CSVs, logs, charts and plotted-data downloads belong in versioned Releases and the Wiki, not in the source tree. Older releases remain available for audit and reproduction. From 090166fb78f607b3153bebef64e0936efabc2f7f Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:48:27 -0400 Subject: [PATCH 5/9] Record successful real-history CSI300 replay validation --- docs/csi300-tutorial.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md index af362b1..9128804 100644 --- a/docs/csi300-tutorial.md +++ b/docs/csi300-tutorial.md @@ -29,3 +29,11 @@ The two-argument Java constructor accepts a caller-supplied `SequentialCacheFact ## Dependency This branch targets the new `tech.nmfin:algoquant-dist:3.3.2-SNAPSHOT` library development version. The new downloaders are introduced by https://github.com/nmltd/algoquant/pull/7 and are not present in 3.3.1. Publish that library build before merging this consumer change. Local verification compiles against the changed library classes plus the existing combined runtime; it does not claim that this new development artifact is already published. + +## Replay validation + +Both final downloaders compiled with Java 8 and the changed library classes against the released 3.3.1 combined runtime. Fourteen offline parser/provider tests passed. The live Java HTTP clients verified the entire 2010–2017 calendar range, including a successful empty holiday-only range, in https://github.com/nmltd/algoquant/actions/runs/39128421735 . + +The full demo's original 2010-01-01 to 2017-01-01 interval completed both baselines using the captured genuine Eastmoney response and the repository's historical constituent membership/adjusted-price files. It exported the measures, executions and trades CSV ZIPs and a headless PNG; both aggregate portfolio P/L and CSI300-correlation values were finite. A separate 2010-01-01 to 2011-01-01 Sina replay also completed both baselines and exports. These are authentic-response replay checks, separate from the live Java transport checks. The 990-optimizer research grid was not run. + +The index-only baseline now replays only index events, rather than every constituent event. Full equal-weight historical replay and its per-product reporting remain substantial work (the complete seven-year replay took roughly nine minutes in this test environment). From 17398c82ce15e41d425c1de9821d1e370904b98e Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:25:20 -0400 Subject: [PATCH 6/9] Reuse persisted CSI300 index histories across demo runs --- docs/csi300-tutorial.md | 8 ++------ .../csi300/CSI300SimulationDemo.java | 16 +++++----------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md index 9128804..dc807f7 100644 --- a/docs/csi300-tutorial.md +++ b/docs/csi300-tutorial.md @@ -30,10 +30,6 @@ The two-argument Java constructor accepts a caller-supplied `SequentialCacheFact This branch targets the new `tech.nmfin:algoquant-dist:3.3.2-SNAPSHOT` library development version. The new downloaders are introduced by https://github.com/nmltd/algoquant/pull/7 and are not present in 3.3.1. Publish that library build before merging this consumer change. Local verification compiles against the changed library classes plus the existing combined runtime; it does not claim that this new development artifact is already published. -## Replay validation +### Reusing index downloads -Both final downloaders compiled with Java 8 and the changed library classes against the released 3.3.1 combined runtime. Fourteen offline parser/provider tests passed. The live Java HTTP clients verified the entire 2010–2017 calendar range, including a successful empty holiday-only range, in https://github.com/nmltd/algoquant/actions/runs/39128421735 . - -The full demo's original 2010-01-01 to 2017-01-01 interval completed both baselines using the captured genuine Eastmoney response and the repository's historical constituent membership/adjusted-price files. It exported the measures, executions and trades CSV ZIPs and a headless PNG; both aggregate portfolio P/L and CSI300-correlation values were finite. A separate 2010-01-01 to 2011-01-01 Sina replay also completed both baselines and exports. These are authentic-response replay checks, separate from the live Java transport checks. The 990-optimizer research grid was not run. - -The index-only baseline now replays only index events, rather than every constituent event. Full equal-weight historical replay and its per-product reporting remain substantial work (the complete seven-year replay took roughly nine minutes in this test environment). +The demo uses `ChinaIndexEODCacheFactory` for both providers. Completed index histories are stored in `./cache/china-index/eastmoney` or `./cache/china-index/sina` and reused across runs for the same or contained date range. Override the root with `-Dcsi300.indexCache=/path/to/cache`. Wider requests fetch a new history; invalid cache files trigger a download. Empty/failed responses and unfinished current-day histories are not saved. Delete the provider cache directory when you want to pick up historical vendor revisions. The raw `*WebDownloader` classes remain network-only, just like Yahoo's web downloader; Yahoo's disk-first behavior is in its cache factory. diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java index 4190aef..c3789ce 100644 --- a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java @@ -36,8 +36,7 @@ import tech.nmfin.algoquant.execution.datatype.product.stock.Stock; import tech.nmfin.algoquant.model.strategy.rebalance.RebalancePortfolio.ProductChooser; import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; -import tech.nmfin.algoquant.data.historicaldata.china.index.EastmoneyIndexEODWebDownloader; -import tech.nmfin.algoquant.data.historicaldata.china.index.SinaIndexEODWebDownloader; +import tech.nmfin.algoquant.data.historicaldata.china.index.ChinaIndexEODCacheFactory; import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CSI300DataProvider; import java.io.IOException; import java.time.LocalDateTime; @@ -70,7 +69,8 @@ * dates. The index is downloaded from Eastmoney by default; select Sina with * {@code -Dcsi300.indexSource=sina}. Optional properties {@code csi300.constituents} * and {@code csi300.prices} override the local constituent-data paths. The index - * history is validated for calibration coverage before any simulation begins. + * history is cached on disk in {@code ./cache/china-index} (override with + * {@code csi300.indexCache}) and validated for calibration coverage before any simulation begins. * The default runs buy-and-hold and equal-weight baselines only; opt into the * expensive 990-optimizer sweep with {@code -Dcsi300.fullGrid=true}. * Reported risk-free rates are assumed zero. Set {@code csi300.showCharts=true} @@ -115,14 +115,8 @@ public CSI300SimulationDemo(LocalDateTimeInterval interval) throws IOException { private static SequentialCacheFactory selectIndexSource() { String sourceName = System.getProperty("csi300.indexSource", "eastmoney"); - SequentialCacheFactory source; - if ("eastmoney".equalsIgnoreCase(sourceName)) { - source = new EastmoneyIndexEODWebDownloader(); - } else if ("sina".equalsIgnoreCase(sourceName)) { - source = new SinaIndexEODWebDownloader(); - } else { - throw new IllegalArgumentException("csi300.indexSource must be eastmoney or sina"); - } + SequentialCacheFactory source = new ChinaIndexEODCacheFactory( + System.getProperty("csi300.indexCache", "./cache/china-index"), sourceName); LOGGER.info("CSI300 index source: {}; report risk-free rate assumed zero", sourceName); return source; } From 3f1506bc97247c869dbbc812788649daac51f0b8 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:39:36 -0400 Subject: [PATCH 7/9] Use shared EOD disk cache and general Sina/Eastmoney downloaders in CSI demo --- docs/csi300-tutorial.md | 4 ++-- .../csi300/CSI300SimulationDemo.java | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md index dc807f7..f8fddb3 100644 --- a/docs/csi300-tutorial.md +++ b/docs/csi300-tutorial.md @@ -4,7 +4,7 @@ This tutorial demonstrates AlgoQuant's historical-universe and simulation APIs. ## Index sources and verified coverage -The demo uses `EastmoneyIndexEODWebDownloader` by default. Select `SinaIndexEODWebDownloader` with `-Dcsi300.indexSource=sina`. Both are Java HTTPS clients provided by the matching AlgoQuant library change. There is no Yahoo index dependency, Python process, cookie or API token in this path. +The demo uses `EastmoneyEODWebDownloader` by default. Select `SinaEODWebDownloader` with `-Dcsi300.indexSource=sina`. Both are Java HTTPS clients provided by the matching AlgoQuant library change. There is no Yahoo index dependency, Python process, cookie or API token in this path. Before implementation, live requests returned **1,944** CSI300 sessions from **2010-01-04** through **2017-12-29** from each provider, with identical date sets and at most **0.005** index-point close difference. The 2008–2017 Eastmoney response contained 2,434 observations for pre-simulation calibration. Evidence: https://github.com/nmltd/algoquant/actions/runs/39127258230 . The library diagnostic workflow additionally checks the actual Java HTTP clients. @@ -32,4 +32,4 @@ This branch targets the new `tech.nmfin:algoquant-dist:3.3.2-SNAPSHOT` library d ### Reusing index downloads -The demo uses `ChinaIndexEODCacheFactory` for both providers. Completed index histories are stored in `./cache/china-index/eastmoney` or `./cache/china-index/sina` and reused across runs for the same or contained date range. Override the root with `-Dcsi300.indexCache=/path/to/cache`. Wider requests fetch a new history; invalid cache files trigger a download. Empty/failed responses and unfinished current-day histories are not saved. Delete the provider cache directory when you want to pick up historical vendor revisions. The raw `*WebDownloader` classes remain network-only, just like Yahoo's web downloader; Yahoo's disk-first behavior is in its cache factory. +The demo wraps either `EastmoneyEODWebDownloader` or `SinaEODWebDownloader` in the shared `DiskCachedEODFactory`. Completed index histories are stored in `./cache/china-index/eastmoney-unadjusted` or `./cache/china-index/sina-unadjusted` and reused across runs for the same or contained date range. Override the root with `-Dcsi300.indexCache=/path/to/cache`. Wider requests fetch a new history; invalid cache files trigger a download. Empty/failed responses and unfinished current-day histories are not saved. Delete the provider cache directory when you want to pick up historical vendor revisions. The raw `*WebDownloader` classes remain network-only, just like Yahoo's web downloader; Yahoo's disk-first behavior is in its cache factory. diff --git a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java index c3789ce..b7d3b62 100644 --- a/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java +++ b/src/main/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/CSI300SimulationDemo.java @@ -36,7 +36,10 @@ import tech.nmfin.algoquant.execution.datatype.product.stock.Stock; import tech.nmfin.algoquant.model.strategy.rebalance.RebalancePortfolio.ProductChooser; import tech.nmfin.algoquant.execution.datatype.product.stock.Exchange; -import tech.nmfin.algoquant.data.historicaldata.china.index.ChinaIndexEODCacheFactory; +import tech.nmfin.algoquant.data.historicaldata.AbstractEODWebDownloader; +import tech.nmfin.algoquant.data.historicaldata.DiskCachedEODFactory; +import tech.nmfin.algoquant.data.historicaldata.china.EastmoneyEODWebDownloader; +import tech.nmfin.algoquant.data.historicaldata.china.SinaEODWebDownloader; import tech.nmfin.algoquant.data.historicaldata.china.zqf.stock.csi300.CSI300DataProvider; import java.io.IOException; import java.time.LocalDateTime; @@ -115,8 +118,12 @@ public CSI300SimulationDemo(LocalDateTimeInterval interval) throws IOException { private static SequentialCacheFactory selectIndexSource() { String sourceName = System.getProperty("csi300.indexSource", "eastmoney"); - SequentialCacheFactory source = new ChinaIndexEODCacheFactory( - System.getProperty("csi300.indexCache", "./cache/china-index"), sourceName); + AbstractEODWebDownloader downloader; + if ("eastmoney".equalsIgnoreCase(sourceName)) downloader = new EastmoneyEODWebDownloader(); + else if ("sina".equalsIgnoreCase(sourceName)) downloader = new SinaEODWebDownloader(); + else throw new IllegalArgumentException("csi300.indexSource must be eastmoney or sina"); + SequentialCacheFactory source = new DiskCachedEODFactory( + System.getProperty("csi300.indexCache", "./cache/china-index"), downloader); LOGGER.info("CSI300 index source: {}; report risk-free rate assumed zero", sourceName); return source; } From cbda3909407c549744151359aa05767f12ca8be4 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:11:30 -0400 Subject: [PATCH 8/9] Initialize the shared test license before CSI provider tests regardless of test order --- .../csi300/LocalCSI300DataProviderTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java b/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java index 65c594f..e4a93cb 100644 --- a/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java +++ b/src/test/java/tech/nmfin/algoquant/demo/model/portfoliooptimization/csi300/LocalCSI300DataProviderTest.java @@ -12,6 +12,11 @@ import tech.nmfin.algoquant.execution.datatype.product.stock.SimpleStock; public class LocalCSI300DataProviderTest { + @org.junit.BeforeClass public static void initializeLibraryLicense() { + // This class may run before the web tests. Initialize the process-wide + // library license before constructing a provider or loading its logger. + tech.nmfin.algoquant.web.TestLicense.load(); + } private Path csv(String value) throws IOException { Path p = Files.createTempFile("csi-index-test", ".csv"); Files.write(p, value.getBytes(StandardCharsets.UTF_8)); From b4a7ff52fe635790bd5d72ad0f0c02e46bea7065 Mon Sep 17 00:00:00 2001 From: Haksun Li <65719379+haksunlinm@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:42:25 -0400 Subject: [PATCH 9/9] Use the AlgoQuant 3.3.2 release in the CSI300 tutorial --- docs/csi300-tutorial.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csi300-tutorial.md b/docs/csi300-tutorial.md index f8fddb3..0b29a29 100644 --- a/docs/csi300-tutorial.md +++ b/docs/csi300-tutorial.md @@ -28,7 +28,7 @@ The two-argument Java constructor accepts a caller-supplied `SequentialCacheFact ## Dependency -This branch targets the new `tech.nmfin:algoquant-dist:3.3.2-SNAPSHOT` library development version. The new downloaders are introduced by https://github.com/nmltd/algoquant/pull/7 and are not present in 3.3.1. Publish that library build before merging this consumer change. Local verification compiles against the changed library classes plus the existing combined runtime; it does not claim that this new development artifact is already published. +This tutorial uses `tech.nmfin:algoquant-dist:3.3.2`. The Sina/Eastmoney downloaders and shared disk cache are introduced in AlgoQuant 3.3.2 and are not present in 3.3.1. Publish the 3.3.2 library release before merging this consumer update. See the library release notes and `docs/eod-examples-compatibility.md` in the library repository for validation results and known demo limitations. ### Reusing index downloads diff --git a/pom.xml b/pom.xml index 3b63776..99c1ad6 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ github-nm-packageshttps://maven.pkg.github.com/nmltd/maven-packages true - junitjunit4.13.2testcom.google.code.gsongson2.13.2tech.nmfinalgoquant-dist3.3.2-SNAPSHOT + junitjunit4.13.2testcom.google.code.gsongson2.13.2tech.nmfinalgoquant-dist3.3.2 org.apache.maven.pluginsmaven-compiler-plugin3.14.1 org.apache.maven.pluginsmaven-jar-plugin3.4.2