stock-market-gen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # stock-market-gen
2
+
3
+ Generate realistic fake stock market data plus SVG charts and standalone HTML pages. Pure JS, zero dependencies, works in Node and the browser.
4
+
5
+ A modern, more capable replacement for the old `fake-stock-market-generator` package.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install stock-market-gen
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```js
16
+ import { generateStock, renderLineChart } from 'stock-market-gen';
17
+
18
+ const stock = generateStock({ bars: 100, interval: '1d', seed: 'demo' });
19
+ const svg = renderLineChart(stock);
20
+ ```
21
+
22
+ CommonJS works too:
23
+
24
+ ```js
25
+ const { generateStock, renderLineChart } = require('stock-market-gen');
26
+ ```
27
+
28
+ ## What you can generate
29
+
30
+ - single stocks or whole markets
31
+ - OHLCV bars (open, high, low, close, volume)
32
+ - pick any time interval — `"1m"`, `"5m"`, `"1h"`, `"1d"`, `"1w"`, `"1mo"`, `"1y"` or raw milliseconds
33
+ - override anything: symbol, name, sector, start price, drift, volatility, start date
34
+ - pass a `stocks` array to define each company yourself
35
+ - reproducible output via a seed
36
+
37
+ ## Chart types
38
+
39
+ - `line` — close price line
40
+ - `area` — line plus filled area
41
+ - `bar` — OHLC bar (tick-left, tick-right)
42
+ - `candlestick` — classic candles, green up / red down
43
+
44
+ All renderers return a complete SVG string. Save it to a file, drop it into HTML, or pipe it anywhere.
45
+
46
+ ## Customize anything
47
+
48
+ Every field is optional. Set the ones you care about, the rest are generated for you. The package only invents the symbol and the price series — `name`, `sector`, `startDate`, etc. are yours to provide.
49
+
50
+ ```js
51
+ import { generateStock } from 'stock-market-gen';
52
+
53
+ const stock = generateStock({
54
+ symbol: 'YOUR_SYMBOL',
55
+ name: 'Your Company Name',
56
+ sector: 'Your Sector',
57
+ startPrice: 180,
58
+ drift: 0.12, // +12% per year
59
+ volatility: 0.28, // 28% per year
60
+ bars: 60,
61
+ interval: '1w', // weekly bars
62
+ startDate: '2024-01-01',
63
+ seed: 'anything'
64
+ });
65
+ ```
66
+
67
+ Same idea for a market — pass a `stocks` array and pin whatever you want per company:
68
+
69
+ ```js
70
+ import { generateMarket } from 'stock-market-gen';
71
+
72
+ const market = generateMarket({
73
+ bars: 90,
74
+ interval: '1mo',
75
+ startDate: '2023-01-01',
76
+ seed: 'demo',
77
+ stocks: [
78
+ { symbol: 'YOUR1', name: 'Your Company 1', sector: 'Your Sector' },
79
+ { symbol: 'YOUR2', name: 'Your Company 2', sector: 'Your Sector', volatility: 0.45 },
80
+ { symbol: 'YOUR3' } // name and sector left empty
81
+ ]
82
+ });
83
+ ```
84
+
85
+ Or skip the array entirely and just get random tickers with no company info:
86
+
87
+ ```js
88
+ const market = generateMarket({ count: 8, bars: 90, seed: 'demo' });
89
+ ```
90
+
91
+ ## API
92
+
93
+ ### `generateStock(options) -> Stock`
94
+
95
+ | Option | Type | Default | Notes |
96
+ |---------------|----------------------------|------------------|-------|
97
+ | `symbol` | `string` | random 2-5 chars | |
98
+ | `name` | `string` | none (you supply it) | optional company name |
99
+ | `sector` | `string` | none (you supply it) | optional sector label |
100
+ | `startPrice` | `number` | 50–500 | |
101
+ | `drift` | `number` | random | annualised, e.g. `0.05` = +5%/yr |
102
+ | `volatility` | `number` | random | annualised, e.g. `0.3` = 30%/yr |
103
+ | `bars` | `number` | `100` | positive integer |
104
+ | `interval` | `number \| string` | `"1d"` | ms or `"1m"`/`"1h"`/`"1d"`/`"1w"`/`"1mo"`/`"1y"` |
105
+ | `startDate` | `Date \| number \| string` | `now - bars*interval` | first bar timestamp |
106
+ | `seed` | `number \| string` | random | reproducible output |
107
+
108
+ ### `generateMarket({ count, stocks, ...stockOptions }) -> Stock[]`
109
+
110
+ Generate several unique tickers in one call. Pass `count` for an auto-generated market (random symbols, no name or sector), or `stocks` to define each company yourself. Any other option is applied as a shared default.
111
+
112
+ ```js
113
+ // Auto-generated, 8 random tickers, all daily, same seed -> same market
114
+ const market = generateMarket({ count: 8, bars: 90, interval: '1d', seed: 'demo' });
115
+
116
+ // Hand-picked companies, with shared defaults
117
+ const market = generateMarket({
118
+ bars: 60,
119
+ interval: '1w',
120
+ seed: 'custom',
121
+ stocks: [
122
+ { symbol: 'YOUR1', name: 'Your Company 1', sector: 'Your Sector', startPrice: 180 },
123
+ { symbol: 'YOUR2', name: 'Your Company 2', sector: 'Your Sector', volatility: 0.45 },
124
+ { symbol: 'YOUR3' }
125
+ ]
126
+ });
127
+ ```
128
+
129
+ ### `renderChart(stock, type, options)`
130
+
131
+ `type` is `"line"`, `"area"`, `"bar"` or `"candlestick"`. Or call the dedicated renderers: `renderLineChart`, `renderAreaChart`, `renderBarChart`, `renderCandlestickChart`.
132
+
133
+ Chart options:
134
+
135
+ ```js
136
+ {
137
+ width: 800,
138
+ height: 400,
139
+ theme: 'light', // 'light' | 'dark'
140
+ title: 'AAPL — daily',
141
+ showGrid: true,
142
+ showAxes: true,
143
+ padding: { top: 24, right: 16, bottom: 36, left: 56 },
144
+ colors: { line: '#2563eb' } // override any single color
145
+ }
146
+ ```
147
+
148
+ ### `renderHtmlPage(marketOrStock, options) -> string`
149
+
150
+ Builds a self-contained HTML page with embedded SVG charts. Pass an array (whole market) or a single stock.
151
+
152
+ ```js
153
+ import { generateMarket, renderHtmlPage } from 'stock-market-gen';
154
+ import { writeFileSync } from 'node:fs';
155
+
156
+ const market = generateMarket({ count: 8, bars: 90, seed: 'page' });
157
+ writeFileSync('market.html', renderHtmlPage(market, { theme: 'dark', chartType: 'area' }));
158
+ ```
159
+
160
+ ## Examples
161
+
162
+ ```bash
163
+ npm run example # writes out/line.svg and out/candle.svg
164
+ npm run example:page # writes out/market.html
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,230 @@
1
+ // Price data generation.
2
+ //
3
+ // Uses Geometric Brownian Motion so prices stay positive and the variance
4
+ // scales with the current price — closer to how real markets behave than a
5
+ // plain random walk.
6
+ //
7
+ // S_{t+1} = S_t * exp((mu - sigma^2 / 2) * dt + sigma * sqrt(dt) * Z)
8
+ //
9
+ // Each bar then gets an open/high/low/close and a synthetic volume.
10
+
11
+ const { createRng } = require('./prng.cjs');
12
+ const { makeSymbol } = require('./symbols.cjs');
13
+ const { parseInterval } = require('./interval.cjs');
14
+
15
+ const DAY_MS = 24 * 60 * 60 * 1000;
16
+ const YEAR_MS = 365 * DAY_MS;
17
+
18
+ /**
19
+ * @typedef {Object} Bar
20
+ * @property {number} time - Unix epoch in milliseconds
21
+ * @property {string} date - ISO 8601 timestamp
22
+ * @property {number} open
23
+ * @property {number} high
24
+ * @property {number} low
25
+ * @property {number} close
26
+ * @property {number} volume
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} Stock
31
+ * @property {string} symbol
32
+ * @property {string} name
33
+ * @property {string} sector
34
+ * @property {number} startPrice
35
+ * @property {number} interval - milliseconds between bars
36
+ * @property {Bar[]} bars
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} GenerateStockOptions
41
+ * @property {string} [symbol] - Override the generated symbol
42
+ * @property {string} [name] - Override the generated company name
43
+ * @property {string} [sector] - Override the generated sector
44
+ * @property {number} [startPrice] - First bar's open price (default: 50-500)
45
+ * @property {number} [drift] - Annualised drift, e.g. 0.05 = +5%/year
46
+ * @property {number} [volatility] - Annualised volatility, e.g. 0.3 = 30%/year
47
+ * @property {number} [bars] - Number of bars to generate (default: 100)
48
+ * @property {number|string} [interval] - Bar size; ms or "1m"/"1h"/"1d" (default: "1d")
49
+ * @property {Date|number|string} [startDate] - First bar timestamp (default: now - bars*interval)
50
+ * @property {number|string} [seed] - Reproducible output
51
+ */
52
+
53
+ const DEFAULTS = {
54
+ bars: 100,
55
+ interval: '1d'
56
+ };
57
+
58
+ function round2(n) {
59
+ return Math.round(n * 100) / 100;
60
+ }
61
+
62
+ function typeOf(v) {
63
+ if (v === null) return 'null';
64
+ if (Array.isArray(v)) return 'array';
65
+ return typeof v;
66
+ }
67
+
68
+ /**
69
+ * Generate price data for a single stock.
70
+ * @param {GenerateStockOptions} [options]
71
+ * @returns {Stock}
72
+ */
73
+ function generateStock(options = {}) {
74
+ const opts = { ...DEFAULTS, ...options };
75
+ const rng = createRng(opts.seed);
76
+
77
+ if (!Number.isInteger(opts.bars) || opts.bars < 1) {
78
+ throw new Error(`"bars" must be a positive integer, got ${opts.bars}`);
79
+ }
80
+
81
+ // Optional fields: only validated when the caller actually provided them.
82
+ if (opts.symbol !== undefined && (typeof opts.symbol !== 'string' || opts.symbol.length === 0)) {
83
+ throw new Error(`"symbol" must be a non-empty string, got ${typeOf(opts.symbol)}`);
84
+ }
85
+ if (opts.name !== undefined && opts.name !== null && typeof opts.name !== 'string') {
86
+ throw new Error(`"name" must be a string, got ${typeOf(opts.name)}`);
87
+ }
88
+ if (opts.sector !== undefined && opts.sector !== null && typeof opts.sector !== 'string') {
89
+ throw new Error(`"sector" must be a string, got ${typeOf(opts.sector)}`);
90
+ }
91
+ if (opts.startPrice !== undefined && (!Number.isFinite(opts.startPrice) || opts.startPrice <= 0)) {
92
+ throw new Error(`"startPrice" must be a positive number, got ${opts.startPrice}`);
93
+ }
94
+ if (opts.drift !== undefined && !Number.isFinite(opts.drift)) {
95
+ throw new Error(`"drift" must be a finite number, got ${opts.drift}`);
96
+ }
97
+ if (opts.volatility !== undefined && (!Number.isFinite(opts.volatility) || opts.volatility < 0)) {
98
+ throw new Error(`"volatility" must be a non-negative number, got ${opts.volatility}`);
99
+ }
100
+ if (opts.startDate !== undefined) {
101
+ const t = new Date(opts.startDate).getTime();
102
+ if (!Number.isFinite(t)) {
103
+ throw new Error(`"startDate" is not a valid date: ${opts.startDate}`);
104
+ }
105
+ }
106
+
107
+ const intervalMs = parseInterval(opts.interval);
108
+
109
+ const symbol = opts.symbol ?? makeSymbol(rng);
110
+ const name = opts.name ?? null;
111
+ const sector = opts.sector ?? null;
112
+
113
+ const startPrice = opts.startPrice ?? round2(rng.next() * 450 + 50); // 50..500
114
+ // Drift roughly in [-15%, +25%] per year, vol in [10%, 60%] per year.
115
+ const drift = opts.drift ?? rng.next() * 0.4 - 0.15;
116
+ const volatility = opts.volatility ?? rng.next() * 0.5 + 0.1;
117
+
118
+ const startTime =
119
+ opts.startDate !== undefined
120
+ ? new Date(opts.startDate).getTime()
121
+ : Date.now() - opts.bars * intervalMs;
122
+
123
+ const dt = intervalMs / YEAR_MS;
124
+ const drift2 = (drift - (volatility * volatility) / 2) * dt;
125
+ const diffusion = volatility * Math.sqrt(dt);
126
+
127
+ const bars = new Array(opts.bars);
128
+ let price = startPrice;
129
+
130
+ for (let i = 0; i < opts.bars; i++) {
131
+ const open = price;
132
+
133
+ // Step the close using GBM
134
+ let close = open * Math.exp(drift2 + diffusion * rng.gauss());
135
+ if (close < 0.01) close = 0.01;
136
+
137
+ // High/low wick around the open-close range, scaled by volatility
138
+ const wick = Math.abs(open - close) + open * diffusion * (0.5 + rng.next());
139
+ const high = Math.max(open, close) + wick * rng.next();
140
+ const low = Math.max(0.01, Math.min(open, close) - wick * rng.next());
141
+
142
+ // Volume: log-normal-ish around 1M, with a kick on bigger moves
143
+ const move = Math.abs(close - open) / open;
144
+ const volume = Math.max(
145
+ 1,
146
+ Math.round((500_000 + rng.next() * 1_500_000) * (1 + move * 20))
147
+ );
148
+
149
+ const time = startTime + i * intervalMs;
150
+ bars[i] = {
151
+ time,
152
+ date: new Date(time).toISOString(),
153
+ open: round2(open),
154
+ high: round2(high),
155
+ low: round2(low),
156
+ close: round2(close),
157
+ volume
158
+ };
159
+
160
+ price = close;
161
+ }
162
+
163
+ return {
164
+ symbol,
165
+ name,
166
+ sector,
167
+ startPrice: round2(startPrice),
168
+ interval: intervalMs,
169
+ bars
170
+ };
171
+ }
172
+
173
+ /**
174
+ * @typedef {Object} GenerateMarketOptions
175
+ * @property {number} [count] - Number of stocks (default: 5)
176
+ * @property {GenerateStockOptions[]} [stocks] - Per-stock overrides. When
177
+ * provided, the market size matches this array's length and each entry can
178
+ * pin its own `symbol`, `name`, `sector`, `startPrice`, etc.
179
+ *
180
+ * Any other field is treated as a shared default applied to every stock.
181
+ */
182
+
183
+ /**
184
+ * Generate a whole market of stocks. Symbols stay unique within the market.
185
+ * @param {GenerateMarketOptions} [options]
186
+ * @returns {Stock[]}
187
+ */
188
+ function generateMarket(options = {}) {
189
+ const { count, stocks, seed, ...sharedOpts } = options;
190
+
191
+ // Decide how many stocks we're producing
192
+ let perStock;
193
+ if (Array.isArray(stocks)) {
194
+ if (stocks.length < 1) {
195
+ throw new Error('"stocks" array must contain at least one entry');
196
+ }
197
+ perStock = stocks;
198
+ } else {
199
+ const n = count ?? 5;
200
+ if (!Number.isInteger(n) || n < 1) {
201
+ throw new Error(`"count" must be a positive integer, got ${n}`);
202
+ }
203
+ perStock = new Array(n).fill(null).map(() => ({}));
204
+ }
205
+
206
+ // A master RNG drives per-stock seeds so the whole market is reproducible
207
+ // from a single user seed.
208
+ const master = createRng(seed);
209
+ const used = new Set();
210
+
211
+ // Reserve any symbols the caller pinned, so generated ones don't collide.
212
+ for (const entry of perStock) {
213
+ if (entry && typeof entry.symbol === 'string') used.add(entry.symbol);
214
+ }
215
+
216
+ return perStock.map((entry) => {
217
+ const stockSeed = master.int(0, 0xFFFFFFFF);
218
+ const merged = { ...sharedOpts, ...entry, seed: entry?.seed ?? stockSeed };
219
+
220
+ if (typeof merged.symbol !== 'string') {
221
+ // Build a deterministic rng just to pick a unique symbol up front
222
+ const rng = createRng(merged.seed);
223
+ merged.symbol = makeSymbol(rng, used);
224
+ }
225
+
226
+ return generateStock(merged);
227
+ });
228
+ }
229
+
230
+ module.exports = { generateStock, generateMarket };
package/dist/html.cjs ADDED
@@ -0,0 +1,154 @@
1
+ // Standalone HTML page renderer. Builds a self-contained document with the
2
+ // market summary and embedded SVG charts. No external assets, no JS.
3
+
4
+ const { renderChart } = require('./svg.cjs');
5
+
6
+ function escapeHtml(str) {
7
+ return String(str)
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
13
+ }
14
+
15
+ function changePct(stock) {
16
+ const first = stock.bars[0].open;
17
+ const last = stock.bars[stock.bars.length - 1].close;
18
+ return ((last - first) / first) * 100;
19
+ }
20
+
21
+ function buildStyles(theme) {
22
+ const dark = theme === 'dark';
23
+ return `
24
+ :root {
25
+ --bg: ${dark ? '#0b1220' : '#f9fafb'};
26
+ --card: ${dark ? '#111827' : '#ffffff'};
27
+ --border: ${dark ? '#1f2937' : '#e5e7eb'};
28
+ --text: ${dark ? '#e5e7eb' : '#111827'};
29
+ --muted: ${dark ? '#9ca3af' : '#6b7280'};
30
+ --up: ${dark ? '#22c55e' : '#16a34a'};
31
+ --down: ${dark ? '#ef4444' : '#dc2626'};
32
+ }
33
+ * { box-sizing: border-box; }
34
+ body {
35
+ margin: 0;
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
39
+ padding: 24px;
40
+ }
41
+ h1 { margin: 0 0 4px; font-size: 22px; }
42
+ p.lead { margin: 0 0 24px; color: var(--muted); }
43
+ .grid {
44
+ display: grid;
45
+ gap: 16px;
46
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
47
+ }
48
+ .card {
49
+ background: var(--card);
50
+ border: 1px solid var(--border);
51
+ border-radius: 10px;
52
+ padding: 16px;
53
+ }
54
+ .header {
55
+ display: flex;
56
+ align-items: baseline;
57
+ justify-content: space-between;
58
+ margin-bottom: 8px;
59
+ gap: 12px;
60
+ }
61
+ .symbol { font-weight: 700; font-size: 16px; }
62
+ .name { color: var(--muted); font-size: 13px; }
63
+ .price { font-variant-numeric: tabular-nums; font-weight: 600; }
64
+ .up { color: var(--up); }
65
+ .down { color: var(--down); }
66
+ .meta {
67
+ display: flex;
68
+ gap: 12px;
69
+ color: var(--muted);
70
+ font-size: 12px;
71
+ margin-top: 8px;
72
+ flex-wrap: wrap;
73
+ }
74
+ svg { display: block; width: 100%; height: auto; }
75
+ `;
76
+ }
77
+
78
+ /**
79
+ * @typedef {Object} HtmlPageOptions
80
+ * @property {string} [title] - Page title
81
+ * @property {'light'|'dark'} [theme] - Page + chart theme
82
+ * @property {'line'|'area'|'bar'|'candlestick'} [chartType] - Chart style
83
+ * @property {import('./svg.js').ChartOptions} [chartOptions] - Forwarded to renderer
84
+ */
85
+
86
+ /**
87
+ * Render a self-contained HTML page for a single stock or a market.
88
+ * @param {import('./generator.js').Stock | import('./generator.js').Stock[]} marketOrStock
89
+ * @param {HtmlPageOptions} [options]
90
+ * @returns {string} HTML document
91
+ */
92
+ function renderHtmlPage(marketOrStock, options = {}) {
93
+ const stocks = Array.isArray(marketOrStock) ? marketOrStock : [marketOrStock];
94
+ const theme = options.theme === 'dark' ? 'dark' : 'light';
95
+ const chartType = options.chartType || 'line';
96
+ const chartOptions = {
97
+ width: 520,
98
+ height: 260,
99
+ theme,
100
+ ...(options.chartOptions || {})
101
+ };
102
+ const title = options.title || 'Fake Stock Market';
103
+
104
+ const cards = stocks
105
+ .map((stock) => {
106
+ const last = stock.bars[stock.bars.length - 1].close;
107
+ const pct = changePct(stock);
108
+ const cls = pct >= 0 ? 'up' : 'down';
109
+ const sign = pct >= 0 ? '+' : '';
110
+ const chart = renderChart(stock, chartType, {
111
+ ...chartOptions,
112
+ title: '' // header in card already shows the symbol
113
+ });
114
+ return `
115
+ <div class="card">
116
+ <div class="header">
117
+ <div>
118
+ <div class="symbol">${escapeHtml(stock.symbol)}</div>
119
+ ${stock.name ? `<div class="name">${escapeHtml(stock.name)}</div>` : ''}
120
+ </div>
121
+ <div>
122
+ <span class="price">${last.toFixed(2)}</span>
123
+ <span class="${cls}">${sign}${pct.toFixed(2)}%</span>
124
+ </div>
125
+ </div>
126
+ ${chart}
127
+ <div class="meta">
128
+ ${stock.sector ? `<span>${escapeHtml(stock.sector)}</span>` : ''}
129
+ <span>${stock.bars.length} bars</span>
130
+ </div>
131
+ </div>
132
+ `;
133
+ })
134
+ .join('\n');
135
+
136
+ return `<!DOCTYPE html>
137
+ <html lang="en">
138
+ <head>
139
+ <meta charset="utf-8">
140
+ <meta name="viewport" content="width=device-width, initial-scale=1">
141
+ <title>${escapeHtml(title)}</title>
142
+ <style>${buildStyles(theme)}</style>
143
+ </head>
144
+ <body>
145
+ <h1>${escapeHtml(title)}</h1>
146
+ <p class="lead">Generated with stock-market-gen — fake data, no real market info.</p>
147
+ <div class="grid">
148
+ ${cards}
149
+ </div>
150
+ </body>
151
+ </html>`;
152
+ }
153
+
154
+ module.exports = { renderHtmlPage };
package/dist/index.cjs ADDED
@@ -0,0 +1,9 @@
1
+ // Public API.
2
+
3
+ const { generateStock, generateMarket } = require('./generator.cjs');
4
+ const { renderChart, renderLineChart, renderAreaChart, renderBarChart, renderCandlestickChart } = require('./svg.cjs');
5
+ const { renderHtmlPage } = require('./html.cjs');
6
+ const { createRng } = require('./prng.cjs');
7
+ const { parseInterval } = require('./interval.cjs');
8
+
9
+ module.exports = { generateStock, generateMarket, renderChart, renderLineChart, renderAreaChart, renderBarChart, renderCandlestickChart, renderHtmlPage, createRng, parseInterval };
@@ -0,0 +1,40 @@
1
+ // Interval parsing. Accepts a number of milliseconds or a shorthand string
2
+ // like "1m", "5m", "1h", "1d", "1w", "1y".
3
+
4
+ const UNIT_MS = {
5
+ m: 60 * 1000,
6
+ h: 60 * 60 * 1000,
7
+ d: 24 * 60 * 60 * 1000,
8
+ w: 7 * 24 * 60 * 60 * 1000,
9
+ mo: 30 * 24 * 60 * 60 * 1000,
10
+ y: 365 * 24 * 60 * 60 * 1000
11
+ };
12
+
13
+ /**
14
+ * Convert an interval (number in ms, or shorthand string) into milliseconds.
15
+ * @param {number|string} interval
16
+ * @returns {number}
17
+ */
18
+ function parseInterval(interval) {
19
+ if (typeof interval === 'number') {
20
+ if (!Number.isFinite(interval) || interval <= 0) {
21
+ throw new Error(`Invalid interval: ${interval}`);
22
+ }
23
+ return interval;
24
+ }
25
+ if (typeof interval !== 'string') {
26
+ throw new Error(`Invalid interval: ${interval}`);
27
+ }
28
+ // "mo" must be matched before "m" so "1mo" doesn't read as "1m" + "o".
29
+ const match = /^(\d+)\s*(mo|m|h|d|w|y)$/i.exec(interval.trim());
30
+ if (!match) {
31
+ throw new Error(
32
+ `Invalid interval string: "${interval}". Use e.g. "1m", "1h", "1d", "1w", "1mo", "1y".`
33
+ );
34
+ }
35
+ const n = Number(match[1]);
36
+ const unit = match[2].toLowerCase();
37
+ return n * UNIT_MS[unit];
38
+ }
39
+
40
+ module.exports = { parseInterval };
package/dist/prng.cjs ADDED
@@ -0,0 +1,73 @@
1
+ // Seeded pseudo-random number generator.
2
+ // Mulberry32 is small, fast and good enough for fake data.
3
+ // Reference: https://stackoverflow.com/a/47593316
4
+
5
+ /**
6
+ * Hash a string seed into a 32-bit integer.
7
+ * @param {string} str
8
+ * @returns {number}
9
+ */
10
+ function hashSeed(str) {
11
+ let h = 2166136261 >>> 0;
12
+ for (let i = 0; i < str.length; i++) {
13
+ h = Math.imul(h ^ str.charCodeAt(i), 16777619);
14
+ }
15
+ return h >>> 0;
16
+ }
17
+
18
+ /**
19
+ * Create a seeded random generator.
20
+ * @param {number|string|undefined} seed
21
+ * @returns {{ next: () => number, int: (min: number, max: number) => number, gauss: () => number, pick: <T>(arr: T[]) => T }}
22
+ */
23
+ function createRng(seed) {
24
+ let state;
25
+ if (seed === undefined || seed === null) {
26
+ state = (Math.random() * 2 ** 32) >>> 0;
27
+ } else if (typeof seed === 'number') {
28
+ state = seed >>> 0;
29
+ } else {
30
+ state = hashSeed(String(seed));
31
+ }
32
+
33
+ // Mulberry32 — uniform [0, 1)
34
+ function next() {
35
+ state = (state + 0x6D2B79F5) >>> 0;
36
+ let t = state;
37
+ t = Math.imul(t ^ (t >>> 15), t | 1);
38
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
39
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
40
+ }
41
+
42
+ // Integer in [min, max] inclusive
43
+ function int(min, max) {
44
+ return Math.floor(next() * (max - min + 1)) + min;
45
+ }
46
+
47
+ // Standard normal via Box-Muller. Cached pair to avoid wasting samples.
48
+ let cached = null;
49
+ function gauss() {
50
+ if (cached !== null) {
51
+ const v = cached;
52
+ cached = null;
53
+ return v;
54
+ }
55
+ let u1 = next();
56
+ let u2 = next();
57
+ // Avoid log(0)
58
+ if (u1 < 1e-12) u1 = 1e-12;
59
+ const mag = Math.sqrt(-2 * Math.log(u1));
60
+ const z0 = mag * Math.cos(2 * Math.PI * u2);
61
+ const z1 = mag * Math.sin(2 * Math.PI * u2);
62
+ cached = z1;
63
+ return z0;
64
+ }
65
+
66
+ function pick(arr) {
67
+ return arr[int(0, arr.length - 1)];
68
+ }
69
+
70
+ return { next, int, gauss, pick };
71
+ }
72
+
73
+ module.exports = { createRng };