dsh-plugin-show-me-data 0.1.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 +27 -0
- package/README.md +96 -0
- package/cordis.patch.yml +40 -0
- package/docs/01-product-effect.md +178 -0
- package/docs/02-architecture.md +275 -0
- package/docs/03-data-contracts.md +291 -0
- package/docs/04-sources.md +342 -0
- package/docs/05-ui-spec.md +167 -0
- package/docs/06-ai-layer.md +194 -0
- package/docs/07-implementation-plan.md +399 -0
- package/docs/08-test-plan.md +133 -0
- package/docs/09-packaging-install.md +249 -0
- package/docs/10-kickoff-prompt.md +94 -0
- package/docs/11-decisions.md +203 -0
- package/docs/12-runtime-verified.md +115 -0
- package/docs/13-acceptance.md +153 -0
- package/docs/14-progress.md +150 -0
- package/docs/15-publish.md +185 -0
- package/lib/app/ai-deterministic.js +327 -0
- package/lib/app/ai-validate.js +284 -0
- package/lib/app/ai.js +440 -0
- package/lib/app/health.js +77 -0
- package/lib/app/overview.js +349 -0
- package/lib/app/propose-indicator.js +122 -0
- package/lib/app/refresh.js +251 -0
- package/lib/app/series-view.js +195 -0
- package/lib/app/watchlist.js +102 -0
- package/lib/client.js +4322 -0
- package/lib/core/ai/prompts.js +213 -0
- package/lib/core/chart/axis.js +133 -0
- package/lib/core/chart/bar.js +58 -0
- package/lib/core/chart/candle.js +216 -0
- package/lib/core/chart/line.js +186 -0
- package/lib/core/chart/scale.js +132 -0
- package/lib/core/format.js +143 -0
- package/lib/core/indicators/catalog.js +1011 -0
- package/lib/core/indicators/resolve.js +196 -0
- package/lib/core/insight/digest.js +250 -0
- package/lib/core/insight/rank.js +115 -0
- package/lib/core/insight/related.js +90 -0
- package/lib/core/insight/rules.js +417 -0
- package/lib/core/stats/derive.js +123 -0
- package/lib/core/stats/series.js +465 -0
- package/lib/core/time/range.js +242 -0
- package/lib/core/types.js +478 -0
- package/lib/host/ai/discussion.js +559 -0
- package/lib/host/ai/dsh-llm-gateway.js +333 -0
- package/lib/host/config.js +194 -0
- package/lib/host/http/respond.js +165 -0
- package/lib/host/http/routes.js +689 -0
- package/lib/host/index.js +293 -0
- package/lib/host/infra/fs-repos.js +179 -0
- package/lib/host/infra/memory-fallback.js +64 -0
- package/lib/host/tools/define-tool.js +295 -0
- package/lib/host/tools/register.js +431 -0
- package/lib/host.js +7 -0
- package/lib/ports/clock.js +57 -0
- package/lib/ports/snapshot-repo.js +48 -0
- package/lib/sources/eastmoney-macro.js +197 -0
- package/lib/sources/eastmoney-quote.js +201 -0
- package/lib/sources/ecb.js +179 -0
- package/lib/sources/fred.js +207 -0
- package/lib/sources/http.js +136 -0
- package/lib/sources/ohlc.js +36 -0
- package/lib/sources/quote-cascade.js +177 -0
- package/lib/sources/registry.js +153 -0
- package/lib/sources/sina-cn.js +197 -0
- package/lib/sources/sina-us.js +187 -0
- package/lib/sources/tencent.js +158 -0
- package/lib/sources/us-treasury-rates.js +275 -0
- package/lib/sources/us-treasury.js +196 -0
- package/lib/sources/worldbank.js +170 -0
- package/package.json +69 -0
- package/src/app/ai-deterministic.js +327 -0
- package/src/app/ai-validate.js +284 -0
- package/src/app/ai.js +440 -0
- package/src/app/health.js +77 -0
- package/src/app/overview.js +349 -0
- package/src/app/propose-indicator.js +122 -0
- package/src/app/refresh.js +251 -0
- package/src/app/series-view.js +195 -0
- package/src/app/watchlist.js +102 -0
- package/src/client/api.js +323 -0
- package/src/client/components.js +1877 -0
- package/src/client/copy.js +368 -0
- package/src/client/index.js +169 -0
- package/src/client/store.js +219 -0
- package/src/core/ai/prompts.js +213 -0
- package/src/core/chart/axis.js +133 -0
- package/src/core/chart/bar.js +58 -0
- package/src/core/chart/candle.js +216 -0
- package/src/core/chart/line.js +186 -0
- package/src/core/chart/scale.js +132 -0
- package/src/core/format.js +143 -0
- package/src/core/indicators/catalog.js +1011 -0
- package/src/core/indicators/resolve.js +196 -0
- package/src/core/insight/digest.js +250 -0
- package/src/core/insight/rank.js +115 -0
- package/src/core/insight/related.js +90 -0
- package/src/core/insight/rules.js +417 -0
- package/src/core/stats/derive.js +123 -0
- package/src/core/stats/series.js +465 -0
- package/src/core/time/range.js +242 -0
- package/src/core/types.js +478 -0
- package/src/host/ai/discussion.js +559 -0
- package/src/host/ai/dsh-llm-gateway.js +333 -0
- package/src/host/config.js +194 -0
- package/src/host/http/respond.js +165 -0
- package/src/host/http/routes.js +689 -0
- package/src/host/index.js +293 -0
- package/src/host/infra/fs-repos.js +179 -0
- package/src/host/infra/memory-fallback.js +64 -0
- package/src/host/tools/define-tool.js +295 -0
- package/src/host/tools/register.js +431 -0
- package/src/ports/clock.js +57 -0
- package/src/ports/snapshot-repo.js +48 -0
- package/src/sources/eastmoney-macro.js +197 -0
- package/src/sources/eastmoney-quote.js +201 -0
- package/src/sources/ecb.js +179 -0
- package/src/sources/fred.js +207 -0
- package/src/sources/http.js +136 -0
- package/src/sources/ohlc.js +36 -0
- package/src/sources/quote-cascade.js +177 -0
- package/src/sources/registry.js +153 -0
- package/src/sources/sina-cn.js +197 -0
- package/src/sources/sina-us.js +187 -0
- package/src/sources/tencent.js +158 -0
- package/src/sources/us-treasury-rates.js +275 -0
- package/src/sources/us-treasury.js +196 -0
- package/src/sources/worldbank.js +170 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar helpers and range resolution — pure, no system time
|
|
3
|
+
* (docs/03 §1.6, docs/07 T1.1).
|
|
4
|
+
*
|
|
5
|
+
* Every function takes the reference "today" as an argument. The Clock port
|
|
6
|
+
* supplies it at the edges; nothing here reads the clock or the host timezone.
|
|
7
|
+
*
|
|
8
|
+
* @module core/time/range
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Range presets offered by the panel. */
|
|
12
|
+
export const RANGE_PRESETS = ['1M', '3M', '6M', 'YTD', '1Y', '3Y', '5Y', 'MAX', 'CUSTOM']
|
|
13
|
+
|
|
14
|
+
/** Milliseconds in one day, for date-only arithmetic on UTC midnights. */
|
|
15
|
+
const DAY_MS = 86400000
|
|
16
|
+
|
|
17
|
+
/** Days per month, indexed by month number (0-based). */
|
|
18
|
+
const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a 'YYYY-MM-DD' string into its numeric parts.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} date - calendar date.
|
|
24
|
+
* @returns {{ y: number, m: number, d: number }} parts (month 1-based).
|
|
25
|
+
*/
|
|
26
|
+
export function parseDate(date) {
|
|
27
|
+
const y = Number(date.slice(0, 4))
|
|
28
|
+
const m = Number(date.slice(5, 7))
|
|
29
|
+
const d = Number(date.slice(8, 10))
|
|
30
|
+
return { y, m, d }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format numeric parts back into 'YYYY-MM-DD'.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} y - year.
|
|
37
|
+
* @param {number} m - month (1-based).
|
|
38
|
+
* @param {number} d - day.
|
|
39
|
+
* @returns {string} calendar date.
|
|
40
|
+
*/
|
|
41
|
+
export function formatDate(y, m, d) {
|
|
42
|
+
return `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize out-of-range month/day parts into a real calendar date, using the
|
|
47
|
+
* 'Date' object purely as a proleptic-Gregorian calculator over UTC fields.
|
|
48
|
+
* This reads no clock (the epoch value is a constant), which the layering test
|
|
49
|
+
* enforces by forbidding bare 'new Date()' / 'Date.now()' in 'core/'.
|
|
50
|
+
*
|
|
51
|
+
* @param {number} y - year.
|
|
52
|
+
* @param {number} m - month (1-based, may overflow).
|
|
53
|
+
* @param {number} d - day of month (may overflow).
|
|
54
|
+
* @returns {string} normalized 'YYYY-MM-DD'.
|
|
55
|
+
*/
|
|
56
|
+
function normalizeDate(y, m, d) {
|
|
57
|
+
const at = new Date(0)
|
|
58
|
+
at.setUTCFullYear(y, m - 1, d)
|
|
59
|
+
return formatDate(at.getUTCFullYear(), at.getUTCMonth() + 1, at.getUTCDate())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a year is a leap year.
|
|
64
|
+
*
|
|
65
|
+
* @param {number} y - year.
|
|
66
|
+
* @returns {boolean} leap-year flag.
|
|
67
|
+
*/
|
|
68
|
+
export function isLeapYear(y) {
|
|
69
|
+
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Number of days in a month.
|
|
74
|
+
*
|
|
75
|
+
* @param {number} y - year.
|
|
76
|
+
* @param {number} m - month (1-based).
|
|
77
|
+
* @returns {number} day count.
|
|
78
|
+
*/
|
|
79
|
+
export function daysInMonth(y, m) {
|
|
80
|
+
if (m === 2 && isLeapYear(y)) return 29
|
|
81
|
+
return MONTH_DAYS[m - 1]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clamp a day to the last valid day of its month, so '2026-02-31' becomes
|
|
86
|
+
* '2026-02-28' rather than rolling into March (docs/07 T1.1: pick one
|
|
87
|
+
* convention; this is the frozen one).
|
|
88
|
+
*
|
|
89
|
+
* @param {number} y - year.
|
|
90
|
+
* @param {number} m - month (1-based).
|
|
91
|
+
* @param {number} d - day of month.
|
|
92
|
+
* @returns {string} valid calendar date.
|
|
93
|
+
*/
|
|
94
|
+
export function clampDay(y, m, d) {
|
|
95
|
+
const limit = daysInMonth(y, m)
|
|
96
|
+
return formatDate(y, m, Math.min(Math.max(d, 1), limit))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add whole months to a calendar date, clamping the day.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} date - base date 'YYYY-MM-DD'.
|
|
103
|
+
* @param {number} months - signed month delta.
|
|
104
|
+
* @returns {string} resulting date.
|
|
105
|
+
*/
|
|
106
|
+
export function addMonths(date, months) {
|
|
107
|
+
const { y, m, d } = parseDate(date)
|
|
108
|
+
const total = (y * 12 + (m - 1)) + months
|
|
109
|
+
const ny = Math.floor(total / 12)
|
|
110
|
+
const nm = (total % 12 + 12) % 12 + 1
|
|
111
|
+
return clampDay(ny, nm, d)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add whole days to a calendar date.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} date - base date 'YYYY-MM-DD'.
|
|
118
|
+
* @param {number} days - signed day delta.
|
|
119
|
+
* @returns {string} resulting date.
|
|
120
|
+
*/
|
|
121
|
+
export function addDays(date, days) {
|
|
122
|
+
const { y, m, d } = parseDate(date)
|
|
123
|
+
return normalizeDate(y, m, d + days)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whole days from 'a' to 'b' (negative when 'b' precedes 'a').
|
|
128
|
+
*
|
|
129
|
+
* @param {string} a - start date.
|
|
130
|
+
* @param {string} b - end date.
|
|
131
|
+
* @returns {number} day difference.
|
|
132
|
+
*/
|
|
133
|
+
export function daysBetween(a, b) {
|
|
134
|
+
const pa = parseDate(a)
|
|
135
|
+
const pb = parseDate(b)
|
|
136
|
+
const from = Date.UTC(pa.y, pa.m - 1, pa.d)
|
|
137
|
+
const to = Date.UTC(pb.y, pb.m - 1, pb.d)
|
|
138
|
+
return Math.round((to - from) / DAY_MS)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whole months from 'a' to 'b' (calendar-month difference).
|
|
143
|
+
*
|
|
144
|
+
* @param {string} a - start date.
|
|
145
|
+
* @param {string} b - end date.
|
|
146
|
+
* @returns {number} month difference.
|
|
147
|
+
*/
|
|
148
|
+
export function monthsBetween(a, b) {
|
|
149
|
+
const pa = parseDate(a)
|
|
150
|
+
const pb = parseDate(b)
|
|
151
|
+
return (pb.y * 12 + pb.m) - (pa.y * 12 + pa.m)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a range preset into a concrete 'Range'.
|
|
156
|
+
*
|
|
157
|
+
* Conventions frozen by tests:
|
|
158
|
+
* - '1M'/'3M'/'6M'/'1Y'/'3Y'/'5Y' subtract whole months and clamp the day
|
|
159
|
+
* ('1M' from '2026-03-31' → '2026-02-28').
|
|
160
|
+
* - 'YTD' starts at January 1st of the reference year.
|
|
161
|
+
* - 'MAX' and 'CUSTOM' are not resolvable from a preset alone: 'MAX' returns a
|
|
162
|
+
* sentinel 'from' far in the past, 'CUSTOM' requires explicit bounds.
|
|
163
|
+
*
|
|
164
|
+
* @param {'1M'|'3M'|'6M'|'YTD'|'1Y'|'3Y'|'5Y'|'MAX'|'CUSTOM'} preset - preset id.
|
|
165
|
+
* @param {string} today - reference calendar date.
|
|
166
|
+
* @param {{ from?: string, to?: string }} [custom] - explicit bounds for 'CUSTOM'.
|
|
167
|
+
* @returns {{ preset: string, from: string, to: string }} resolved range.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveRange(preset, today, custom = {}) {
|
|
170
|
+
if (!RANGE_PRESETS.includes(preset)) {
|
|
171
|
+
throw new RangeError(`unknown range preset: ${JSON.stringify(preset)}`)
|
|
172
|
+
}
|
|
173
|
+
if (preset === 'CUSTOM') {
|
|
174
|
+
const { from, to } = custom
|
|
175
|
+
if (typeof from !== 'string' || typeof to !== 'string') {
|
|
176
|
+
throw new RangeError('CUSTOM range requires both `from` and `to`')
|
|
177
|
+
}
|
|
178
|
+
if (from > to) throw new RangeError(`CUSTOM range is inverted: from ${from} > to ${to}`)
|
|
179
|
+
return { preset, from, to }
|
|
180
|
+
}
|
|
181
|
+
if (preset === 'MAX') return { preset, from: '1900-01-01', to: today }
|
|
182
|
+
if (preset === 'YTD') {
|
|
183
|
+
const { y } = parseDate(today)
|
|
184
|
+
return { preset, from: formatDate(y, 1, 1), to: today }
|
|
185
|
+
}
|
|
186
|
+
const months = { '1M': 1, '3M': 3, '6M': 6, '1Y': 12, '3Y': 36, '5Y': 60 }[preset]
|
|
187
|
+
return { preset, from: addMonths(today, -months), to: today }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether a calendar date falls inside a closed range.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} date - candidate date.
|
|
194
|
+
* @param {{ from: string, to: string }} range - closed bounds.
|
|
195
|
+
* @returns {boolean} membership.
|
|
196
|
+
*/
|
|
197
|
+
export function inRange(date, range) {
|
|
198
|
+
return date >= range.from && date <= range.to
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Keep only the points inside a closed range (docs/07 T1.2).
|
|
203
|
+
*
|
|
204
|
+
* @param {Array<{ t: string, v: number }>} points - ascending points.
|
|
205
|
+
* @param {{ from: string, to: string }} range - closed bounds.
|
|
206
|
+
* @returns {Array<{ t: string, v: number }>} filtered points.
|
|
207
|
+
*/
|
|
208
|
+
export function filterRange(points, range) {
|
|
209
|
+
return points.filter((point) => inRange(point.t, range))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The window length in months for a preset, used for comparison ranges.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} preset - range preset.
|
|
216
|
+
* @returns {number} months, or '0' for 'MAX'/'CUSTOM'.
|
|
217
|
+
*/
|
|
218
|
+
export function presetMonths(preset) {
|
|
219
|
+
return { '1M': 1, '3M': 3, '6M': 6, 'YTD': 12, '1Y': 12, '3Y': 36, '5Y': 60 }[preset] ?? 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The previous comparable window, used for "vs previous period" comparisons.
|
|
224
|
+
*
|
|
225
|
+
* @param {{ preset: string, from: string, to: string }} range - current range.
|
|
226
|
+
* @returns {{ from: string, to: string }} previous window (same length, ending the day before 'from').
|
|
227
|
+
*/
|
|
228
|
+
export function previousRange(range) {
|
|
229
|
+
if (range.preset === 'MAX' || range.preset === 'CUSTOM') {
|
|
230
|
+
const span = daysBetween(range.from, range.to)
|
|
231
|
+
const to = addDays(range.from, -1)
|
|
232
|
+
return { from: addDays(to, -span), to }
|
|
233
|
+
}
|
|
234
|
+
const to = addDays(range.from, -1)
|
|
235
|
+
if (range.preset === 'YTD') {
|
|
236
|
+
// "Year to date" compares against the same calendar window one year
|
|
237
|
+
// earlier (2026-01-01…2026-09-11 → 2025-01-01…2025-09-11).
|
|
238
|
+
return { from: addMonths(range.from, -12), to: addMonths(range.to, -12) }
|
|
239
|
+
}
|
|
240
|
+
const months = presetMonths(range.preset)
|
|
241
|
+
return { from: addMonths(to, -months), to }
|
|
242
|
+
}
|