@ch20026103/anysis 0.0.19-alpha → 0.0.19-beta

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.
@@ -0,0 +1,28 @@
1
+ import { StockListType, StockType } from "./types";
2
+ export type DmiResType = {
3
+ dataset: StockListType;
4
+ pDi: number;
5
+ mDi: number;
6
+ adx: number;
7
+ type: number;
8
+ smoothTr: number;
9
+ smoothPdm: number;
10
+ smoothMdm: number;
11
+ dxBuffer: number[];
12
+ smoothAdx: number;
13
+ };
14
+ export default class Dmi {
15
+ init(data: StockType, type: number): DmiResType;
16
+ next(data: StockType, preList: DmiResType, type: number): DmiResType;
17
+ calculateDmiValues(list: StockListType, period?: number): {
18
+ pDi: number;
19
+ mDi: number;
20
+ adx: number;
21
+ c: number;
22
+ v: number;
23
+ l: number;
24
+ h: number;
25
+ o: number;
26
+ t: number;
27
+ }[];
28
+ }
@@ -0,0 +1,204 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
13
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
14
+ if (ar || !(i in from)) {
15
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
16
+ ar[i] = from[i];
17
+ }
18
+ }
19
+ return to.concat(ar || Array.prototype.slice.call(from));
20
+ };
21
+ var Dmi = /** @class */ (function () {
22
+ function Dmi() {
23
+ }
24
+ Object.defineProperty(Dmi.prototype, "init", {
25
+ enumerable: false,
26
+ configurable: true,
27
+ writable: true,
28
+ value: function (data, type) {
29
+ return {
30
+ dataset: [data],
31
+ pDi: 0,
32
+ mDi: 0,
33
+ adx: 0,
34
+ type: type,
35
+ smoothTr: 0,
36
+ smoothPdm: 0,
37
+ smoothMdm: 0,
38
+ dxBuffer: [],
39
+ smoothAdx: 0,
40
+ };
41
+ }
42
+ });
43
+ Object.defineProperty(Dmi.prototype, "next", {
44
+ enumerable: false,
45
+ configurable: true,
46
+ writable: true,
47
+ value: function (data, preList, type) {
48
+ preList.dataset.push(data);
49
+ // Need type + 1 data points to have 'type' periods of change
50
+ if (preList.dataset.length < type + 1) {
51
+ return __assign(__assign({}, preList), { pDi: 0, mDi: 0, adx: 0 });
52
+ }
53
+ else {
54
+ var currentTr = 0;
55
+ var currentPdm = 0;
56
+ var currentMdm = 0;
57
+ // Logic to get TR, +DM, -DM for the NEWEST data point
58
+ // We need the newest point and the one before it.
59
+ // If we are in the initialization phase (length === type + 1), we process the whole window.
60
+ // If we are in the incremental phase (length > type + 1), we just process the last step.
61
+ var newSmoothTr = preList.smoothTr;
62
+ var newSmoothPdm = preList.smoothPdm;
63
+ var newSmoothMdm = preList.smoothMdm;
64
+ if (preList.dataset.length === type + 1) {
65
+ // Initialization: Calculate sum of first N periods (using N+1 data points)
66
+ var sumTr = 0;
67
+ var sumPdm = 0;
68
+ var sumMdm = 0;
69
+ for (var i = 1; i <= type; i++) {
70
+ var curr = preList.dataset[i];
71
+ var prev = preList.dataset[i - 1];
72
+ // TR
73
+ var hl = curr.h - curr.l;
74
+ var hpc = Math.abs(curr.h - prev.c);
75
+ var lpc = Math.abs(curr.l - prev.c);
76
+ var tr = Math.max(hl, hpc, lpc);
77
+ // DM
78
+ var hph = curr.h - prev.h;
79
+ var pll = prev.l - curr.l;
80
+ var pdm = 0;
81
+ var mdm = 0;
82
+ if (hph > pll && hph > 0) {
83
+ pdm = hph;
84
+ }
85
+ if (pll > hph && pll > 0) {
86
+ mdm = pll;
87
+ }
88
+ sumTr += tr;
89
+ sumPdm += pdm;
90
+ sumMdm += mdm;
91
+ }
92
+ // Wilder's first value is often just the Sum.
93
+ // But to make it consistent with the "Average" view for the formula:
94
+ // NextAvg = (PrevAvg * (N-1) + Curr) / N
95
+ // The first "PrevAvg" acts as the seed. The seed is usually the Simple Average.
96
+ // So we divide by type.
97
+ newSmoothTr = sumTr; // Some sources say keep Sum. But standard indicators often normalize to Average range.
98
+ // Let's check RSI implementation. RSI divides by type: gains / type.
99
+ // So we will divide by type to get the Average TR/DM.
100
+ // Wait! DMI standard often keeps the SUM for the first value?
101
+ // Wilder's book: "+DM14 is the sum of the +DM for the last 14 days".
102
+ // Then subsequent: "+DM14_today = +DM14_yesterday - (+DM14_yesterday/14) + +DM_today".
103
+ // This formula maintains the "Sum" magnitude (approx 14x the average).
104
+ // BUT, RSI implementation uses Average magnitude (0-100 range inputs usually lead to small AvgGain).
105
+ // Let's stick to the RSI pattern: Average.
106
+ // Formula: (Avg * (N-1) + Curr) / N. This maintains "Average" magnitude.
107
+ // If we used Sum logic: (Sum - Sum/N + Curr) = Sum * (1 - 1/N) + Curr.
108
+ // These are mathematically consistent in shape, just scaled by N.
109
+ // DI calculation is (Pdm / Tr) * 100. The scale cancels out!
110
+ // So using Average is safer for preventing overflow and easier to debug (per-day values).
111
+ newSmoothTr = sumTr / type;
112
+ newSmoothPdm = sumPdm / type;
113
+ newSmoothMdm = sumMdm / type;
114
+ }
115
+ else {
116
+ // Shift if needed to keep dataset size manageable, though strictly we only need last 2 points
117
+ // reusing rsi pattern:
118
+ if (preList.dataset.length > type + 1) {
119
+ preList.dataset.shift();
120
+ }
121
+ var curr = preList.dataset[preList.dataset.length - 1];
122
+ var prev = preList.dataset[preList.dataset.length - 2];
123
+ // TR
124
+ var hl = curr.h - curr.l;
125
+ var hpc = Math.abs(curr.h - prev.c);
126
+ var lpc = Math.abs(curr.l - prev.c);
127
+ var tr = Math.max(hl, hpc, lpc);
128
+ // DM
129
+ var hph = curr.h - prev.h;
130
+ var pll = prev.l - curr.l;
131
+ var pdm = 0;
132
+ var mdm = 0;
133
+ if (hph > pll && hph > 0)
134
+ pdm = hph;
135
+ if (pll > hph && pll > 0)
136
+ mdm = pll;
137
+ // Wilder's Smoothing (Average form)
138
+ newSmoothTr = (preList.smoothTr * (type - 1) + tr) / type;
139
+ newSmoothPdm = (preList.smoothPdm * (type - 1) + pdm) / type;
140
+ newSmoothMdm = (preList.smoothMdm * (type - 1) + mdm) / type;
141
+ }
142
+ // Calculate DI
143
+ // Avoid division by zero
144
+ var pDi = newSmoothTr === 0 ? 0 : (newSmoothPdm / newSmoothTr) * 100;
145
+ var mDi = newSmoothTr === 0 ? 0 : (newSmoothMdm / newSmoothTr) * 100;
146
+ // Calculate DX
147
+ var diDiff = Math.abs(pDi - mDi);
148
+ var diSum = pDi + mDi;
149
+ var dx = diSum === 0 ? 0 : (diDiff / diSum) * 100;
150
+ // ADX Logic
151
+ var dxBuffer = __spreadArray([], preList.dxBuffer, true);
152
+ var adx = preList.adx;
153
+ var newSmoothAdx = preList.smoothAdx;
154
+ if (dxBuffer.length < type) {
155
+ dxBuffer.push(dx);
156
+ // Special case: if we Just reached 'type' count, we can calc initial ADX
157
+ if (dxBuffer.length === type) {
158
+ // First ADX is average of the DX buffer?
159
+ // "ADX is the 14-day smoothed average of DX".
160
+ // First value is simple average of previous 14 DX values.
161
+ var sumDx = dxBuffer.reduce(function (a, b) { return a + b; }, 0);
162
+ newSmoothAdx = sumDx / type;
163
+ adx = newSmoothAdx;
164
+ }
165
+ else {
166
+ // Not enough data for ADX yet
167
+ adx = 0;
168
+ }
169
+ }
170
+ else {
171
+ // We already have ADX initialized, so we smooth it
172
+ // Note: we don't need to keep growing dxBuffer indefinitely.
173
+ // We just needed it for startup.
174
+ // Update ADX using Wilder's smoothing
175
+ newSmoothAdx = (preList.smoothAdx * (type - 1) + dx) / type;
176
+ adx = newSmoothAdx;
177
+ }
178
+ return __assign(__assign({}, preList), { pDi: pDi, // +DI
179
+ mDi: mDi, // -DI
180
+ adx: adx, smoothTr: newSmoothTr, smoothPdm: newSmoothPdm, smoothMdm: newSmoothMdm, dxBuffer: dxBuffer, smoothAdx: newSmoothAdx });
181
+ }
182
+ }
183
+ });
184
+ // Helper to get formatted results similar to ma.ts
185
+ Object.defineProperty(Dmi.prototype, "calculateDmiValues", {
186
+ enumerable: false,
187
+ configurable: true,
188
+ writable: true,
189
+ value: function (list, period) {
190
+ if (period === void 0) { period = 14; }
191
+ var res = [];
192
+ var state = this.init(list[0], period);
193
+ // First point (index 0) has 0 DMI
194
+ res.push(__assign(__assign({}, list[0]), { pDi: 0, mDi: 0, adx: 0 }));
195
+ for (var i = 1; i < list.length; i++) {
196
+ state = this.next(list[i], state, period);
197
+ res.push(__assign(__assign({}, list[i]), { pDi: state.pDi, mDi: state.mDi, adx: state.adx }));
198
+ }
199
+ return res;
200
+ }
201
+ });
202
+ return Dmi;
203
+ }());
204
+ export default Dmi;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import Dmi from "./dmi";
2
+ import data from "./test_data.test";
3
+ describe("test dmi methods", function () {
4
+ it("test init & next", function () {
5
+ var dmi = new Dmi();
6
+ var period = 14;
7
+ // Calculate all values using loop
8
+ var results = dmi.calculateDmiValues(data, period);
9
+ var lastResult = results[results.length - 1];
10
+ // Verify consistency
11
+ // Re-run step-by-step
12
+ var state = dmi.init(data[0], period);
13
+ for (var i = 1; i < data.length; i++) {
14
+ state = dmi.next(data[i], state, period);
15
+ }
16
+ expect(state.pDi).toEqual(lastResult.pDi);
17
+ expect(state.mDi).toEqual(lastResult.mDi);
18
+ expect(state.adx).toEqual(lastResult.adx);
19
+ });
20
+ it("test values consistency (basic sanity)", function () {
21
+ // Generate a simple synthetic dataset where Trend is obvious
22
+ // Upward trend -> +DI should be high, -DI low
23
+ var upTrendData = [];
24
+ for (var i = 0; i < 50; i++) {
25
+ upTrendData.push({
26
+ c: 10 + i,
27
+ h: 11 + i,
28
+ l: 9 + i,
29
+ o: 10 + i,
30
+ v: 1000,
31
+ t: 20210101 + i,
32
+ });
33
+ }
34
+ var dmi = new Dmi();
35
+ var res = dmi.calculateDmiValues(upTrendData, 14);
36
+ var last = res[res.length - 1];
37
+ // +DI should be dominant
38
+ expect(last.pDi).toBeGreaterThan(last.mDi);
39
+ // ADX should be high (strong trend)
40
+ expect(last.adx).toBeGreaterThan(20);
41
+ });
42
+ it("test flat trend", function () {
43
+ // Flat data
44
+ var flatData = [];
45
+ for (var i = 0; i < 50; i++) {
46
+ flatData.push({
47
+ c: 10,
48
+ h: 11,
49
+ l: 9,
50
+ o: 10,
51
+ v: 1000,
52
+ t: 20210101 + i,
53
+ });
54
+ }
55
+ var dmi = new Dmi();
56
+ var res = dmi.calculateDmiValues(flatData, 14);
57
+ var last = res[res.length - 1];
58
+ // No directional movement
59
+ // h-ph = 0, pl-l = 0.
60
+ // +DM = 0, -DM = 0.
61
+ // +DI = 0, -DI = 0.
62
+ // ADX = 0?
63
+ // DX = 0.
64
+ expect(last.pDi).toEqual(0);
65
+ expect(last.mDi).toEqual(0);
66
+ // ADX might be 0 or close to 0
67
+ expect(last.adx).toEqual(0);
68
+ });
69
+ });
@@ -0,0 +1,35 @@
1
+ import { StockListType, StockType } from "./types";
2
+ type NewStockType = Required<Pick<StockType, "v" | "h" | "l" | "c">> & StockType;
3
+ export type IchimokuValType = {
4
+ tenkan: number | null;
5
+ kijun: number | null;
6
+ senkouA: number | null;
7
+ senkouB: number | null;
8
+ chikou: number | null;
9
+ };
10
+ export type IchimokuResType = {
11
+ dataset: Readonly<StockListType>;
12
+ ichimoku: IchimokuValType;
13
+ };
14
+ interface IchimokuType {
15
+ init: (data: NewStockType) => IchimokuResType;
16
+ next: (data: NewStockType, preList: IchimokuResType) => IchimokuResType;
17
+ }
18
+ export default class Ichimoku implements IchimokuType {
19
+ init(data: NewStockType): IchimokuResType;
20
+ /**
21
+ * 優化說明:
22
+ * 如果 preList.dataset 是一個可變陣列,我們直接 push 以達到最佳效能 O(1)。
23
+ * 如果你的框架 (如 React state) 強制要求 immutable,則需要改回 [...prev, data] 的寫法。
24
+ * 下面的寫法假設可以 Mutation (這在 Class 內部運算或 Backend 處理很常見)。
25
+ */
26
+ next(data: NewStockType, preList: IchimokuResType): IchimokuResType;
27
+ private calcIchimoku;
28
+ /**
29
+ * 優化:
30
+ * 1. 移除 .slice(),避免產生 Garbage Collection。
31
+ * 2. 使用反向迴圈 (i--),通常在 JS 引擎中微幅快一點,且語意上是「從現在往回看」。
32
+ */
33
+ private getMidPrice;
34
+ }
35
+ export {};
@@ -0,0 +1,94 @@
1
+ var Ichimoku = /** @class */ (function () {
2
+ function Ichimoku() {
3
+ }
4
+ Object.defineProperty(Ichimoku.prototype, "init", {
5
+ enumerable: false,
6
+ configurable: true,
7
+ writable: true,
8
+ value: function (data) {
9
+ var dataset = [data];
10
+ return {
11
+ dataset: dataset,
12
+ ichimoku: this.calcIchimoku(dataset, dataset.length - 1),
13
+ };
14
+ }
15
+ });
16
+ /**
17
+ * 優化說明:
18
+ * 如果 preList.dataset 是一個可變陣列,我們直接 push 以達到最佳效能 O(1)。
19
+ * 如果你的框架 (如 React state) 強制要求 immutable,則需要改回 [...prev, data] 的寫法。
20
+ * 下面的寫法假設可以 Mutation (這在 Class 內部運算或 Backend 處理很常見)。
21
+ */
22
+ Object.defineProperty(Ichimoku.prototype, "next", {
23
+ enumerable: false,
24
+ configurable: true,
25
+ writable: true,
26
+ value: function (data, preList) {
27
+ // 強制轉型以進行 push (避免 typescript 報錯 readonly)
28
+ var mutableDataset = preList.dataset;
29
+ mutableDataset.push(data);
30
+ // 只需要計算最後一筆
31
+ var currentResult = this.calcIchimoku(mutableDataset, mutableDataset.length - 1);
32
+ return {
33
+ dataset: mutableDataset,
34
+ ichimoku: currentResult,
35
+ };
36
+ }
37
+ });
38
+ // 核心計算邏輯
39
+ Object.defineProperty(Ichimoku.prototype, "calcIchimoku", {
40
+ enumerable: false,
41
+ configurable: true,
42
+ writable: true,
43
+ value: function (dataList, i) {
44
+ var currentData = dataList[i];
45
+ // 優化:直接傳入 index 與 list,不產生新陣列
46
+ var tenkanVal = this.getMidPrice(dataList, i, 9);
47
+ var kijunVal = this.getMidPrice(dataList, i, 26);
48
+ var senkouBVal = this.getMidPrice(dataList, i, 52);
49
+ var senkouAVal = null;
50
+ if (tenkanVal !== null && kijunVal !== null) {
51
+ senkouAVal = (tenkanVal + kijunVal) / 2;
52
+ }
53
+ return {
54
+ tenkan: tenkanVal,
55
+ kijun: kijunVal,
56
+ senkouA: senkouAVal,
57
+ senkouB: senkouBVal,
58
+ chikou: currentData.c,
59
+ };
60
+ }
61
+ });
62
+ /**
63
+ * 優化:
64
+ * 1. 移除 .slice(),避免產生 Garbage Collection。
65
+ * 2. 使用反向迴圈 (i--),通常在 JS 引擎中微幅快一點,且語意上是「從現在往回看」。
66
+ */
67
+ Object.defineProperty(Ichimoku.prototype, "getMidPrice", {
68
+ enumerable: false,
69
+ configurable: true,
70
+ writable: true,
71
+ value: function (list, currentIndex, periods) {
72
+ if (currentIndex < periods - 1) {
73
+ return null;
74
+ }
75
+ // 計算視窗的起始 index
76
+ var start = currentIndex - (periods - 1);
77
+ // 初始化最大最小
78
+ // 小技巧:直接拿第一個值當初始值,避免 Infinity 的比較
79
+ var maxH = list[start].h;
80
+ var minL = list[start].l;
81
+ // 從 start + 1 開始遍歷到 currentIndex
82
+ for (var j = start + 1; j <= currentIndex; j++) {
83
+ var _a = list[j], h = _a.h, l = _a.l;
84
+ if (h > maxH)
85
+ maxH = h;
86
+ if (l < minL)
87
+ minL = l;
88
+ }
89
+ return (maxH + minL) / 2;
90
+ }
91
+ });
92
+ return Ichimoku;
93
+ }());
94
+ export default Ichimoku;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { beforeEach, describe, expect, it } from "vitest";
13
+ import Ichimoku from "./ichimoku";
14
+ // 輔助函式:快速產生測試用的假資料
15
+ var createStock = function (idx, override) {
16
+ return __assign({ t: idx, o: 100, c: 100, h: 100, l: 100, v: 1000 }, override);
17
+ };
18
+ describe("Ichimoku Algo", function () {
19
+ var ichimoku;
20
+ beforeEach(function () {
21
+ ichimoku = new Ichimoku();
22
+ });
23
+ describe("init", function () {
24
+ it("should initialize with correct structure", function () {
25
+ var data = createStock(0, { c: 150 });
26
+ var res = ichimoku.init(data);
27
+ expect(res.dataset).toHaveLength(1);
28
+ expect(res.dataset[0]).toEqual(data);
29
+ // 第一筆資料,除了 chikou (收盤價) 外,其他應為 null
30
+ expect(res.ichimoku.chikou).toBe(150);
31
+ expect(res.ichimoku.tenkan).toBeNull();
32
+ expect(res.ichimoku.kijun).toBeNull();
33
+ expect(res.ichimoku.senkouA).toBeNull();
34
+ expect(res.ichimoku.senkouB).toBeNull();
35
+ });
36
+ });
37
+ describe("next & Logic Calculation", function () {
38
+ it("should accumulate dataset correctly", function () {
39
+ var res = ichimoku.init(createStock(0));
40
+ res = ichimoku.next(createStock(1), res);
41
+ res = ichimoku.next(createStock(2), res);
42
+ expect(res.dataset).toHaveLength(3);
43
+ expect(res.dataset[2].t).toBe(2);
44
+ });
45
+ it("should calculate Tenkan-sen (9 periods) correctly", function () {
46
+ // 策略:建立 8 筆資料,確認 Tenkan 為 null
47
+ // 再加第 9 筆,確認 Tenkan 有值
48
+ // 設定數據:High 逐漸增加 (10~90),Low 保持 0
49
+ // 預期結果:(MaxHigh(90) + MinLow(0)) / 2 = 45
50
+ var res = ichimoku.init(createStock(0, { h: 10, l: 0 }));
51
+ // 加入第 2 到第 8 筆 (共 8 筆)
52
+ for (var i = 1; i < 8; i++) {
53
+ res = ichimoku.next(createStock(i, { h: (i + 1) * 10, l: 0 }), res);
54
+ expect(res.ichimoku.tenkan).toBeNull(); // 資料不足 9 筆
55
+ }
56
+ // 加入第 9 筆
57
+ res = ichimoku.next(createStock(8, { h: 90, l: 0 }), res);
58
+ // 驗證
59
+ expect(res.dataset).toHaveLength(9);
60
+ expect(res.ichimoku.tenkan).toBe(45); // (90 + 0) / 2
61
+ });
62
+ it("should calculate Kijun-sen (26 periods) correctly", function () {
63
+ // 策略:直接灌入 25 筆資料
64
+ // 第 1 筆 Low = 10,第 26 筆 High = 110,中間平穩
65
+ // Kijun = (110 + 10) / 2 = 60
66
+ var res = ichimoku.init(createStock(0, { h: 50, l: 10 })); // 最低點在 index 0
67
+ // 填補中間資料 (index 1 ~ 24)
68
+ for (var i = 1; i < 25; i++) {
69
+ res = ichimoku.next(createStock(i, { h: 50, l: 50 }), res);
70
+ expect(res.ichimoku.kijun).toBeNull(); // 資料不足 26 筆
71
+ }
72
+ // 加入第 26 筆 (index 25)
73
+ res = ichimoku.next(createStock(25, { h: 110, l: 50 }), res); // 最高點在 index 25
74
+ expect(res.dataset).toHaveLength(26);
75
+ expect(res.ichimoku.kijun).toBe(60); // (110 + 10) / 2
76
+ });
77
+ it("should calculate Senkou Span A correctly", function () {
78
+ // Senkou A = (Tenkan + Kijun) / 2
79
+ // 我們需要至少 26 筆資料讓 Kijun 有值 (此時 Tenkan 也有值)
80
+ var res = ichimoku.init(createStock(0));
81
+ // 我們設計一個場景:
82
+ // 過去 9 天 (Tenkan window): Max=20, Min=10 -> Tenkan = 15
83
+ // 過去 26 天 (Kijun window): Max=30, Min=0 -> Kijun = 15
84
+ // 預期 Senkou A = (15 + 15) / 2 = 15
85
+ // 先塞入前段資料
86
+ for (var i = 1; i < 26; i++) {
87
+ // 為了方便控制,我們讓最後一筆資料決定極值
88
+ // 這裡隨便塞,只要最後一筆能控制範圍即可,因為我們邏輯是找區間最大最小
89
+ // 但為了確保 Kijun 區間的 Min 是 0,我們在 index 0 設 l=0
90
+ // 為了確保 Kijun 區間的 Max 是 30,我們在 index 0 設 h=30 (如果它是最大)
91
+ // 簡單化:讓資料全部平躺,最後一瞬間拉高拉低
92
+ // 這裡不用迴圈邏輯太複雜,直接用輔助函式慢慢疊
93
+ res = ichimoku.next(createStock(i), res);
94
+ }
95
+ // 重新建立一個乾淨的測試流程,比較好控制數學
96
+ // Reset
97
+ res = ichimoku.init(createStock(0, { h: 100, l: 100 }));
98
+ // 填充 24 筆 (共 25 筆)
99
+ for (var i = 1; i < 25; i++) {
100
+ res = ichimoku.next(createStock(i, { h: 100, l: 100 }), res);
101
+ }
102
+ // 第 26 筆 (Index 25) 進來,決定生死
103
+ // Kijun 看過去 26 筆 (idx 0~25)
104
+ // Tenkan 看過去 9 筆 (idx 17~25)
105
+ // 設定目標:
106
+ // Tenkan: Max=110, Min=90 => Avg=100
107
+ // Kijun: Max=120, Min=80 => Avg=100
108
+ // SenkouA => 100
109
+ // 修改 dataset 裡的值 (模擬真實波動)
110
+ // 讓 index 0 (很久以前) 有個極低值 80 (影響 Kijun 不影響 Tenkan)
111
+ // 讓 index 0 有個極高值 120 (影響 Kijun 不影響 Tenkan)
112
+ // 注意:上面的 init 和 next 已經把資料寫死為 100 了,這在單元測試有點難搞
113
+ // 所以我們用更簡單的方法:在 next 過程中精準控制
114
+ // --- 重來:精準控制版 ---
115
+ var ichi = new Ichimoku();
116
+ var r = ichi.init(createStock(0, { h: 120, l: 80 })); // Kijun Range: H=120, L=80
117
+ // 填滿中間 16 筆 (Index 1~16),數值平穩不影響極值,且不讓 Tenkan 抓到
118
+ for (var i = 1; i <= 16; i++) {
119
+ r = ichi.next(createStock(i, { h: 100, l: 100 }), r);
120
+ }
121
+ // 接下來 8 筆 (Index 17~24),準備進入 Tenkan 範圍
122
+ // 我們讓 Tenkan 範圍 (包含下一筆 index 25) 為 H=110, L=90
123
+ for (var i = 17; i <= 24; i++) {
124
+ r = ichi.next(createStock(i, { h: 110, l: 90 }), r);
125
+ }
126
+ // 第 26 筆 (Index 25)
127
+ r = ichi.next(createStock(25, { h: 110, l: 90 }), r);
128
+ // 驗證 Tenkan (看 index 17~25)
129
+ // MaxH = 110, MinL = 90 => 100
130
+ expect(r.ichimoku.tenkan).toBe(100);
131
+ // 驗證 Kijun (看 index 0~25)
132
+ // MaxH = 120 (from index 0), MinL = 80 (from index 0) => 100
133
+ expect(r.ichimoku.kijun).toBe(100);
134
+ // 驗證 Senkou A
135
+ expect(r.ichimoku.senkouA).toBe(100);
136
+ });
137
+ it("should calculate Senkou Span B (52 periods) correctly", function () {
138
+ var res = ichimoku.init(createStock(0, { h: 200, l: 0 })); // 極值在最開始
139
+ // 填到 50 筆
140
+ for (var i = 1; i < 51; i++) {
141
+ res = ichimoku.next(createStock(i, { h: 100, l: 100 }), res);
142
+ expect(res.ichimoku.senkouB).toBeNull();
143
+ }
144
+ // 第 52 筆
145
+ res = ichimoku.next(createStock(51, { h: 100, l: 100 }), res);
146
+ // 52 期間 (0~51): Max=200, Min=0 => 100
147
+ expect(res.ichimoku.senkouB).toBe(100);
148
+ });
149
+ it("should handle Chikou Span (just close price)", function () {
150
+ var res = ichimoku.init(createStock(0, { c: 555 }));
151
+ expect(res.ichimoku.chikou).toBe(555);
152
+ res = ichimoku.next(createStock(1, { c: 888 }), res);
153
+ expect(res.ichimoku.chikou).toBe(888);
154
+ });
155
+ });
156
+ });