@363045841yyt/klinechart-core 0.7.3 → 0.7.5
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/README.md +201 -201
- package/README.zh-CN.md +201 -201
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +1 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/engine/chart.d.ts +11 -19
- package/dist/engine/chart.d.ts.map +1 -1
- package/dist/engine/chart.js +92 -109
- package/dist/engine/chart.js.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.d.ts +1 -0
- package/dist/engine/renderers/Indicator/indicatorData.d.ts.map +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js +1 -1
- package/dist/engine/renderers/Indicator/indicatorData.js.map +1 -1
- package/dist/engine/renderers/webgl/candleSurface.js +47 -47
- package/dist/engine/subPaneManager.d.ts +4 -0
- package/dist/engine/subPaneManager.d.ts.map +1 -1
- package/dist/engine/subPaneManager.js +13 -0
- package/dist/engine/subPaneManager.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -2
- package/dist/version.js.map +1 -1
- package/package.json +129 -122
- package/src/__tests__/signal.test.ts +124 -124
- package/src/config/chartSettings.ts +66 -66
- package/src/controllers/__tests__/drawing.test.ts +214 -214
- package/src/controllers/__tests__/indicatorSelector.test.ts +481 -481
- package/src/controllers/__tests__/toolbar.test.ts +225 -225
- package/src/controllers/createChartController.ts +665 -665
- package/src/controllers/createDrawingController.ts +96 -96
- package/src/controllers/createIndicatorSelectorController.ts +307 -307
- package/src/controllers/createToolbarController.ts +146 -146
- package/src/controllers/index.ts +20 -19
- package/src/controllers/types.ts +284 -284
- package/src/engine/__tests__/chart.dpr.test.ts +401 -401
- package/src/engine/__tests__/paneRenderer.resize.test.ts +92 -92
- package/src/engine/chart-store.ts +121 -121
- package/src/engine/chart.d.ts +617 -617
- package/src/engine/chart.ts +2803 -2815
- package/src/engine/controller/__tests__/interaction.dpr.test.ts +259 -259
- package/src/engine/controller/interaction.ts +722 -722
- package/src/engine/controller/markerInteraction.ts +130 -130
- package/src/engine/controller/pinchTracker.ts +82 -82
- package/src/engine/controller/tooltipPosition.ts +48 -48
- package/src/engine/draw/__tests__/pixelAlign.spec.ts +176 -176
- package/src/engine/draw/pixelAlign.ts +259 -259
- package/src/engine/drawing/index.ts +655 -655
- package/src/engine/drawing/interaction.ts +842 -842
- package/src/engine/drawing/plugin.ts +343 -343
- package/src/engine/indicators/__tests__/__fixtures__/golden/atr.json +38 -38
- package/src/engine/indicators/__tests__/__fixtures__/golden/dema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/hma.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/index.ts +55 -55
- package/src/engine/indicators/__tests__/__fixtures__/golden/kama.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/tema.json +14 -14
- package/src/engine/indicators/__tests__/__fixtures__/golden/wma.json +40 -40
- package/src/engine/indicators/__tests__/__fixtures__/synthetic.ts +65 -65
- package/src/engine/indicators/__tests__/_propertyAssertions.ts +76 -76
- package/src/engine/indicators/__tests__/atr.test.ts +153 -153
- package/src/engine/indicators/__tests__/calculators.test.ts +614 -614
- package/src/engine/indicators/__tests__/cmf-mfi.test.ts +100 -100
- package/src/engine/indicators/__tests__/dema.test.ts +73 -73
- package/src/engine/indicators/__tests__/donchian.test.ts +70 -70
- package/src/engine/indicators/__tests__/hma.test.ts +73 -73
- package/src/engine/indicators/__tests__/ichimoku.test.ts +105 -105
- package/src/engine/indicators/__tests__/kama.test.ts +80 -80
- package/src/engine/indicators/__tests__/keltner.test.ts +65 -65
- package/src/engine/indicators/__tests__/pivot-fib.test.ts +110 -110
- package/src/engine/indicators/__tests__/roc.test.ts +68 -68
- package/src/engine/indicators/__tests__/sar.test.ts +86 -86
- package/src/engine/indicators/__tests__/scheduler.test.ts +831 -831
- package/src/engine/indicators/__tests__/soa.test.ts +533 -533
- package/src/engine/indicators/__tests__/structure.test.ts +110 -110
- package/src/engine/indicators/__tests__/supertrend.test.ts +65 -65
- package/src/engine/indicators/__tests__/tema.test.ts +68 -68
- package/src/engine/indicators/__tests__/trix.test.ts +70 -70
- package/src/engine/indicators/__tests__/volatility.test.ts +117 -117
- package/src/engine/indicators/__tests__/volume.test.ts +115 -115
- package/src/engine/indicators/__tests__/volumeProfile.test.ts +74 -74
- package/src/engine/indicators/__tests__/vwap.test.ts +69 -69
- package/src/engine/indicators/__tests__/wma.test.ts +112 -112
- package/src/engine/indicators/__tests__/zones.test.ts +95 -95
- package/src/engine/indicators/atrState.ts +27 -27
- package/src/engine/indicators/bollState.ts +51 -51
- package/src/engine/indicators/calculators.ts +2593 -2593
- package/src/engine/indicators/cciState.ts +25 -25
- package/src/engine/indicators/chaikinVolState.ts +32 -32
- package/src/engine/indicators/cmfState.ts +27 -27
- package/src/engine/indicators/demaState.ts +27 -27
- package/src/engine/indicators/donchianState.ts +43 -43
- package/src/engine/indicators/eneState.ts +43 -43
- package/src/engine/indicators/expmaState.ts +43 -43
- package/src/engine/indicators/fastkState.ts +25 -25
- package/src/engine/indicators/fibState.ts +41 -41
- package/src/engine/indicators/hmaState.ts +27 -27
- package/src/engine/indicators/hvState.ts +28 -28
- package/src/engine/indicators/ichimokuState.ts +70 -70
- package/src/engine/indicators/indicator.worker.ts +169 -169
- package/src/engine/indicators/indicatorDefinitionRegistry.ts +62 -62
- package/src/engine/indicators/indicatorMetadata.ts +110 -110
- package/src/engine/indicators/indicatorRegistry.ts +106 -106
- package/src/engine/indicators/indicatorRuntime.ts +1548 -1548
- package/src/engine/indicators/kamaState.ts +34 -34
- package/src/engine/indicators/keltnerState.ts +49 -49
- package/src/engine/indicators/kstState.ts +42 -42
- package/src/engine/indicators/maState.ts +36 -36
- package/src/engine/indicators/macdState.ts +76 -76
- package/src/engine/indicators/mfiState.ts +27 -27
- package/src/engine/indicators/momState.ts +25 -25
- package/src/engine/indicators/obvState.ts +25 -25
- package/src/engine/indicators/parkinsonState.ts +28 -28
- package/src/engine/indicators/pivotState.ts +51 -51
- package/src/engine/indicators/pvtState.ts +25 -25
- package/src/engine/indicators/rocState.ts +27 -27
- package/src/engine/indicators/rsiState.ts +65 -65
- package/src/engine/indicators/sarState.ts +41 -41
- package/src/engine/indicators/scheduler.ts +1205 -1205
- package/src/engine/indicators/soa.ts +352 -352
- package/src/engine/indicators/stateComposer.ts +1262 -1262
- package/src/engine/indicators/stochState.ts +26 -26
- package/src/engine/indicators/structureState.ts +69 -69
- package/src/engine/indicators/supertrendState.ts +37 -37
- package/src/engine/indicators/temaState.ts +27 -27
- package/src/engine/indicators/trixState.ts +35 -35
- package/src/engine/indicators/vmaState.ts +27 -27
- package/src/engine/indicators/volumeProfileState.ts +63 -63
- package/src/engine/indicators/vwapState.ts +29 -29
- package/src/engine/indicators/wmaState.ts +27 -27
- package/src/engine/indicators/wmsrState.ts +25 -25
- package/src/engine/indicators/workerProtocol.ts +613 -613
- package/src/engine/indicators/zonesState.ts +47 -47
- package/src/engine/layout/pane.ts +161 -161
- package/src/engine/marker/registry.ts +265 -265
- package/src/engine/paneRenderer.ts +169 -169
- package/src/engine/renderers/Indicator/atr.ts +237 -237
- package/src/engine/renderers/Indicator/boll.ts +317 -317
- package/src/engine/renderers/Indicator/cci.ts +275 -275
- package/src/engine/renderers/Indicator/chaikinVol.ts +138 -138
- package/src/engine/renderers/Indicator/cmf.ts +137 -137
- package/src/engine/renderers/Indicator/dema.ts +136 -136
- package/src/engine/renderers/Indicator/donchian.ts +137 -137
- package/src/engine/renderers/Indicator/ene.ts +271 -271
- package/src/engine/renderers/Indicator/expma.ts +197 -197
- package/src/engine/renderers/Indicator/fastk.ts +316 -316
- package/src/engine/renderers/Indicator/fib.ts +141 -141
- package/src/engine/renderers/Indicator/hma.ts +136 -136
- package/src/engine/renderers/Indicator/hv.ts +124 -124
- package/src/engine/renderers/Indicator/ichimoku.ts +181 -181
- package/src/engine/renderers/Indicator/index.ts +241 -241
- package/src/engine/renderers/Indicator/indicatorData.ts +650 -650
- package/src/engine/renderers/Indicator/kama.ts +136 -136
- package/src/engine/renderers/Indicator/keltner.ts +137 -137
- package/src/engine/renderers/Indicator/kst.ts +302 -302
- package/src/engine/renderers/Indicator/ma.ts +200 -200
- package/src/engine/renderers/Indicator/macd.ts +477 -477
- package/src/engine/renderers/Indicator/macdLegend.ts +141 -141
- package/src/engine/renderers/Indicator/mainIndicatorLegend.ts +272 -272
- package/src/engine/renderers/Indicator/mfi.ts +142 -142
- package/src/engine/renderers/Indicator/mom.ts +311 -311
- package/src/engine/renderers/Indicator/obv.ts +123 -123
- package/src/engine/renderers/Indicator/parkinson.ts +124 -124
- package/src/engine/renderers/Indicator/pivot.ts +131 -131
- package/src/engine/renderers/Indicator/pvt.ts +123 -123
- package/src/engine/renderers/Indicator/roc.ts +143 -143
- package/src/engine/renderers/Indicator/rsi.ts +390 -390
- package/src/engine/renderers/Indicator/sar.ts +113 -113
- package/src/engine/renderers/Indicator/scale/atr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/cci_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/fastk_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/indicator_scale.ts +204 -204
- package/src/engine/renderers/Indicator/scale/kst_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/macd_scale.ts +22 -22
- package/src/engine/renderers/Indicator/scale/mom_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/rsi_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/stoch_scale.ts +19 -19
- package/src/engine/renderers/Indicator/scale/volume_scale.ts +26 -26
- package/src/engine/renderers/Indicator/scale/wmsr_scale.ts +19 -19
- package/src/engine/renderers/Indicator/stoch.ts +359 -359
- package/src/engine/renderers/Indicator/structure.ts +126 -126
- package/src/engine/renderers/Indicator/subPaneConfig.ts +265 -265
- package/src/engine/renderers/Indicator/supertrend.ts +115 -115
- package/src/engine/renderers/Indicator/tema.ts +136 -136
- package/src/engine/renderers/Indicator/trix.ts +158 -158
- package/src/engine/renderers/Indicator/vma.ts +124 -124
- package/src/engine/renderers/Indicator/volumeProfile.ts +125 -125
- package/src/engine/renderers/Indicator/vwap.ts +123 -123
- package/src/engine/renderers/Indicator/wma.ts +136 -136
- package/src/engine/renderers/Indicator/wmsr.ts +328 -328
- package/src/engine/renderers/Indicator/zones.ts +104 -104
- package/src/engine/renderers/__tests__/boll.renderer.test.ts +314 -314
- package/src/engine/renderers/__tests__/ene.renderer.test.ts +305 -305
- package/src/engine/renderers/__tests__/expma.renderer.test.ts +279 -279
- package/src/engine/renderers/__tests__/ma.renderer.test.ts +426 -426
- package/src/engine/renderers/__tests__/mainIndicatorLegend.renderer.test.ts +502 -502
- package/src/engine/renderers/__tests__/yAxis.renderer.test.ts +173 -173
- package/src/engine/renderers/candle.ts +459 -459
- package/src/engine/renderers/crosshair.ts +69 -69
- package/src/engine/renderers/customMarkers.ts +162 -162
- package/src/engine/renderers/extremaMarkers.ts +246 -246
- package/src/engine/renderers/gridLines.ts +90 -90
- package/src/engine/renderers/lastPrice.ts +97 -97
- package/src/engine/renderers/paneTitle.ts +136 -136
- package/src/engine/renderers/subVolume.ts +236 -236
- package/src/engine/renderers/timeAxis.ts +121 -121
- package/src/engine/renderers/webgl/candleSurface.ts +955 -955
- package/src/engine/renderers/webgl/sharedWebGLSurface.ts +146 -146
- package/src/engine/renderers/yAxis.ts +105 -105
- package/src/engine/scale/__tests__/logFormula.spec.ts +148 -148
- package/src/engine/scale/logFormula.ts +130 -130
- package/src/engine/scale/price.ts +39 -39
- package/src/engine/scale/priceScale.ts +264 -264
- package/src/engine/subPaneManager.ts +442 -427
- package/src/engine/theme/colors.ts +642 -642
- package/src/engine/theme/fonts.ts +20 -20
- package/src/engine/utils/klineConfig.ts +49 -49
- package/src/engine/utils/tickCount.ts +11 -11
- package/src/engine/utils/tickPosition.ts +214 -214
- package/src/engine/utils/zoom.ts +83 -83
- package/src/engine/viewport/viewport.ts +67 -67
- package/src/index.ts +3 -3
- package/src/plugin/ConfigManager.ts +93 -93
- package/src/plugin/EventBus.ts +77 -77
- package/src/plugin/HookSystem.ts +106 -106
- package/src/plugin/PluginHost.ts +243 -243
- package/src/plugin/PluginRegistry.ts +92 -92
- package/src/plugin/StateStore.ts +73 -73
- package/src/plugin/index.ts +19 -19
- package/src/plugin/rendererPluginManager.ts +368 -368
- package/src/plugin/stateKeys.ts +8 -8
- package/src/plugin/types.ts +526 -526
- package/src/reactivity/index.ts +2 -2
- package/src/reactivity/signal.ts +119 -119
- package/src/semantic/controller.ts +251 -251
- package/src/semantic/drawShape.ts +260 -260
- package/src/semantic/index.ts +28 -28
- package/src/semantic/schema.json +256 -256
- package/src/semantic/types.ts +251 -251
- package/src/semantic/validator.ts +349 -349
- package/src/types/kLine.ts +13 -13
- package/src/types/price.ts +56 -56
- package/src/types/volumePrice.ts +33 -33
- package/src/utils/dateFormat.ts +208 -208
- package/src/utils/kLineDraw/axis.ts +562 -562
- package/src/utils/priceToY.ts +34 -34
- package/src/utils/volumePrice.ts +202 -202
- package/src/version.ts +1 -1
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { calcStructureData } from '../calculators'
|
|
3
|
-
import type { KLineData } from '@/types/price'
|
|
4
|
-
import {
|
|
5
|
-
empty,
|
|
6
|
-
pureUptrend,
|
|
7
|
-
pureDowntrend,
|
|
8
|
-
sideways,
|
|
9
|
-
} from './__fixtures__/synthetic'
|
|
10
|
-
|
|
11
|
-
// Build a fixture with a clear swing high, swing low, and breakout
|
|
12
|
-
function buildPyramidFixture(): KLineData[] {
|
|
13
|
-
const result: KLineData[] = []
|
|
14
|
-
let close = 100
|
|
15
|
-
const T0 = 1_700_000_000_000
|
|
16
|
-
// Up 0..9
|
|
17
|
-
for (let i = 0; i < 10; i++) {
|
|
18
|
-
close += 1
|
|
19
|
-
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
20
|
-
}
|
|
21
|
-
// Down 10..19 (creates swing high at ~bar 9)
|
|
22
|
-
for (let i = 10; i < 20; i++) {
|
|
23
|
-
close -= 1
|
|
24
|
-
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
25
|
-
}
|
|
26
|
-
// Up again to break swing high (creates BOS at the breakout bar)
|
|
27
|
-
for (let i = 20; i < 35; i++) {
|
|
28
|
-
close += 1
|
|
29
|
-
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
30
|
-
}
|
|
31
|
-
return result
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('calcStructureData', () => {
|
|
35
|
-
it('empty data → empty snapshot', () => {
|
|
36
|
-
const snap = calcStructureData(empty, 2, 2, 'close')
|
|
37
|
-
expect(snap.swings).toEqual([])
|
|
38
|
-
expect(snap.events).toEqual([])
|
|
39
|
-
expect(snap.trend).toBe('range')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('on pureUptrend (monotonic): no internal swings, no events', () => {
|
|
43
|
-
const snap = calcStructureData(pureUptrend, 2, 2, 'close')
|
|
44
|
-
// Monotonic uptrend has no local extrema inside the window
|
|
45
|
-
expect(snap.swings.length).toBe(0)
|
|
46
|
-
expect(snap.events.length).toBe(0)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('on pureDowntrend (monotonic): no internal swings', () => {
|
|
50
|
-
const snap = calcStructureData(pureDowntrend, 2, 2, 'close')
|
|
51
|
-
expect(snap.swings.length).toBe(0)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('on pyramid fixture: produces alternating swing highs and lows with valid labels', () => {
|
|
55
|
-
const data = buildPyramidFixture()
|
|
56
|
-
const snap = calcStructureData(data, 2, 2, 'close')
|
|
57
|
-
expect(snap.swings.length).toBeGreaterThan(0)
|
|
58
|
-
for (const s of snap.swings) {
|
|
59
|
-
expect(['HH', 'HL', 'LH', 'LL']).toContain(s.label)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('pyramid fixture: detects a BOS when uptrend breaks prior swing high', () => {
|
|
64
|
-
const data = buildPyramidFixture()
|
|
65
|
-
const snap = calcStructureData(data, 2, 2, 'close')
|
|
66
|
-
// Should have at least one swing
|
|
67
|
-
expect(snap.swings.length).toBeGreaterThan(0)
|
|
68
|
-
// Should detect a BOS or CHOCH event during the second uptrend
|
|
69
|
-
const upEvents = snap.events.filter((e) => e.direction === 'up')
|
|
70
|
-
expect(upEvents.length).toBeGreaterThanOrEqual(1)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('breakoutSource = wick vs close produces (possibly) different event timing', () => {
|
|
74
|
-
const data = buildPyramidFixture()
|
|
75
|
-
const closeSnap = calcStructureData(data, 2, 2, 'close')
|
|
76
|
-
const wickSnap = calcStructureData(data, 2, 2, 'wick')
|
|
77
|
-
// Both should produce at least one event; their indices may differ
|
|
78
|
-
expect(closeSnap.events.length).toBeGreaterThanOrEqual(0)
|
|
79
|
-
expect(wickSnap.events.length).toBeGreaterThanOrEqual(0)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('swing.confirmed is false within rightWindow of latest bar', () => {
|
|
83
|
-
const data = buildPyramidFixture()
|
|
84
|
-
const right = 2
|
|
85
|
-
const snap = calcStructureData(data, 2, right, 'close')
|
|
86
|
-
for (const s of snap.swings) {
|
|
87
|
-
if (s.index + right >= data.length) {
|
|
88
|
-
expect(s.confirmed).toBe(false)
|
|
89
|
-
} else {
|
|
90
|
-
expect(s.confirmed).toBe(true)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('extensional consistency on pyramid fixture', () => {
|
|
96
|
-
const data = buildPyramidFixture()
|
|
97
|
-
const full = calcStructureData(data, 2, 2, 'close')
|
|
98
|
-
for (let n = 15; n < data.length; n++) {
|
|
99
|
-
const partial = calcStructureData(data.slice(0, n), 2, 2, 'close')
|
|
100
|
-
// Confirmed swings in the prefix should match the full version
|
|
101
|
-
const fullConfirmedInPrefix = full.swings.filter((s) => s.confirmed && s.index + 2 < n)
|
|
102
|
-
const partialConfirmed = partial.swings.filter((s) => s.confirmed)
|
|
103
|
-
for (const fs of fullConfirmedInPrefix) {
|
|
104
|
-
const match = partialConfirmed.find((s) => s.index === fs.index && s.kind === fs.kind)
|
|
105
|
-
expect(match).toBeDefined()
|
|
106
|
-
if (match) expect(match.price).toBeCloseTo(fs.price, 9)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { calcStructureData } from '../calculators'
|
|
3
|
+
import type { KLineData } from '@/types/price'
|
|
4
|
+
import {
|
|
5
|
+
empty,
|
|
6
|
+
pureUptrend,
|
|
7
|
+
pureDowntrend,
|
|
8
|
+
sideways,
|
|
9
|
+
} from './__fixtures__/synthetic'
|
|
10
|
+
|
|
11
|
+
// Build a fixture with a clear swing high, swing low, and breakout
|
|
12
|
+
function buildPyramidFixture(): KLineData[] {
|
|
13
|
+
const result: KLineData[] = []
|
|
14
|
+
let close = 100
|
|
15
|
+
const T0 = 1_700_000_000_000
|
|
16
|
+
// Up 0..9
|
|
17
|
+
for (let i = 0; i < 10; i++) {
|
|
18
|
+
close += 1
|
|
19
|
+
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
20
|
+
}
|
|
21
|
+
// Down 10..19 (creates swing high at ~bar 9)
|
|
22
|
+
for (let i = 10; i < 20; i++) {
|
|
23
|
+
close -= 1
|
|
24
|
+
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
25
|
+
}
|
|
26
|
+
// Up again to break swing high (creates BOS at the breakout bar)
|
|
27
|
+
for (let i = 20; i < 35; i++) {
|
|
28
|
+
close += 1
|
|
29
|
+
result.push({ timestamp: T0 + i * 60000, open: close, high: close + 0.5, low: close - 0.5, close, volume: 100 })
|
|
30
|
+
}
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('calcStructureData', () => {
|
|
35
|
+
it('empty data → empty snapshot', () => {
|
|
36
|
+
const snap = calcStructureData(empty, 2, 2, 'close')
|
|
37
|
+
expect(snap.swings).toEqual([])
|
|
38
|
+
expect(snap.events).toEqual([])
|
|
39
|
+
expect(snap.trend).toBe('range')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('on pureUptrend (monotonic): no internal swings, no events', () => {
|
|
43
|
+
const snap = calcStructureData(pureUptrend, 2, 2, 'close')
|
|
44
|
+
// Monotonic uptrend has no local extrema inside the window
|
|
45
|
+
expect(snap.swings.length).toBe(0)
|
|
46
|
+
expect(snap.events.length).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('on pureDowntrend (monotonic): no internal swings', () => {
|
|
50
|
+
const snap = calcStructureData(pureDowntrend, 2, 2, 'close')
|
|
51
|
+
expect(snap.swings.length).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('on pyramid fixture: produces alternating swing highs and lows with valid labels', () => {
|
|
55
|
+
const data = buildPyramidFixture()
|
|
56
|
+
const snap = calcStructureData(data, 2, 2, 'close')
|
|
57
|
+
expect(snap.swings.length).toBeGreaterThan(0)
|
|
58
|
+
for (const s of snap.swings) {
|
|
59
|
+
expect(['HH', 'HL', 'LH', 'LL']).toContain(s.label)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('pyramid fixture: detects a BOS when uptrend breaks prior swing high', () => {
|
|
64
|
+
const data = buildPyramidFixture()
|
|
65
|
+
const snap = calcStructureData(data, 2, 2, 'close')
|
|
66
|
+
// Should have at least one swing
|
|
67
|
+
expect(snap.swings.length).toBeGreaterThan(0)
|
|
68
|
+
// Should detect a BOS or CHOCH event during the second uptrend
|
|
69
|
+
const upEvents = snap.events.filter((e) => e.direction === 'up')
|
|
70
|
+
expect(upEvents.length).toBeGreaterThanOrEqual(1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('breakoutSource = wick vs close produces (possibly) different event timing', () => {
|
|
74
|
+
const data = buildPyramidFixture()
|
|
75
|
+
const closeSnap = calcStructureData(data, 2, 2, 'close')
|
|
76
|
+
const wickSnap = calcStructureData(data, 2, 2, 'wick')
|
|
77
|
+
// Both should produce at least one event; their indices may differ
|
|
78
|
+
expect(closeSnap.events.length).toBeGreaterThanOrEqual(0)
|
|
79
|
+
expect(wickSnap.events.length).toBeGreaterThanOrEqual(0)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('swing.confirmed is false within rightWindow of latest bar', () => {
|
|
83
|
+
const data = buildPyramidFixture()
|
|
84
|
+
const right = 2
|
|
85
|
+
const snap = calcStructureData(data, 2, right, 'close')
|
|
86
|
+
for (const s of snap.swings) {
|
|
87
|
+
if (s.index + right >= data.length) {
|
|
88
|
+
expect(s.confirmed).toBe(false)
|
|
89
|
+
} else {
|
|
90
|
+
expect(s.confirmed).toBe(true)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('extensional consistency on pyramid fixture', () => {
|
|
96
|
+
const data = buildPyramidFixture()
|
|
97
|
+
const full = calcStructureData(data, 2, 2, 'close')
|
|
98
|
+
for (let n = 15; n < data.length; n++) {
|
|
99
|
+
const partial = calcStructureData(data.slice(0, n), 2, 2, 'close')
|
|
100
|
+
// Confirmed swings in the prefix should match the full version
|
|
101
|
+
const fullConfirmedInPrefix = full.swings.filter((s) => s.confirmed && s.index + 2 < n)
|
|
102
|
+
const partialConfirmed = partial.swings.filter((s) => s.confirmed)
|
|
103
|
+
for (const fs of fullConfirmedInPrefix) {
|
|
104
|
+
const match = partialConfirmed.find((s) => s.index === fs.index && s.kind === fs.kind)
|
|
105
|
+
expect(match).toBeDefined()
|
|
106
|
+
if (match) expect(match.price).toBeCloseTo(fs.price, 9)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { calcSuperTrendData } from '../calculators'
|
|
3
|
-
import {
|
|
4
|
-
empty,
|
|
5
|
-
pureUptrend,
|
|
6
|
-
pureDowntrend,
|
|
7
|
-
spikeAtBar19,
|
|
8
|
-
gapUp,
|
|
9
|
-
} from './__fixtures__/synthetic'
|
|
10
|
-
|
|
11
|
-
describe('calcSuperTrendData', () => {
|
|
12
|
-
it('empty returns empty', () => {
|
|
13
|
-
expect(calcSuperTrendData(empty, 10, 3)).toEqual([])
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('atrPeriod <= 0 returns all undefined', () => {
|
|
17
|
-
const out = calcSuperTrendData(pureUptrend, 0, 3)
|
|
18
|
-
for (const v of out) expect(v).toBeUndefined()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('multiplier <= 0 returns all undefined', () => {
|
|
22
|
-
const out = calcSuperTrendData(pureUptrend, 10, 0)
|
|
23
|
-
for (const v of out) expect(v).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('on pureUptrend SuperTrend trend stays up after warm-up', () => {
|
|
27
|
-
const out = calcSuperTrendData(pureUptrend, 10, 3)
|
|
28
|
-
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
29
|
-
expect(valid.length).toBeGreaterThan(5)
|
|
30
|
-
// All trends should be up since price keeps rising
|
|
31
|
-
for (const p of valid) {
|
|
32
|
-
expect(p.trend).toBe('up')
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('on pureDowntrend SuperTrend trend stays down', () => {
|
|
37
|
-
const out = calcSuperTrendData(pureDowntrend, 10, 3)
|
|
38
|
-
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
39
|
-
// Most of post-warm-up should be down
|
|
40
|
-
const downCount = valid.filter((p) => p.trend === 'down').length
|
|
41
|
-
expect(downCount).toBeGreaterThan(valid.length / 2)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('extensional consistency', () => {
|
|
45
|
-
const full = calcSuperTrendData(gapUp, 10, 3)
|
|
46
|
-
for (let n = 12; n < gapUp.length; n++) {
|
|
47
|
-
const partial = calcSuperTrendData(gapUp.slice(0, n), 10, 3)
|
|
48
|
-
for (let i = 0; i < n; i++) {
|
|
49
|
-
const f = full[i]
|
|
50
|
-
const p = partial[i]
|
|
51
|
-
if (f && p) {
|
|
52
|
-
expect(p.value).toBeCloseTo(f.value, 9)
|
|
53
|
-
expect(p.trend).toBe(f.trend)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('trend flips on spike fixture', () => {
|
|
60
|
-
const out = calcSuperTrendData(spikeAtBar19, 10, 3)
|
|
61
|
-
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
62
|
-
const trends = new Set(valid.map((p) => p.trend))
|
|
63
|
-
expect(trends.size).toBeGreaterThanOrEqual(1)
|
|
64
|
-
})
|
|
65
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { calcSuperTrendData } from '../calculators'
|
|
3
|
+
import {
|
|
4
|
+
empty,
|
|
5
|
+
pureUptrend,
|
|
6
|
+
pureDowntrend,
|
|
7
|
+
spikeAtBar19,
|
|
8
|
+
gapUp,
|
|
9
|
+
} from './__fixtures__/synthetic'
|
|
10
|
+
|
|
11
|
+
describe('calcSuperTrendData', () => {
|
|
12
|
+
it('empty returns empty', () => {
|
|
13
|
+
expect(calcSuperTrendData(empty, 10, 3)).toEqual([])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('atrPeriod <= 0 returns all undefined', () => {
|
|
17
|
+
const out = calcSuperTrendData(pureUptrend, 0, 3)
|
|
18
|
+
for (const v of out) expect(v).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('multiplier <= 0 returns all undefined', () => {
|
|
22
|
+
const out = calcSuperTrendData(pureUptrend, 10, 0)
|
|
23
|
+
for (const v of out) expect(v).toBeUndefined()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('on pureUptrend SuperTrend trend stays up after warm-up', () => {
|
|
27
|
+
const out = calcSuperTrendData(pureUptrend, 10, 3)
|
|
28
|
+
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
29
|
+
expect(valid.length).toBeGreaterThan(5)
|
|
30
|
+
// All trends should be up since price keeps rising
|
|
31
|
+
for (const p of valid) {
|
|
32
|
+
expect(p.trend).toBe('up')
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('on pureDowntrend SuperTrend trend stays down', () => {
|
|
37
|
+
const out = calcSuperTrendData(pureDowntrend, 10, 3)
|
|
38
|
+
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
39
|
+
// Most of post-warm-up should be down
|
|
40
|
+
const downCount = valid.filter((p) => p.trend === 'down').length
|
|
41
|
+
expect(downCount).toBeGreaterThan(valid.length / 2)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('extensional consistency', () => {
|
|
45
|
+
const full = calcSuperTrendData(gapUp, 10, 3)
|
|
46
|
+
for (let n = 12; n < gapUp.length; n++) {
|
|
47
|
+
const partial = calcSuperTrendData(gapUp.slice(0, n), 10, 3)
|
|
48
|
+
for (let i = 0; i < n; i++) {
|
|
49
|
+
const f = full[i]
|
|
50
|
+
const p = partial[i]
|
|
51
|
+
if (f && p) {
|
|
52
|
+
expect(p.value).toBeCloseTo(f.value, 9)
|
|
53
|
+
expect(p.trend).toBe(f.trend)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('trend flips on spike fixture', () => {
|
|
60
|
+
const out = calcSuperTrendData(spikeAtBar19, 10, 3)
|
|
61
|
+
const valid = out.filter((p): p is { value: number; trend: 'up' | 'down' } => p !== undefined)
|
|
62
|
+
const trends = new Set(valid.map((p) => p.trend))
|
|
63
|
+
expect(trends.size).toBeGreaterThanOrEqual(1)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { calcTEMAData } from '../calculators'
|
|
3
|
-
import {
|
|
4
|
-
empty,
|
|
5
|
-
singleBar,
|
|
6
|
-
constantPrice,
|
|
7
|
-
pureUptrend,
|
|
8
|
-
pureDowntrend,
|
|
9
|
-
sideways,
|
|
10
|
-
spikeAtBar19,
|
|
11
|
-
} from './__fixtures__/synthetic'
|
|
12
|
-
import { TEMA_GOLDEN, assertSeriesClose } from './__fixtures__/golden'
|
|
13
|
-
import {
|
|
14
|
-
assertFiniteOrUndefined,
|
|
15
|
-
} from './_propertyAssertions'
|
|
16
|
-
|
|
17
|
-
describe('calcTEMAData — Triple Exponential Moving Average', () => {
|
|
18
|
-
describe('edge cases', () => {
|
|
19
|
-
it('empty returns empty array', () => {
|
|
20
|
-
expect(calcTEMAData(empty, 20)).toEqual([])
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('single bar produces a single defined value', () => {
|
|
24
|
-
const out = calcTEMAData(singleBar, 20)
|
|
25
|
-
expect(out).toHaveLength(1)
|
|
26
|
-
expect(out[0]).toBeCloseTo(100, 9)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('period = 0 returns all undefined', () => {
|
|
30
|
-
const zero = calcTEMAData(pureUptrend, 0)
|
|
31
|
-
for (const v of zero) expect(v).toBeUndefined()
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('golden values', () => {
|
|
36
|
-
it('constantPrice → TEMA(20) = 100 throughout', () => {
|
|
37
|
-
assertSeriesClose(calcTEMAData(constantPrice, 20), TEMA_GOLDEN.constantPrice!.series)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('mathematical properties', () => {
|
|
42
|
-
it('all values finite', () => {
|
|
43
|
-
for (const fx of [pureUptrend, pureDowntrend, sideways, spikeAtBar19]) {
|
|
44
|
-
assertFiniteOrUndefined(calcTEMAData(fx, 20), 'TEMA series')
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('tracks uptrend monotonically once warmed', () => {
|
|
49
|
-
const out = calcTEMAData(pureUptrend, 20)
|
|
50
|
-
const tail = out.slice(20).filter((v): v is number => v !== undefined)
|
|
51
|
-
for (let i = 1; i < tail.length; i++) {
|
|
52
|
-
expect(tail[i]!).toBeGreaterThan(tail[i - 1]!)
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('extensional consistency', () => {
|
|
57
|
-
const full = calcTEMAData(pureUptrend, 20)
|
|
58
|
-
for (let n = 5; n < pureUptrend.length; n++) {
|
|
59
|
-
const partial = calcTEMAData(pureUptrend.slice(0, n), 20)
|
|
60
|
-
for (let i = 0; i < n; i++) {
|
|
61
|
-
if (full[i] !== undefined && partial[i] !== undefined) {
|
|
62
|
-
expect(partial[i]).toBeCloseTo(full[i]!, 9)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { calcTEMAData } from '../calculators'
|
|
3
|
+
import {
|
|
4
|
+
empty,
|
|
5
|
+
singleBar,
|
|
6
|
+
constantPrice,
|
|
7
|
+
pureUptrend,
|
|
8
|
+
pureDowntrend,
|
|
9
|
+
sideways,
|
|
10
|
+
spikeAtBar19,
|
|
11
|
+
} from './__fixtures__/synthetic'
|
|
12
|
+
import { TEMA_GOLDEN, assertSeriesClose } from './__fixtures__/golden'
|
|
13
|
+
import {
|
|
14
|
+
assertFiniteOrUndefined,
|
|
15
|
+
} from './_propertyAssertions'
|
|
16
|
+
|
|
17
|
+
describe('calcTEMAData — Triple Exponential Moving Average', () => {
|
|
18
|
+
describe('edge cases', () => {
|
|
19
|
+
it('empty returns empty array', () => {
|
|
20
|
+
expect(calcTEMAData(empty, 20)).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('single bar produces a single defined value', () => {
|
|
24
|
+
const out = calcTEMAData(singleBar, 20)
|
|
25
|
+
expect(out).toHaveLength(1)
|
|
26
|
+
expect(out[0]).toBeCloseTo(100, 9)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('period = 0 returns all undefined', () => {
|
|
30
|
+
const zero = calcTEMAData(pureUptrend, 0)
|
|
31
|
+
for (const v of zero) expect(v).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('golden values', () => {
|
|
36
|
+
it('constantPrice → TEMA(20) = 100 throughout', () => {
|
|
37
|
+
assertSeriesClose(calcTEMAData(constantPrice, 20), TEMA_GOLDEN.constantPrice!.series)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('mathematical properties', () => {
|
|
42
|
+
it('all values finite', () => {
|
|
43
|
+
for (const fx of [pureUptrend, pureDowntrend, sideways, spikeAtBar19]) {
|
|
44
|
+
assertFiniteOrUndefined(calcTEMAData(fx, 20), 'TEMA series')
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('tracks uptrend monotonically once warmed', () => {
|
|
49
|
+
const out = calcTEMAData(pureUptrend, 20)
|
|
50
|
+
const tail = out.slice(20).filter((v): v is number => v !== undefined)
|
|
51
|
+
for (let i = 1; i < tail.length; i++) {
|
|
52
|
+
expect(tail[i]!).toBeGreaterThan(tail[i - 1]!)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('extensional consistency', () => {
|
|
57
|
+
const full = calcTEMAData(pureUptrend, 20)
|
|
58
|
+
for (let n = 5; n < pureUptrend.length; n++) {
|
|
59
|
+
const partial = calcTEMAData(pureUptrend.slice(0, n), 20)
|
|
60
|
+
for (let i = 0; i < n; i++) {
|
|
61
|
+
if (full[i] !== undefined && partial[i] !== undefined) {
|
|
62
|
+
expect(partial[i]).toBeCloseTo(full[i]!, 9)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { calcTRIXData } from '../calculators'
|
|
3
|
-
import {
|
|
4
|
-
empty,
|
|
5
|
-
constantPrice,
|
|
6
|
-
pureUptrend,
|
|
7
|
-
pureDowntrend,
|
|
8
|
-
sideways,
|
|
9
|
-
} from './__fixtures__/synthetic'
|
|
10
|
-
|
|
11
|
-
describe('calcTRIXData', () => {
|
|
12
|
-
it('empty returns empty result', () => {
|
|
13
|
-
const out = calcTRIXData(empty, 15, 9)
|
|
14
|
-
expect(out.series).toEqual([])
|
|
15
|
-
expect(out.signalSeries).toEqual([])
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('period <= 0 returns empty series', () => {
|
|
19
|
-
const out = calcTRIXData(pureUptrend, 0, 9)
|
|
20
|
-
for (const v of out.series) expect(v).toBeUndefined()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('on constantPrice TRIX = 0 (constant EMA3 → no change)', () => {
|
|
24
|
-
const out = calcTRIXData(constantPrice, 15, 9)
|
|
25
|
-
for (let t = 1; t < out.series.length; t++) {
|
|
26
|
-
if (out.series[t] !== undefined) expect(out.series[t]).toBeCloseTo(0, 9)
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('on pureUptrend TRIX > 0 once warmed', () => {
|
|
31
|
-
const out = calcTRIXData(pureUptrend, 5, 3)
|
|
32
|
-
const tail = out.series.slice(10).filter((v): v is number => v !== undefined)
|
|
33
|
-
expect(tail.length).toBeGreaterThan(0)
|
|
34
|
-
for (const v of tail) {
|
|
35
|
-
expect(v).toBeGreaterThan(0)
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('on pureDowntrend TRIX < 0 once warmed', () => {
|
|
40
|
-
const out = calcTRIXData(pureDowntrend, 5, 3)
|
|
41
|
-
const tail = out.series.slice(10).filter((v): v is number => v !== undefined)
|
|
42
|
-
for (const v of tail) {
|
|
43
|
-
expect(v).toBeLessThan(0)
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('signal series is a smoothing of TRIX (less volatile)', () => {
|
|
48
|
-
const out = calcTRIXData(sideways, 5, 3)
|
|
49
|
-
// Both should have defined tails; signal range should not exceed series range
|
|
50
|
-
const series = out.series.filter((v): v is number => v !== undefined)
|
|
51
|
-
const signal = out.signalSeries.filter((v): v is number => v !== undefined)
|
|
52
|
-
if (series.length >= 5 && signal.length >= 5) {
|
|
53
|
-
const seriesRange = Math.max(...series) - Math.min(...series)
|
|
54
|
-
const signalRange = Math.max(...signal) - Math.min(...signal)
|
|
55
|
-
expect(signalRange).toBeLessThanOrEqual(seriesRange + 1e-9)
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('extensional consistency', () => {
|
|
60
|
-
const full = calcTRIXData(pureUptrend, 5, 3)
|
|
61
|
-
for (let n = 10; n < pureUptrend.length; n++) {
|
|
62
|
-
const partial = calcTRIXData(pureUptrend.slice(0, n), 5, 3)
|
|
63
|
-
for (let i = 0; i < n; i++) {
|
|
64
|
-
if (full.series[i] !== undefined && partial.series[i] !== undefined) {
|
|
65
|
-
expect(partial.series[i]).toBeCloseTo(full.series[i]!, 9)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { calcTRIXData } from '../calculators'
|
|
3
|
+
import {
|
|
4
|
+
empty,
|
|
5
|
+
constantPrice,
|
|
6
|
+
pureUptrend,
|
|
7
|
+
pureDowntrend,
|
|
8
|
+
sideways,
|
|
9
|
+
} from './__fixtures__/synthetic'
|
|
10
|
+
|
|
11
|
+
describe('calcTRIXData', () => {
|
|
12
|
+
it('empty returns empty result', () => {
|
|
13
|
+
const out = calcTRIXData(empty, 15, 9)
|
|
14
|
+
expect(out.series).toEqual([])
|
|
15
|
+
expect(out.signalSeries).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('period <= 0 returns empty series', () => {
|
|
19
|
+
const out = calcTRIXData(pureUptrend, 0, 9)
|
|
20
|
+
for (const v of out.series) expect(v).toBeUndefined()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('on constantPrice TRIX = 0 (constant EMA3 → no change)', () => {
|
|
24
|
+
const out = calcTRIXData(constantPrice, 15, 9)
|
|
25
|
+
for (let t = 1; t < out.series.length; t++) {
|
|
26
|
+
if (out.series[t] !== undefined) expect(out.series[t]).toBeCloseTo(0, 9)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('on pureUptrend TRIX > 0 once warmed', () => {
|
|
31
|
+
const out = calcTRIXData(pureUptrend, 5, 3)
|
|
32
|
+
const tail = out.series.slice(10).filter((v): v is number => v !== undefined)
|
|
33
|
+
expect(tail.length).toBeGreaterThan(0)
|
|
34
|
+
for (const v of tail) {
|
|
35
|
+
expect(v).toBeGreaterThan(0)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('on pureDowntrend TRIX < 0 once warmed', () => {
|
|
40
|
+
const out = calcTRIXData(pureDowntrend, 5, 3)
|
|
41
|
+
const tail = out.series.slice(10).filter((v): v is number => v !== undefined)
|
|
42
|
+
for (const v of tail) {
|
|
43
|
+
expect(v).toBeLessThan(0)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('signal series is a smoothing of TRIX (less volatile)', () => {
|
|
48
|
+
const out = calcTRIXData(sideways, 5, 3)
|
|
49
|
+
// Both should have defined tails; signal range should not exceed series range
|
|
50
|
+
const series = out.series.filter((v): v is number => v !== undefined)
|
|
51
|
+
const signal = out.signalSeries.filter((v): v is number => v !== undefined)
|
|
52
|
+
if (series.length >= 5 && signal.length >= 5) {
|
|
53
|
+
const seriesRange = Math.max(...series) - Math.min(...series)
|
|
54
|
+
const signalRange = Math.max(...signal) - Math.min(...signal)
|
|
55
|
+
expect(signalRange).toBeLessThanOrEqual(seriesRange + 1e-9)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('extensional consistency', () => {
|
|
60
|
+
const full = calcTRIXData(pureUptrend, 5, 3)
|
|
61
|
+
for (let n = 10; n < pureUptrend.length; n++) {
|
|
62
|
+
const partial = calcTRIXData(pureUptrend.slice(0, n), 5, 3)
|
|
63
|
+
for (let i = 0; i < n; i++) {
|
|
64
|
+
if (full.series[i] !== undefined && partial.series[i] !== undefined) {
|
|
65
|
+
expect(partial.series[i]).toBeCloseTo(full.series[i]!, 9)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|