@ch20026103/anysis 0.0.17-alpha2 → 0.0.19-alpha
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/demo/main.js +42 -11
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/stockSkills/ad.d.ts +17 -0
- package/dist/cjs/stockSkills/ad.js +57 -0
- package/dist/cjs/stockSkills/mfi.d.ts +15 -0
- package/dist/cjs/stockSkills/mfi.js +166 -0
- package/dist/cjs/stockSkills/mfi.test.d.ts +1 -0
- package/dist/cjs/stockSkills/mfi.test.js +193 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/stockSkills/ad.d.ts +17 -0
- package/dist/esm/stockSkills/ad.js +55 -0
- package/dist/esm/stockSkills/mfi.d.ts +15 -0
- package/dist/esm/stockSkills/mfi.js +164 -0
- package/dist/esm/stockSkills/mfi.test.d.ts +1 -0
- package/dist/esm/stockSkills/mfi.test.js +191 -0
- package/dist/umd/index.js +274 -53
- package/package.json +17 -15
package/demo/main.js
CHANGED
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
/* eslint @typescript-eslint/no-var-requires: "off" */
|
|
2
2
|
const axios = require("axios");
|
|
3
|
-
const {
|
|
3
|
+
const { Ad } = require("../dist/cjs/index.js");
|
|
4
4
|
|
|
5
5
|
// 使用示例
|
|
6
|
-
const
|
|
6
|
+
const ad = new Ad();
|
|
7
7
|
function DemoDay(stockId) {
|
|
8
8
|
axios
|
|
9
9
|
.get(
|
|
10
|
-
`https://tw.
|
|
10
|
+
`https://tw.stock.yahoo.com/_td-stock/api/resource/FinanceChartService.ApacLibraCharts;period=d;symbols=[%22${stockId}%22]`
|
|
11
11
|
)
|
|
12
12
|
.then((res) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const json = res.data;
|
|
14
|
+
const opens = json[0].chart.indicators.quote[0].open;
|
|
15
|
+
const closes = json[0].chart.indicators.quote[0].close;
|
|
16
|
+
const highs = json[0].chart.indicators.quote[0].high;
|
|
17
|
+
const lows = json[0].chart.indicators.quote[0].low;
|
|
18
|
+
const volumes = json[0].chart.indicators.quote[0].volume;
|
|
19
|
+
const ts = json[0].chart.timestamp.map((item) => {
|
|
20
|
+
const date = new Date(item * 1000);
|
|
21
|
+
const formatDateTime = () => {
|
|
22
|
+
const year = date.getFullYear();
|
|
23
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
24
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
25
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
26
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
27
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
28
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
29
|
+
};
|
|
30
|
+
return formatDateTime();
|
|
31
|
+
});
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
for (let i =
|
|
19
|
-
|
|
33
|
+
const response = [];
|
|
34
|
+
for (let i = 0; i < opens.length; i++) {
|
|
35
|
+
if (opens[i] !== null) {
|
|
36
|
+
response.push({
|
|
37
|
+
t: ts[i],
|
|
38
|
+
o: opens[i],
|
|
39
|
+
c: closes[i],
|
|
40
|
+
h: highs[i],
|
|
41
|
+
l: lows[i],
|
|
42
|
+
v: volumes[i],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
20
45
|
}
|
|
21
|
-
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
let adData = ad.init(response[0], 14);
|
|
49
|
+
for (let i = 1; i < response.length; i++) {
|
|
50
|
+
adData = ad.next(response[i], adData, 14);
|
|
51
|
+
}
|
|
52
|
+
console.log(adData);
|
|
22
53
|
})
|
|
23
54
|
.catch((error) => {
|
|
24
55
|
console.error(error);
|
|
@@ -28,4 +59,4 @@ function DemoDay(stockId) {
|
|
|
28
59
|
});
|
|
29
60
|
}
|
|
30
61
|
|
|
31
|
-
DemoDay("
|
|
62
|
+
DemoDay("1618");
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { default as Week } from "./stockSkills/week.js";
|
|
|
19
19
|
export { default as Vma } from "./stockSkills/vma.js";
|
|
20
20
|
export { default as Williams } from "./stockSkills/williams.js";
|
|
21
21
|
export { default as ObvEma } from "./stockSkills/obv_ema.js";
|
|
22
|
+
export { default as Mfi } from "./stockSkills/mfi.js";
|
|
23
|
+
export { default as Ad } from "./stockSkills/ad.js";
|
|
22
24
|
export { add } from "./test/add.js";
|
|
23
25
|
export { minus } from "./test/minus.js";
|
|
24
26
|
export { default as calculateDivisionFactor } from "./utils/calculateDivisionFactor.js";
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseLsusbOutput = exports.isJSON = exports.calculateDivisionFactor = exports.minus = exports.add = exports.ObvEma = exports.Williams = exports.Vma = exports.Week = exports.dateFormat = exports.Rsi = exports.Obv = exports.Macd = exports.Ma = exports.Kd = exports.Gold = exports.Ema = exports.Boll = exports.calcSeasonalIndicesNoTrend = exports.weightMovingAverages = exports.movingAverages = exports.exponentialSmoothing = exports.findTroughByGradient = exports.findPeaksByGradient = exports.SwingExtremesType = exports.SwingExtremes = exports.slope = exports.simpleRegressionModel = exports.Angle = void 0;
|
|
3
|
+
exports.parseLsusbOutput = exports.isJSON = exports.calculateDivisionFactor = exports.minus = exports.add = exports.Ad = exports.Mfi = exports.ObvEma = exports.Williams = exports.Vma = exports.Week = exports.dateFormat = exports.Rsi = exports.Obv = exports.Macd = exports.Ma = exports.Kd = exports.Gold = exports.Ema = exports.Boll = exports.calcSeasonalIndicesNoTrend = exports.weightMovingAverages = exports.movingAverages = exports.exponentialSmoothing = exports.findTroughByGradient = exports.findPeaksByGradient = exports.SwingExtremesType = exports.SwingExtremes = exports.slope = exports.simpleRegressionModel = exports.Angle = void 0;
|
|
4
4
|
/*
|
|
5
5
|
請注意,在 src/index.ts 中,我的導入包含文件擴展名(.js)。
|
|
6
6
|
如果需要支持 Node.js 和構建工具(ex: webpack),則不需要這樣做。 **因為commonJs默認js副檔名**
|
|
@@ -49,6 +49,10 @@ var williams_js_1 = require("./stockSkills/williams.js");
|
|
|
49
49
|
Object.defineProperty(exports, "Williams", { enumerable: true, get: function () { return williams_js_1.default; } });
|
|
50
50
|
var obv_ema_js_1 = require("./stockSkills/obv_ema.js");
|
|
51
51
|
Object.defineProperty(exports, "ObvEma", { enumerable: true, get: function () { return obv_ema_js_1.default; } });
|
|
52
|
+
var mfi_js_1 = require("./stockSkills/mfi.js");
|
|
53
|
+
Object.defineProperty(exports, "Mfi", { enumerable: true, get: function () { return mfi_js_1.default; } });
|
|
54
|
+
var ad_js_1 = require("./stockSkills/ad.js");
|
|
55
|
+
Object.defineProperty(exports, "Ad", { enumerable: true, get: function () { return ad_js_1.default; } });
|
|
52
56
|
var add_js_1 = require("./test/add.js");
|
|
53
57
|
Object.defineProperty(exports, "add", { enumerable: true, get: function () { return add_js_1.add; } });
|
|
54
58
|
var minus_js_1 = require("./test/minus.js");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StockListType, StockType } from "./types";
|
|
2
|
+
type NewStockType = Required<Pick<StockType, "v">> & StockType;
|
|
3
|
+
export type AdResType = {
|
|
4
|
+
dataset: StockListType;
|
|
5
|
+
ad: number;
|
|
6
|
+
preClose: number;
|
|
7
|
+
};
|
|
8
|
+
interface AdType {
|
|
9
|
+
init: (data: NewStockType) => AdResType;
|
|
10
|
+
next: (data: NewStockType, preList: AdResType) => AdResType;
|
|
11
|
+
}
|
|
12
|
+
export default class Ad implements AdType {
|
|
13
|
+
init(data: NewStockType): AdResType;
|
|
14
|
+
next(data: NewStockType, preList: AdResType): AdResType;
|
|
15
|
+
private calcAD;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
3
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
4
|
+
if (ar || !(i in from)) {
|
|
5
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
6
|
+
ar[i] = from[i];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
var Ad = /** @class */ (function () {
|
|
13
|
+
function Ad() {
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(Ad.prototype, "init", {
|
|
16
|
+
enumerable: false,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: function (data) {
|
|
20
|
+
// AD 初始值
|
|
21
|
+
var ad = this.calcAD(data);
|
|
22
|
+
return {
|
|
23
|
+
dataset: [data],
|
|
24
|
+
ad: ad,
|
|
25
|
+
preClose: data.c,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(Ad.prototype, "next", {
|
|
30
|
+
enumerable: false,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: function (data, preList) {
|
|
34
|
+
// AD 累加
|
|
35
|
+
var ad = preList.ad + this.calcAD(data);
|
|
36
|
+
return {
|
|
37
|
+
dataset: __spreadArray(__spreadArray([], preList.dataset, true), [data], false),
|
|
38
|
+
ad: ad,
|
|
39
|
+
preClose: data.c,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// AD 計算公式
|
|
44
|
+
Object.defineProperty(Ad.prototype, "calcAD", {
|
|
45
|
+
enumerable: false,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: function (data) {
|
|
49
|
+
var h = data.h, l = data.l, c = data.c, v = data.v;
|
|
50
|
+
if (h === l)
|
|
51
|
+
return 0; // 避免除以零
|
|
52
|
+
return ((c - l - (h - c)) / (h - l)) * v;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return Ad;
|
|
56
|
+
}());
|
|
57
|
+
exports.default = Ad;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StockListType, StockType } from "./types";
|
|
2
|
+
export type MfiResType = {
|
|
3
|
+
dataset: StockListType;
|
|
4
|
+
mfi: number | null;
|
|
5
|
+
type: number;
|
|
6
|
+
sumPositiveMF: number;
|
|
7
|
+
sumNegativeMF: number;
|
|
8
|
+
};
|
|
9
|
+
export default class Mfi {
|
|
10
|
+
private getTypicalPrice;
|
|
11
|
+
private getRawMoneyFlow;
|
|
12
|
+
init(data: StockType, type: number): MfiResType;
|
|
13
|
+
next(data: StockType, preList: MfiResType, type: number): MfiResType;
|
|
14
|
+
calculateMFI(prices: StockListType, period?: number): number[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
var Mfi = /** @class */ (function () {
|
|
15
|
+
function Mfi() {
|
|
16
|
+
}
|
|
17
|
+
// 計算典型價格 (Typical Price)
|
|
18
|
+
Object.defineProperty(Mfi.prototype, "getTypicalPrice", {
|
|
19
|
+
enumerable: false,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: function (data) {
|
|
23
|
+
return (data.h + data.l + data.c) / 3;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// 計算原始資金流量 (Raw Money Flow)
|
|
27
|
+
Object.defineProperty(Mfi.prototype, "getRawMoneyFlow", {
|
|
28
|
+
enumerable: false,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: function (data) {
|
|
32
|
+
return this.getTypicalPrice(data) * data.v;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(Mfi.prototype, "init", {
|
|
36
|
+
enumerable: false,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: function (data, type) {
|
|
40
|
+
return {
|
|
41
|
+
dataset: [data],
|
|
42
|
+
mfi: null,
|
|
43
|
+
type: type,
|
|
44
|
+
sumPositiveMF: 0,
|
|
45
|
+
sumNegativeMF: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(Mfi.prototype, "next", {
|
|
50
|
+
enumerable: false,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: function (data, preList, type) {
|
|
54
|
+
preList.dataset.push(data);
|
|
55
|
+
// 資料不足,不計算 MFI
|
|
56
|
+
if (preList.dataset.length < type + 1) {
|
|
57
|
+
return __assign(__assign({}, preList), { mfi: null, type: type, sumPositiveMF: 0, sumNegativeMF: 0 });
|
|
58
|
+
}
|
|
59
|
+
var sumPositiveMF = preList.sumPositiveMF;
|
|
60
|
+
var sumNegativeMF = preList.sumNegativeMF;
|
|
61
|
+
if (preList.dataset.length === type + 1) {
|
|
62
|
+
// 第一次達到計算條件,初始化計算
|
|
63
|
+
sumPositiveMF = 0;
|
|
64
|
+
sumNegativeMF = 0;
|
|
65
|
+
for (var i = 1; i <= type; i++) {
|
|
66
|
+
var currentTP = this.getTypicalPrice(preList.dataset[i]);
|
|
67
|
+
var prevTP = this.getTypicalPrice(preList.dataset[i - 1]);
|
|
68
|
+
var rawMF = this.getRawMoneyFlow(preList.dataset[i]);
|
|
69
|
+
if (currentTP > prevTP) {
|
|
70
|
+
sumPositiveMF += rawMF;
|
|
71
|
+
}
|
|
72
|
+
else if (currentTP < prevTP) {
|
|
73
|
+
sumNegativeMF += rawMF;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// 增量更新:移除最舊的影響,加上最新的影響
|
|
79
|
+
// 移除最舊的資料影響
|
|
80
|
+
var oldestTP = this.getTypicalPrice(preList.dataset[0]);
|
|
81
|
+
var secondOldestTP = this.getTypicalPrice(preList.dataset[1]);
|
|
82
|
+
var oldestRawMF = this.getRawMoneyFlow(preList.dataset[1]);
|
|
83
|
+
if (secondOldestTP > oldestTP) {
|
|
84
|
+
sumPositiveMF -= oldestRawMF;
|
|
85
|
+
}
|
|
86
|
+
else if (secondOldestTP < oldestTP) {
|
|
87
|
+
sumNegativeMF -= oldestRawMF;
|
|
88
|
+
}
|
|
89
|
+
// 移除最舊的資料
|
|
90
|
+
preList.dataset.shift();
|
|
91
|
+
// 加上最新的資料影響
|
|
92
|
+
var newTP = this.getTypicalPrice(preList.dataset[preList.dataset.length - 1]);
|
|
93
|
+
var prevTP = this.getTypicalPrice(preList.dataset[preList.dataset.length - 2]);
|
|
94
|
+
var newRawMF = this.getRawMoneyFlow(preList.dataset[preList.dataset.length - 1]);
|
|
95
|
+
if (newTP > prevTP) {
|
|
96
|
+
sumPositiveMF += newRawMF;
|
|
97
|
+
}
|
|
98
|
+
else if (newTP < prevTP) {
|
|
99
|
+
sumNegativeMF += newRawMF;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 計算 MFI
|
|
103
|
+
var mfi;
|
|
104
|
+
if (sumNegativeMF === 0) {
|
|
105
|
+
mfi = 100;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
var moneyFlowRatio = sumPositiveMF / sumNegativeMF;
|
|
109
|
+
mfi = 100 - (100 / (1 + moneyFlowRatio));
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
dataset: preList.dataset,
|
|
113
|
+
type: type,
|
|
114
|
+
mfi: mfi,
|
|
115
|
+
sumPositiveMF: sumPositiveMF,
|
|
116
|
+
sumNegativeMF: sumNegativeMF
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
Object.defineProperty(Mfi.prototype, "calculateMFI", {
|
|
121
|
+
enumerable: false,
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
value: function (prices, period) {
|
|
125
|
+
if (period === void 0) { period = 14; }
|
|
126
|
+
if (prices.length < period + 1) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
var mfis = [];
|
|
130
|
+
// 從 period 開始,確保有足夠的資料進行比較
|
|
131
|
+
for (var endIdx = period; endIdx < prices.length; endIdx++) {
|
|
132
|
+
var sumPositiveMF = 0;
|
|
133
|
+
var sumNegativeMF = 0;
|
|
134
|
+
// 計算 period 個交易日的資金流量
|
|
135
|
+
// 從 endIdx - period + 1 到 endIdx,共 period 個資料點
|
|
136
|
+
// 但比較是從 endIdx - period + 2 開始(因為要跟前一天比)
|
|
137
|
+
for (var i = endIdx - period + 1; i <= endIdx; i++) {
|
|
138
|
+
// 確保 i-1 >= 0,避免溢位
|
|
139
|
+
if (i === 0)
|
|
140
|
+
continue;
|
|
141
|
+
var currentTP = this.getTypicalPrice(prices[i]);
|
|
142
|
+
var prevTP = this.getTypicalPrice(prices[i - 1]);
|
|
143
|
+
var rawMF = this.getRawMoneyFlow(prices[i]);
|
|
144
|
+
if (currentTP > prevTP) {
|
|
145
|
+
sumPositiveMF += rawMF;
|
|
146
|
+
}
|
|
147
|
+
else if (currentTP < prevTP) {
|
|
148
|
+
sumNegativeMF += rawMF;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 計算 MFI
|
|
152
|
+
if (sumNegativeMF === 0) {
|
|
153
|
+
mfis.push(100);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
var moneyFlowRatio = sumPositiveMF / sumNegativeMF;
|
|
157
|
+
var mfi = 100 - (100 / (1 + moneyFlowRatio));
|
|
158
|
+
mfis.push(mfi);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return mfis;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return Mfi;
|
|
165
|
+
}());
|
|
166
|
+
exports.default = Mfi;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
var mfi_1 = require("./mfi");
|
|
4
|
+
describe('MFI (Money Flow Index) Algorithm', function () {
|
|
5
|
+
var mfi;
|
|
6
|
+
beforeEach(function () {
|
|
7
|
+
mfi = new mfi_1.default();
|
|
8
|
+
});
|
|
9
|
+
// 測試用的股票資料
|
|
10
|
+
var createStockData = function (high, low, close, volume) { return ({
|
|
11
|
+
h: high,
|
|
12
|
+
l: low,
|
|
13
|
+
c: close,
|
|
14
|
+
v: volume,
|
|
15
|
+
o: close,
|
|
16
|
+
t: Date.now()
|
|
17
|
+
}); };
|
|
18
|
+
describe('init method', function () {
|
|
19
|
+
it('should initialize with correct default values', function () {
|
|
20
|
+
var stockData = createStockData(100, 95, 98, 1000);
|
|
21
|
+
var result = mfi.init(stockData, 14);
|
|
22
|
+
expect(result).toEqual({
|
|
23
|
+
dataset: [stockData],
|
|
24
|
+
mfi: null,
|
|
25
|
+
type: 14,
|
|
26
|
+
sumPositiveMF: 0,
|
|
27
|
+
sumNegativeMF: 0,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it('should work with different period types', function () {
|
|
31
|
+
var stockData = createStockData(100, 95, 98, 1000);
|
|
32
|
+
var result = mfi.init(stockData, 5);
|
|
33
|
+
expect(result.type).toBe(5);
|
|
34
|
+
expect(result.dataset).toHaveLength(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('next method - insufficient data', function () {
|
|
38
|
+
it('should return null MFI when data is insufficient', function () {
|
|
39
|
+
var result = mfi.init(createStockData(100, 95, 98, 1000), 3);
|
|
40
|
+
// 第二個資料點
|
|
41
|
+
result = mfi.next(createStockData(102, 97, 100, 1200), result, 3);
|
|
42
|
+
expect(result.mfi).toBeNull();
|
|
43
|
+
expect(result.dataset).toHaveLength(2);
|
|
44
|
+
// 第三個資料點
|
|
45
|
+
result = mfi.next(createStockData(105, 99, 103, 1500), result, 3);
|
|
46
|
+
expect(result.mfi).toBeNull();
|
|
47
|
+
expect(result.dataset).toHaveLength(3);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('next method - sufficient data', function () {
|
|
51
|
+
it('should calculate MFI correctly when reaching minimum period', function () {
|
|
52
|
+
var period = 3;
|
|
53
|
+
var result = mfi.init(createStockData(100, 95, 98, 1000), period);
|
|
54
|
+
// 添加資料直到滿足計算條件
|
|
55
|
+
result = mfi.next(createStockData(102, 97, 100, 1200), result, period); // TP上升
|
|
56
|
+
result = mfi.next(createStockData(105, 99, 103, 1500), result, period); // TP上升
|
|
57
|
+
result = mfi.next(createStockData(103, 98, 101, 1300), result, period); // TP下降
|
|
58
|
+
expect(result.mfi).not.toBeNull();
|
|
59
|
+
expect(typeof result.mfi).toBe('number');
|
|
60
|
+
expect(result.mfi).toBeGreaterThanOrEqual(0);
|
|
61
|
+
expect(result.mfi).toBeLessThanOrEqual(100);
|
|
62
|
+
expect(result.dataset).toHaveLength(period + 1);
|
|
63
|
+
});
|
|
64
|
+
it('should handle all positive money flows (MFI = 100)', function () {
|
|
65
|
+
var period = 2;
|
|
66
|
+
var result = mfi.init(createStockData(100, 95, 98, 1000), period);
|
|
67
|
+
// 所有後續價格都上升
|
|
68
|
+
result = mfi.next(createStockData(102, 97, 100, 1200), result, period);
|
|
69
|
+
result = mfi.next(createStockData(105, 99, 103, 1500), result, period);
|
|
70
|
+
expect(result.mfi).toBe(100);
|
|
71
|
+
});
|
|
72
|
+
it('should maintain sliding window correctly', function () {
|
|
73
|
+
var period = 2;
|
|
74
|
+
var result = mfi.init(createStockData(100, 95, 98, 1000), period);
|
|
75
|
+
result = mfi.next(createStockData(102, 97, 100, 1200), result, period);
|
|
76
|
+
result = mfi.next(createStockData(105, 99, 103, 1500), result, period);
|
|
77
|
+
// 新增第五個資料點,應該移除第一個
|
|
78
|
+
var oldFirstData = result.dataset[0];
|
|
79
|
+
result = mfi.next(createStockData(103, 98, 101, 1300), result, period);
|
|
80
|
+
expect(result.dataset).toHaveLength(period + 1);
|
|
81
|
+
expect(result.dataset[0]).not.toEqual(oldFirstData);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('calculateMFI method', function () {
|
|
85
|
+
it('should return empty array for insufficient data', function () {
|
|
86
|
+
var prices = [
|
|
87
|
+
createStockData(100, 95, 98, 1000),
|
|
88
|
+
createStockData(102, 97, 100, 1200),
|
|
89
|
+
];
|
|
90
|
+
var result = mfi.calculateMFI(prices, 14);
|
|
91
|
+
expect(result).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
it('should calculate MFI for sufficient data', function () {
|
|
94
|
+
// 創建測試資料:期間為3,需要4個資料點
|
|
95
|
+
var prices = [
|
|
96
|
+
createStockData(100, 95, 98, 1000),
|
|
97
|
+
createStockData(102, 97, 100, 1200),
|
|
98
|
+
createStockData(105, 99, 103, 1500),
|
|
99
|
+
createStockData(103, 98, 101, 1300), // TP = 100.67 (下降)
|
|
100
|
+
];
|
|
101
|
+
var result = mfi.calculateMFI(prices, 3);
|
|
102
|
+
expect(result).toHaveLength(1); // 只能計算一個MFI值
|
|
103
|
+
expect(result[0]).toBeGreaterThanOrEqual(0);
|
|
104
|
+
expect(result[0]).toBeLessThanOrEqual(100);
|
|
105
|
+
});
|
|
106
|
+
it('should calculate multiple MFI values for longer data series', function () {
|
|
107
|
+
var prices = [];
|
|
108
|
+
// 創建6個資料點,期間為3,應該能計算3個MFI值
|
|
109
|
+
for (var i = 0; i < 6; i++) {
|
|
110
|
+
prices.push(createStockData(100 + i * 2, 95 + i * 2, 98 + i * 2 + (i % 2 === 0 ? 1 : -1), // 交替上下
|
|
111
|
+
1000 + i * 100));
|
|
112
|
+
}
|
|
113
|
+
var result = mfi.calculateMFI(prices, 3);
|
|
114
|
+
expect(result).toHaveLength(3); // 6 - 3 = 3個MFI值
|
|
115
|
+
result.forEach(function (mfiValue) {
|
|
116
|
+
expect(mfiValue).toBeGreaterThanOrEqual(0);
|
|
117
|
+
expect(mfiValue).toBeLessThanOrEqual(100);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
it('should handle edge case with no negative money flow', function () {
|
|
121
|
+
// 創建持續上升的價格資料
|
|
122
|
+
var prices = [];
|
|
123
|
+
for (var i = 0; i < 5; i++) {
|
|
124
|
+
prices.push(createStockData(100 + i * 5, 95 + i * 5, 98 + i * 5, 1000));
|
|
125
|
+
}
|
|
126
|
+
var result = mfi.calculateMFI(prices, 3);
|
|
127
|
+
expect(result).toHaveLength(2);
|
|
128
|
+
result.forEach(function (mfiValue) {
|
|
129
|
+
expect(mfiValue).toBe(100); // 全部正向資金流量
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
it('should handle equal typical prices correctly', function () {
|
|
133
|
+
var prices = [
|
|
134
|
+
createStockData(100, 95, 98, 1000),
|
|
135
|
+
createStockData(102, 97, 99, 1200),
|
|
136
|
+
createStockData(101, 96, 99, 1500),
|
|
137
|
+
createStockData(103, 98, 100, 1300),
|
|
138
|
+
createStockData(102, 97, 99, 1100), // TP = 99.33 (相等)
|
|
139
|
+
];
|
|
140
|
+
var result = mfi.calculateMFI(prices, 3);
|
|
141
|
+
expect(result).toHaveLength(2);
|
|
142
|
+
result.forEach(function (mfiValue) {
|
|
143
|
+
expect(mfiValue).toBeGreaterThanOrEqual(0);
|
|
144
|
+
expect(mfiValue).toBeLessThanOrEqual(100);
|
|
145
|
+
expect(Number.isNaN(mfiValue)).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('typical price calculation', function () {
|
|
150
|
+
it('should calculate typical price correctly', function () {
|
|
151
|
+
var stockData = createStockData(105, 95, 100, 1000);
|
|
152
|
+
var mfiInstance = new mfi_1.default();
|
|
153
|
+
// 使用反射來測試私有方法(僅用於測試)
|
|
154
|
+
var typicalPrice = mfiInstance.getTypicalPrice(stockData);
|
|
155
|
+
expect(typicalPrice).toBeCloseTo((105 + 95 + 100) / 3, 2);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('raw money flow calculation', function () {
|
|
159
|
+
it('should calculate raw money flow correctly', function () {
|
|
160
|
+
var stockData = createStockData(105, 95, 100, 1500);
|
|
161
|
+
var mfiInstance = new mfi_1.default();
|
|
162
|
+
var typicalPrice = (105 + 95 + 100) / 3;
|
|
163
|
+
var expectedMF = typicalPrice * 1500;
|
|
164
|
+
var rawMF = mfiInstance.getRawMoneyFlow(stockData);
|
|
165
|
+
expect(rawMF).toBeCloseTo(expectedMF, 2);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('consistency between methods', function () {
|
|
169
|
+
it('should produce same results using next() and calculateMFI()', function () {
|
|
170
|
+
var prices = [];
|
|
171
|
+
for (var i = 0; i < 8; i++) {
|
|
172
|
+
prices.push(createStockData(100 + i * 3, 95 + i * 2, 98 + i * 2.5, 1000 + i * 50));
|
|
173
|
+
}
|
|
174
|
+
var period = 3;
|
|
175
|
+
// 使用 calculateMFI
|
|
176
|
+
var batchResult = mfi.calculateMFI(prices, period);
|
|
177
|
+
// 使用 next 方法逐步計算
|
|
178
|
+
var result = mfi.init(prices[0], period);
|
|
179
|
+
var nextResults = [];
|
|
180
|
+
for (var i = 1; i < prices.length; i++) {
|
|
181
|
+
result = mfi.next(prices[i], result, period);
|
|
182
|
+
nextResults.push(result.mfi);
|
|
183
|
+
}
|
|
184
|
+
// 過濾掉 null 值
|
|
185
|
+
var validNextResults = nextResults.filter(function (val) { return val !== null; });
|
|
186
|
+
expect(validNextResults).toHaveLength(batchResult.length);
|
|
187
|
+
// 比較結果(允許小的浮點誤差)
|
|
188
|
+
for (var i = 0; i < batchResult.length; i++) {
|
|
189
|
+
expect(validNextResults[i]).toBeCloseTo(batchResult[i], 6);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { default as Week } from "./stockSkills/week.js";
|
|
|
19
19
|
export { default as Vma } from "./stockSkills/vma.js";
|
|
20
20
|
export { default as Williams } from "./stockSkills/williams.js";
|
|
21
21
|
export { default as ObvEma } from "./stockSkills/obv_ema.js";
|
|
22
|
+
export { default as Mfi } from "./stockSkills/mfi.js";
|
|
23
|
+
export { default as Ad } from "./stockSkills/ad.js";
|
|
22
24
|
export { add } from "./test/add.js";
|
|
23
25
|
export { minus } from "./test/minus.js";
|
|
24
26
|
export { default as calculateDivisionFactor } from "./utils/calculateDivisionFactor.js";
|
package/dist/esm/index.js
CHANGED
|
@@ -22,6 +22,8 @@ export { default as Week } from "./stockSkills/week.js";
|
|
|
22
22
|
export { default as Vma } from "./stockSkills/vma.js";
|
|
23
23
|
export { default as Williams } from "./stockSkills/williams.js";
|
|
24
24
|
export { default as ObvEma } from "./stockSkills/obv_ema.js";
|
|
25
|
+
export { default as Mfi } from "./stockSkills/mfi.js";
|
|
26
|
+
export { default as Ad } from "./stockSkills/ad.js";
|
|
25
27
|
export { add } from "./test/add.js";
|
|
26
28
|
export { minus } from "./test/minus.js";
|
|
27
29
|
export { default as calculateDivisionFactor } from "./utils/calculateDivisionFactor.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StockListType, StockType } from "./types";
|
|
2
|
+
type NewStockType = Required<Pick<StockType, "v">> & StockType;
|
|
3
|
+
export type AdResType = {
|
|
4
|
+
dataset: StockListType;
|
|
5
|
+
ad: number;
|
|
6
|
+
preClose: number;
|
|
7
|
+
};
|
|
8
|
+
interface AdType {
|
|
9
|
+
init: (data: NewStockType) => AdResType;
|
|
10
|
+
next: (data: NewStockType, preList: AdResType) => AdResType;
|
|
11
|
+
}
|
|
12
|
+
export default class Ad implements AdType {
|
|
13
|
+
init(data: NewStockType): AdResType;
|
|
14
|
+
next(data: NewStockType, preList: AdResType): AdResType;
|
|
15
|
+
private calcAD;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
2
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
3
|
+
if (ar || !(i in from)) {
|
|
4
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
5
|
+
ar[i] = from[i];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
9
|
+
};
|
|
10
|
+
var Ad = /** @class */ (function () {
|
|
11
|
+
function Ad() {
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(Ad.prototype, "init", {
|
|
14
|
+
enumerable: false,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: function (data) {
|
|
18
|
+
// AD 初始值
|
|
19
|
+
var ad = this.calcAD(data);
|
|
20
|
+
return {
|
|
21
|
+
dataset: [data],
|
|
22
|
+
ad: ad,
|
|
23
|
+
preClose: data.c,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(Ad.prototype, "next", {
|
|
28
|
+
enumerable: false,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: function (data, preList) {
|
|
32
|
+
// AD 累加
|
|
33
|
+
var ad = preList.ad + this.calcAD(data);
|
|
34
|
+
return {
|
|
35
|
+
dataset: __spreadArray(__spreadArray([], preList.dataset, true), [data], false),
|
|
36
|
+
ad: ad,
|
|
37
|
+
preClose: data.c,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// AD 計算公式
|
|
42
|
+
Object.defineProperty(Ad.prototype, "calcAD", {
|
|
43
|
+
enumerable: false,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: function (data) {
|
|
47
|
+
var h = data.h, l = data.l, c = data.c, v = data.v;
|
|
48
|
+
if (h === l)
|
|
49
|
+
return 0; // 避免除以零
|
|
50
|
+
return ((c - l - (h - c)) / (h - l)) * v;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return Ad;
|
|
54
|
+
}());
|
|
55
|
+
export default Ad;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StockListType, StockType } from "./types";
|
|
2
|
+
export type MfiResType = {
|
|
3
|
+
dataset: StockListType;
|
|
4
|
+
mfi: number | null;
|
|
5
|
+
type: number;
|
|
6
|
+
sumPositiveMF: number;
|
|
7
|
+
sumNegativeMF: number;
|
|
8
|
+
};
|
|
9
|
+
export default class Mfi {
|
|
10
|
+
private getTypicalPrice;
|
|
11
|
+
private getRawMoneyFlow;
|
|
12
|
+
init(data: StockType, type: number): MfiResType;
|
|
13
|
+
next(data: StockType, preList: MfiResType, type: number): MfiResType;
|
|
14
|
+
calculateMFI(prices: StockListType, period?: number): number[];
|
|
15
|
+
}
|