lit-forge-mcp 0.3.0 → 0.4.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.
Files changed (84) hide show
  1. package/README.md +16 -6
  2. package/dist/lib/__tests__/correlation.test.d.ts +1 -0
  3. package/dist/lib/__tests__/correlation.test.js +53 -0
  4. package/dist/lib/__tests__/correlation.test.js.map +1 -0
  5. package/dist/lib/__tests__/quartile.test.d.ts +1 -0
  6. package/dist/lib/__tests__/quartile.test.js +148 -0
  7. package/dist/lib/__tests__/quartile.test.js.map +1 -0
  8. package/dist/lib/__tests__/rankings.test.d.ts +1 -0
  9. package/dist/lib/__tests__/rankings.test.js +67 -0
  10. package/dist/lib/__tests__/rankings.test.js.map +1 -0
  11. package/dist/lib/__tests__/sessions.test.d.ts +1 -0
  12. package/dist/lib/__tests__/sessions.test.js +47 -0
  13. package/dist/lib/__tests__/sessions.test.js.map +1 -0
  14. package/dist/lib/__tests__/sma.test.d.ts +1 -0
  15. package/dist/lib/__tests__/sma.test.js +26 -0
  16. package/dist/lib/__tests__/sma.test.js.map +1 -0
  17. package/dist/lib/__tests__/thermometer.test.d.ts +1 -0
  18. package/dist/lib/__tests__/thermometer.test.js +120 -0
  19. package/dist/lib/__tests__/thermometer.test.js.map +1 -0
  20. package/dist/lib/__tests__/yield-spread.test.d.ts +1 -0
  21. package/dist/lib/__tests__/yield-spread.test.js +46 -0
  22. package/dist/lib/__tests__/yield-spread.test.js.map +1 -0
  23. package/dist/lib/correlation.d.ts +3 -0
  24. package/dist/lib/correlation.js +77 -0
  25. package/dist/lib/correlation.js.map +1 -0
  26. package/dist/lib/fangplus.d.ts +7 -0
  27. package/dist/lib/fangplus.js +19 -0
  28. package/dist/lib/fangplus.js.map +1 -0
  29. package/dist/lib/indicators.d.ts +1 -0
  30. package/dist/lib/indicators.js +36 -1
  31. package/dist/lib/indicators.js.map +1 -1
  32. package/dist/lib/mag7.d.ts +7 -0
  33. package/dist/lib/mag7.js +13 -0
  34. package/dist/lib/mag7.js.map +1 -0
  35. package/dist/lib/market-types.d.ts +22 -6
  36. package/dist/lib/market-types.js +1 -0
  37. package/dist/lib/market-types.js.map +1 -1
  38. package/dist/lib/performance.d.ts +3 -0
  39. package/dist/lib/performance.js +43 -0
  40. package/dist/lib/performance.js.map +1 -0
  41. package/dist/lib/quartile.d.ts +24 -0
  42. package/dist/lib/quartile.js +128 -0
  43. package/dist/lib/quartile.js.map +1 -0
  44. package/dist/lib/rankings.d.ts +7 -0
  45. package/dist/lib/rankings.js +29 -0
  46. package/dist/lib/rankings.js.map +1 -0
  47. package/dist/lib/sectors.d.ts +7 -0
  48. package/dist/lib/sectors.js +62 -0
  49. package/dist/lib/sectors.js.map +1 -0
  50. package/dist/lib/sessions.d.ts +14 -0
  51. package/dist/lib/sessions.js +50 -0
  52. package/dist/lib/sessions.js.map +1 -0
  53. package/dist/lib/sma.d.ts +2 -0
  54. package/dist/lib/sma.js +33 -0
  55. package/dist/lib/sma.js.map +1 -0
  56. package/dist/lib/thermometer.d.ts +13 -0
  57. package/dist/lib/thermometer.js +118 -0
  58. package/dist/lib/thermometer.js.map +1 -0
  59. package/dist/lib/yahoo.d.ts +2 -0
  60. package/dist/lib/yahoo.js +76 -15
  61. package/dist/lib/yahoo.js.map +1 -1
  62. package/dist/lib/yield-spread.d.ts +6 -0
  63. package/dist/lib/yield-spread.js +20 -0
  64. package/dist/lib/yield-spread.js.map +1 -0
  65. package/dist/tools/get-market-sessions.d.ts +2 -0
  66. package/dist/tools/get-market-sessions.js +27 -0
  67. package/dist/tools/get-market-sessions.js.map +1 -0
  68. package/dist/tools/get-market-snapshot.js +6 -4
  69. package/dist/tools/get-market-snapshot.js.map +1 -1
  70. package/dist/tools/get-market-thermometer.d.ts +2 -0
  71. package/dist/tools/get-market-thermometer.js +47 -0
  72. package/dist/tools/get-market-thermometer.js.map +1 -0
  73. package/dist/tools/get-performance-ranking.d.ts +2 -0
  74. package/dist/tools/get-performance-ranking.js +75 -0
  75. package/dist/tools/get-performance-ranking.js.map +1 -0
  76. package/dist/tools/get-sector-heatmap.d.ts +2 -0
  77. package/dist/tools/get-sector-heatmap.js +34 -0
  78. package/dist/tools/get-sector-heatmap.js.map +1 -0
  79. package/dist/tools/get-yield-spread.d.ts +2 -0
  80. package/dist/tools/get-yield-spread.js +41 -0
  81. package/dist/tools/get-yield-spread.js.map +1 -0
  82. package/dist/tools/index.js +13 -0
  83. package/dist/tools/index.js.map +1 -1
  84. package/package.json +5 -2
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  Claude Desktop / Claude Code / Cursor など、MCP に対応した任意の AI クライアントで動作します。
10
10
 
11
- > **v0.2.0 で「金融・個人投資家特化」にピボット**、**v0.3.0 で「毎朝の市況チェック」系 3 ツールを追加**しました。
11
+ > **v0.2.0 で「金融・個人投資家特化」にピボット**、**v0.3.0 で「毎朝の市況チェック」系 3 ツールを追加**、**v0.4.0 で銘柄数 9 → 28 に拡大 + 5 つの分析ツール追加**しました。
12
12
 
13
- ## 提供ツール(7 種)
13
+ ## 提供ツール(12 種)
14
14
 
15
15
  ### 個人資産形成プランナー(純関数、外部 API 不要)
16
16
 
@@ -21,17 +21,27 @@ Claude Desktop / Claude Code / Cursor など、MCP に対応した任意の AI
21
21
  | `calculate_required_monthly` | 目標金額・現在の貯蓄・年利・年数から、達成に必要な毎月の積立額を逆算 |
22
22
  | `calculate_compound_interest` | 元本(一括)と月次積立を月次複利で評価する汎用複利計算ツール |
23
23
 
24
- ### 市況・経済イベント(v0.3.0 新規。**HTTP 通信あり**)
24
+ ### 市況・経済イベント(**HTTP 通信あり**)
25
25
 
26
26
  | ツール名 | 説明 |
27
27
  |---|---|
28
- | `get_market_snapshot` | USD/JPY・EUR/JPY・日経平均・TOPIX・S&P 500・NASDAQ 100・米10年金利・金・原油 WTI の現在値・前日比を一括取得([lit-forge.com/today](https://lit-forge.com/today) と同等情報) |
28
+ | `get_market_snapshot` | USD/JPY・EUR/JPY・GBP/JPY・AUD/JPY・EUR/USD・CHF/JPY・ドル指数・日経平均・TOPIX・NY ダウ・S&P 500・NASDAQ・VIX・NYSE FANG+・SOX・DAX・FTSE・上海総合・ハンセン・KOSPI・SENSEX・米10年/5年金利・金・原油・銅・ビットコイン・イーサリアム の主要 28 指標を一括取得([lit-forge.com/today](https://lit-forge.com/today) と同等) |
29
29
  | `get_economic_events_today` | 当日 or 今週の経済イベント(FOMC・日銀金融政策決定会合・米雇用統計・CPI・GDP・中国 PMI 等)を重要度付きで返す。半年分を手動キュレーション |
30
30
  | `get_quote` | 任意の Yahoo Finance ティッカー(株・為替・指数・コモディティ・暗号資産)の現在値・前日比を取得。例: `AAPL` / `^DJI` / `BTC-USD` |
31
31
 
32
- > ⚠ **HTTP 通信について**: v0.3.0 から、`get_market_snapshot` / `get_quote` は **Yahoo Finance API(query1.finance.yahoo.com)** へ HTTPS リクエストを送信します。実行 PC のネットワークから外部に出る通信が発生する点にご留意ください。データは Yahoo Finance より取得(約 1 時間遅れ)、**投資助言ではなく情報集約**として提供しています。
32
+ ### 分析ツール(v0.4.0 新規)
33
33
 
34
- Claude / GPT / Cursor との対話の中で「老後資金大丈夫?」「月いくら積み立てれば?」「今日の市況を要約して」「FOMC は今週いつ?」を即座に試算・確認できます。
34
+ | ツール名 | 説明 |
35
+ |---|---|
36
+ | `get_market_thermometer` | VIX・S&P 500・米10年金利・ドル指数を合成した 0-100 のリスクオン/オフ・スコア + 過去 30 営業日推移 |
37
+ | `get_performance_ranking` | 28 銘柄を `1d`/`1w`/`1m` のパフォーマンスでソート、上位/下位 N 件を返す |
38
+ | `get_yield_spread` | 米10年-5年イールドスプレッド(プラス=順イールド / マイナス=逆イールド) |
39
+ | `get_market_sessions` | 主要 4 市場(東京・上海・ロンドン・NY)の現在の取引時間ステータス |
40
+ | `get_sector_heatmap` | 米株セクター ETF(SPDR、11 セクター)の前日比一覧 |
41
+
42
+ > ⚠ **HTTP 通信について**: 市況・分析系ツールは **Yahoo Finance API(query1.finance.yahoo.com)** へ HTTPS リクエストを送信します。実行 PC のネットワークから外部に出る通信が発生する点にご留意ください。データは約 1 時間遅れの参考値で、**投資助言ではなく情報集約**として提供しています。
43
+
44
+ Claude / GPT / Cursor との対話の中で「老後資金大丈夫?」「月いくら積み立てれば?」「今日の市況を要約して」「FOMC は今週いつ?」「マーケット温度計は?」「セクターでどこが強い?」を即座に試算・確認できます。
35
45
 
36
46
  ## インストール / 設定
37
47
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { correlationMatrix, pearson, tailReturns, } from "../correlation.js";
3
+ describe("tailReturns", () => {
4
+ it("末尾 31 件から 30 件のリターンを返す", () => {
5
+ const closes = Array.from({ length: 50 }, (_, i) => 100 + i);
6
+ const r = tailReturns(closes, 30);
7
+ expect(r.length).toBe(30);
8
+ });
9
+ it("データ不足は空配列", () => {
10
+ expect(tailReturns([100], 30)).toEqual([]);
11
+ expect(tailReturns([], 30)).toEqual([]);
12
+ });
13
+ it("分母が 0(直前の close=0)の点を除外", () => {
14
+ const r = tailReturns([100, 0, 50, 75], 10);
15
+ // 100→0 (-100%)、0→50 は分母 0 で除外、50→75 (+50%) → 2 要素
16
+ expect(r.length).toBe(2);
17
+ });
18
+ });
19
+ describe("pearson", () => {
20
+ it("完全に相関する系列は 1", () => {
21
+ const a = Array.from({ length: 30 }, (_, i) => i / 100);
22
+ const b = Array.from({ length: 30 }, (_, i) => i / 200);
23
+ expect(pearson(a, b)).toBeCloseTo(1, 5);
24
+ });
25
+ it("完全に逆相関する系列は -1", () => {
26
+ const a = Array.from({ length: 30 }, (_, i) => i / 100);
27
+ const b = Array.from({ length: 30 }, (_, i) => -i / 100);
28
+ expect(pearson(a, b)).toBeCloseTo(-1, 5);
29
+ });
30
+ it("点数不足は null", () => {
31
+ expect(pearson([1, 2], [3, 4])).toBeNull();
32
+ });
33
+ it("片方が定数(std=0)は null", () => {
34
+ const a = Array.from({ length: 30 }, () => 0.01);
35
+ const b = Array.from({ length: 30 }, (_, i) => i / 100);
36
+ expect(pearson(a, b)).toBeNull();
37
+ });
38
+ });
39
+ describe("correlationMatrix", () => {
40
+ it("対角線は 1(データ十分時)", () => {
41
+ const closes = Array.from({ length: 50 }, (_, i) => 100 + i);
42
+ const m = correlationMatrix(["A", "B"], () => closes);
43
+ expect(m[0][0]).toBe(1);
44
+ expect(m[1][1]).toBe(1);
45
+ });
46
+ it("対称行列", () => {
47
+ const a = Array.from({ length: 50 }, (_, i) => 100 + i);
48
+ const b = Array.from({ length: 50 }, (_, i) => 200 - i);
49
+ const m = correlationMatrix(["A", "B"], (s) => (s === "A" ? a : b));
50
+ expect(m[0][1]).toBeCloseTo(m[1][0], 5);
51
+ });
52
+ });
53
+ //# sourceMappingURL=correlation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/correlation.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { detectLevelEvents, detectMilestone, detectOutlier, percentileLabel, pricePercentile, } from "../quartile.js";
3
+ function arithmetic(len, start, step) {
4
+ return Array.from({ length: len }, (_, i) => start + step * i);
5
+ }
6
+ describe("pricePercentile", () => {
7
+ it("単調増加配列の末尾は最高値(100)", () => {
8
+ const closes = arithmetic(252, 100, 0.1);
9
+ expect(pricePercentile(closes)).toBe(100);
10
+ });
11
+ it("単調減少配列の末尾は最安値(0)", () => {
12
+ const closes = arithmetic(252, 200, -0.1);
13
+ expect(pricePercentile(closes)).toBe(0);
14
+ });
15
+ it("中央値付近", () => {
16
+ // 0..100 の値、末尾を 50 にすると過半数が末尾以下なので約 50%
17
+ const closes = Array.from({ length: 250 }, (_, i) => i);
18
+ closes.push(50);
19
+ const p = pricePercentile(closes);
20
+ // 50 以下の数 = 51 (0..50 を含む) / 250 ≒ 20.4%
21
+ expect(p).toBeCloseTo(20.4, 1);
22
+ });
23
+ it("点数不足(< 30)は null", () => {
24
+ expect(pricePercentile([1, 2, 3])).toBeNull();
25
+ expect(pricePercentile([])).toBeNull();
26
+ });
27
+ });
28
+ describe("detectOutlier", () => {
29
+ it("等差で増加する穏やかな系列に対して 5% の動きは 1σ を大きく超える", () => {
30
+ // closes = [100, 100.1, 100.2, ..., 125]; daily ret ~ 0.1%, std ≈ 0
31
+ const closes = Array.from({ length: 252 }, (_, i) => 100 + i * 0.1);
32
+ const r = detectOutlier(closes, 5);
33
+ expect(r).not.toBeNull();
34
+ expect(r.exceeds1Sigma).toBe(true);
35
+ expect(r.sigma).toBeGreaterThan(1);
36
+ });
37
+ it("典型的な日次変動の範囲内なら 1σ を超えない", () => {
38
+ // 0.5% 標準偏差程度の系列を作る
39
+ const closes = [100];
40
+ for (let i = 1; i < 252; i++) {
41
+ // 単純な疑似乱数
42
+ const r = Math.sin(i * 1.7) * 0.5;
43
+ closes.push(closes[i - 1] * (1 + r / 100));
44
+ }
45
+ const result = detectOutlier(closes, 0.1);
46
+ expect(result).not.toBeNull();
47
+ expect(result.exceeds1Sigma).toBe(false);
48
+ });
49
+ it("点数不足は null", () => {
50
+ expect(detectOutlier([1, 2, 3], 1)).toBeNull();
51
+ });
52
+ it("全て同値(std=0)は null", () => {
53
+ const closes = Array.from({ length: 252 }, () => 100);
54
+ expect(detectOutlier(closes, 0)).toBeNull();
55
+ });
56
+ });
57
+ describe("detectLevelEvents", () => {
58
+ it("52 週新高値: 当日高値 >= 52w 高値で発火", () => {
59
+ const events = detectLevelEvents({
60
+ dayHigh: 5550,
61
+ fiftyTwoWeekHigh: 5500,
62
+ });
63
+ expect(events.find((e) => e.type === "52w-new-high")).toBeDefined();
64
+ });
65
+ it("52 週新安値: 当日安値 <= 52w 安値で発火", () => {
66
+ const events = detectLevelEvents({
67
+ dayLow: 4400,
68
+ fiftyTwoWeekLow: 4500,
69
+ });
70
+ expect(events.find((e) => e.type === "52w-new-low")).toBeDefined();
71
+ });
72
+ it("レベル系イベントは複数同時に出る", () => {
73
+ const events = detectLevelEvents({
74
+ dayHigh: 5550,
75
+ dayLow: 5520,
76
+ fiftyTwoWeekHigh: 5500,
77
+ fiftyTwoWeekLow: 4500,
78
+ });
79
+ expect(events.length).toBeGreaterThanOrEqual(1);
80
+ });
81
+ it("200 日線上抜け: 直近終値 > SMA200、前日 <= SMA200(-1)", () => {
82
+ // 200日 SMA を上抜けるパターン:前 200 日は 100 で平均、最終日に 200 へジャンプ
83
+ const closes = Array.from({ length: 200 }, () => 100);
84
+ closes.push(99); // prev: under
85
+ closes.push(200); // last: above
86
+ const events = detectLevelEvents({ closes1y: closes });
87
+ expect(events.find((e) => e.type === "ma200-cross-up")).toBeDefined();
88
+ });
89
+ it("200 日線下抜け", () => {
90
+ const closes = Array.from({ length: 200 }, () => 100);
91
+ closes.push(101); // prev: above
92
+ closes.push(50); // last: below
93
+ const events = detectLevelEvents({ closes1y: closes });
94
+ expect(events.find((e) => e.type === "ma200-cross-down")).toBeDefined();
95
+ });
96
+ it("データ不足では何も出ない", () => {
97
+ expect(detectLevelEvents({})).toEqual([]);
98
+ expect(detectLevelEvents({ closes1y: [1, 2, 3] })).toEqual([]);
99
+ });
100
+ });
101
+ describe("detectMilestone", () => {
102
+ it("当日値が直近 30 日の最大なら「30日ぶり高値」", () => {
103
+ const closes = Array.from({ length: 50 }, (_, i) => 100 + i * 0.1);
104
+ closes.push(200); // 末尾を大幅高
105
+ const m = detectMilestone(closes);
106
+ expect(m).not.toBeNull();
107
+ expect(m.type).toBe("n-day-high");
108
+ // 50 件しかないので 30 日ぶり判定が出る
109
+ expect(m.label).toContain("日ぶり高値");
110
+ });
111
+ it("当日値が 1 年(252 日)の最大なら「1年ぶり高値」", () => {
112
+ const closes = Array.from({ length: 300 }, (_, i) => 100 + i * 0.01);
113
+ closes.push(500);
114
+ const m = detectMilestone(closes);
115
+ expect(m?.label).toContain("1年ぶり高値");
116
+ });
117
+ it("どのウィンドウの高安にも届かなければ null", () => {
118
+ // sin 波で 100±5 の範囲に振動する系列。最後を 100(中央値)にする。
119
+ const closes = Array.from({ length: 300 }, (_, i) => 100 + Math.sin(i / 5) * 5);
120
+ closes.push(100);
121
+ expect(detectMilestone(closes)).toBeNull();
122
+ });
123
+ it("点数不足は null", () => {
124
+ expect(detectMilestone([1, 2, 3])).toBeNull();
125
+ });
126
+ });
127
+ describe("percentileLabel", () => {
128
+ it("90% 以上は「上位X%水準」", () => {
129
+ expect(percentileLabel(95)).toContain("上位5%水準");
130
+ expect(percentileLabel(100)).toContain("上位0%水準");
131
+ });
132
+ it("70-90% は「上位X%」", () => {
133
+ expect(percentileLabel(80)).toContain("上位20%");
134
+ });
135
+ it("30-70% は「中央値付近」", () => {
136
+ expect(percentileLabel(50)).toContain("中央値付近");
137
+ });
138
+ it("10-30% は「下位X%」", () => {
139
+ expect(percentileLabel(20)).toContain("下位20%");
140
+ });
141
+ it("10% 未満は「下位X%水準」", () => {
142
+ expect(percentileLabel(5)).toContain("下位5%水準");
143
+ });
144
+ it("null は空文字", () => {
145
+ expect(percentileLabel(null)).toBe("");
146
+ });
147
+ });
148
+ //# sourceMappingURL=quartile.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quartile.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/quartile.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { rankByPerformance } from "../rankings.js";
3
+ function ind(symbol, name) {
4
+ return {
5
+ symbol,
6
+ displayName: name,
7
+ category: "equity",
8
+ unit: "",
9
+ decimals: 2,
10
+ changeStyle: "ratio",
11
+ };
12
+ }
13
+ function q(symbol, changePercent, perf) {
14
+ return {
15
+ symbol,
16
+ price: 100,
17
+ previousClose: 100,
18
+ change: 0,
19
+ changePercent,
20
+ changeBp: 0,
21
+ fetchedAt: "2026-05-01T00:00:00.000Z",
22
+ sparkline: [100],
23
+ performance: perf
24
+ ? { d7: perf.d7 ?? null, d30: perf.d30 ?? null, d365: null }
25
+ : undefined,
26
+ };
27
+ }
28
+ describe("rankByPerformance", () => {
29
+ const indicators = [ind("A", "AAA"), ind("B", "BBB"), ind("C", "CCC")];
30
+ const quotes = {
31
+ A: q("A", 2.0, { d7: 5, d30: 10 }),
32
+ B: q("B", -1.0, { d7: -3, d30: 8 }),
33
+ C: q("C", 0.5, { d7: 7, d30: 2 }),
34
+ };
35
+ it("1d 期間で changePercent 降順", () => {
36
+ const r = rankByPerformance(indicators, quotes, "1d");
37
+ expect(r.map((x) => x.indicator.symbol)).toEqual(["A", "C", "B"]);
38
+ });
39
+ it("1w 期間で d7 降順", () => {
40
+ const r = rankByPerformance(indicators, quotes, "1w");
41
+ expect(r.map((x) => x.indicator.symbol)).toEqual(["C", "A", "B"]);
42
+ });
43
+ it("1m 期間で d30 降順", () => {
44
+ const r = rankByPerformance(indicators, quotes, "1m");
45
+ expect(r.map((x) => x.indicator.symbol)).toEqual(["A", "B", "C"]);
46
+ });
47
+ it("performance 無しは 1w/1m から除外される", () => {
48
+ const noPerf = {
49
+ A: q("A", 2.0),
50
+ B: q("B", -1.0, { d7: 3 }),
51
+ };
52
+ const r = rankByPerformance(indicators, noPerf, "1w");
53
+ // A は performance なし、B のみ含まれる、C は quote 自体無い
54
+ expect(r.length).toBe(1);
55
+ expect(r[0].indicator.symbol).toBe("B");
56
+ });
57
+ it("error の quote は除外", () => {
58
+ const errored = {
59
+ A: { error: "fail" },
60
+ B: q("B", 5),
61
+ };
62
+ const r = rankByPerformance(indicators, errored, "1d");
63
+ expect(r.length).toBe(1);
64
+ expect(r[0].indicator.symbol).toBe("B");
65
+ });
66
+ });
67
+ //# sourceMappingURL=rankings.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rankings.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/rankings.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getSessionStatuses } from "../sessions.js";
3
+ // JST 時刻指定用ヘルパ:JST H 時の UTC 換算時刻を作る
4
+ function atJst(hourJst, dow = "mon") {
5
+ // 2026-05-04 (月) 00:00 JST = 2026-05-03 15:00 UTC
6
+ const baseDates = {
7
+ mon: "2026-05-04T00:00:00+09:00",
8
+ tue: "2026-05-05T00:00:00+09:00",
9
+ fri: "2026-05-08T00:00:00+09:00",
10
+ sat: "2026-05-09T00:00:00+09:00",
11
+ sun: "2026-05-10T00:00:00+09:00",
12
+ };
13
+ const base = new Date(baseDates[dow]).getTime();
14
+ return new Date(base + hourJst * 3600 * 1000);
15
+ }
16
+ describe("getSessionStatuses", () => {
17
+ it("月曜 12:00 JST:東京と上海はオープン、他はクローズ", () => {
18
+ const out = getSessionStatuses(atJst(12, "mon"));
19
+ expect(out.find((s) => s.session.id === "tokyo")?.state).toBe("open");
20
+ expect(out.find((s) => s.session.id === "shanghai")?.state).toBe("open");
21
+ expect(out.find((s) => s.session.id === "london")?.state).toBe("closed");
22
+ expect(out.find((s) => s.session.id === "ny")?.state).toBe("closed");
23
+ });
24
+ it("月曜 23:00 JST:NY とロンドンがオープン", () => {
25
+ const out = getSessionStatuses(atJst(23, "mon"));
26
+ expect(out.find((s) => s.session.id === "ny")?.state).toBe("open");
27
+ expect(out.find((s) => s.session.id === "london")?.state).toBe("open");
28
+ expect(out.find((s) => s.session.id === "tokyo")?.state).toBe("closed");
29
+ });
30
+ it("月曜 8:50 JST:東京は pre-open(10 分後にオープン)", () => {
31
+ const out = getSessionStatuses(atJst(8 + 50 / 60, "mon"));
32
+ const tokyo = out.find((s) => s.session.id === "tokyo");
33
+ expect(tokyo?.state).toBe("pre-open");
34
+ expect(tokyo?.hoursUntilOpen).toBeDefined();
35
+ });
36
+ it("土曜は全市場 closed", () => {
37
+ const out = getSessionStatuses(atJst(10, "sat"));
38
+ for (const s of out)
39
+ expect(s.state).toBe("closed");
40
+ });
41
+ it("日曜は全市場 closed", () => {
42
+ const out = getSessionStatuses(atJst(15, "sun"));
43
+ for (const s of out)
44
+ expect(s.state).toBe("closed");
45
+ });
46
+ });
47
+ //# sourceMappingURL=sessions.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/sessions.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { lastSMA, rollingSMA } from "../sma.js";
3
+ describe("rollingSMA", () => {
4
+ it("先頭 N-1 個は null、N 番目以降は移動平均", () => {
5
+ const r = rollingSMA([1, 2, 3, 4, 5], 3);
6
+ expect(r).toEqual([null, null, 2, 3, 4]);
7
+ });
8
+ it("window=1 は元の配列と同じ", () => {
9
+ expect(rollingSMA([10, 20, 30], 1)).toEqual([10, 20, 30]);
10
+ });
11
+ it("空配列は空", () => {
12
+ expect(rollingSMA([], 3)).toEqual([]);
13
+ });
14
+ });
15
+ describe("lastSMA", () => {
16
+ it("末尾 N 個の平均を返す", () => {
17
+ expect(lastSMA([1, 2, 3, 4, 5], 3)).toBeCloseTo(4, 5); // (3+4+5)/3
18
+ });
19
+ it("点数不足は null", () => {
20
+ expect(lastSMA([1, 2], 3)).toBeNull();
21
+ });
22
+ it("window=length は全平均", () => {
23
+ expect(lastSMA([10, 20, 30], 3)).toBe(20);
24
+ });
25
+ });
26
+ //# sourceMappingURL=sma.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sma.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/sma.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEhD,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACf,MAAM,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;QACtB,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE;QACpB,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeThermometer, computeThermometerHistory, } from "../thermometer.js";
3
+ function q(symbol, price, changePercent = 0, changeBp = 0) {
4
+ return {
5
+ symbol,
6
+ price,
7
+ previousClose: price - (price * changePercent) / 100,
8
+ change: (price * changePercent) / 100,
9
+ changePercent,
10
+ changeBp,
11
+ fetchedAt: "2026-05-01T00:00:00.000Z",
12
+ sparkline: [price],
13
+ };
14
+ }
15
+ function snap(quotes) {
16
+ return { fetchedAt: "2026-05-01T00:00:00.000Z", quotes };
17
+ }
18
+ describe("computeThermometer", () => {
19
+ it("典型的なリスクオン構成 → score >= 70、level=risk-on", () => {
20
+ const s = snap({
21
+ "^VIX": q("^VIX", 12, -5),
22
+ "^GSPC": q("^GSPC", 5500, 1.2),
23
+ "^TNX": q("^TNX", 4.5, 0, 8),
24
+ "DX-Y.NYB": q("DX-Y.NYB", 105, -0.4),
25
+ });
26
+ const r = computeThermometer(s);
27
+ expect(r.score).toBeGreaterThanOrEqual(70);
28
+ expect(r.level).toBe("risk-on");
29
+ });
30
+ it("典型的なリスクオフ構成 → score <= 30、level=risk-off", () => {
31
+ const s = snap({
32
+ "^VIX": q("^VIX", 28, 8),
33
+ "^GSPC": q("^GSPC", 5500, -1.4),
34
+ "^TNX": q("^TNX", 4.5, 0, -8),
35
+ "DX-Y.NYB": q("DX-Y.NYB", 105, 0.4),
36
+ });
37
+ const r = computeThermometer(s);
38
+ expect(r.score).toBeLessThanOrEqual(30);
39
+ expect(r.level).toBe("risk-off");
40
+ });
41
+ it("中立 → 30 < score < 70、level=neutral", () => {
42
+ const s = snap({
43
+ "^VIX": q("^VIX", 20, 0),
44
+ "^GSPC": q("^GSPC", 5500, 0),
45
+ "^TNX": q("^TNX", 4.5, 0, 0),
46
+ "DX-Y.NYB": q("DX-Y.NYB", 105, 0),
47
+ });
48
+ const r = computeThermometer(s);
49
+ expect(r.score).toBeCloseTo(50, 0);
50
+ expect(r.level).toBe("neutral");
51
+ });
52
+ it("一部欠損でも平均を返す(部分縮退)", () => {
53
+ const s = snap({
54
+ "^VIX": q("^VIX", 15, 0),
55
+ "^GSPC": q("^GSPC", 5500, 0.5),
56
+ });
57
+ const r = computeThermometer(s);
58
+ expect(r).not.toBeNull();
59
+ expect(r.components).toHaveLength(4);
60
+ // 取得できた 2 成分のみ平均
61
+ const valid = r.components.filter((c) => c.value !== null);
62
+ expect(valid.length).toBe(2);
63
+ });
64
+ it("全成分欠損なら null", () => {
65
+ expect(computeThermometer(snap({}))).toBeNull();
66
+ });
67
+ it("clamp: 極端値(VIX=5)でも 0..100 に収まる", () => {
68
+ const s = snap({ "^VIX": q("^VIX", 5, 0) });
69
+ const r = computeThermometer(s);
70
+ expect(r.components[0].value).toBeLessThanOrEqual(100);
71
+ expect(r.components[0].value).toBeGreaterThanOrEqual(0);
72
+ });
73
+ it("clamp: 極端値(VIX=80)でも 0..100 に収まる", () => {
74
+ const s = snap({ "^VIX": q("^VIX", 80, 0) });
75
+ const r = computeThermometer(s);
76
+ expect(r.components[0].value).toBeLessThanOrEqual(100);
77
+ expect(r.components[0].value).toBeGreaterThanOrEqual(0);
78
+ });
79
+ });
80
+ describe("computeThermometerHistory", () => {
81
+ function withCloses(symbol, closes) {
82
+ return { ...q(symbol, closes[closes.length - 1] ?? 0, 0, 0), closes1y: closes };
83
+ }
84
+ it("VIX のみの履歴でもスコアを返す", () => {
85
+ const s = {
86
+ fetchedAt: "2026-05-01T00:00:00.000Z",
87
+ quotes: {
88
+ "^VIX": withCloses("^VIX", [20, 18, 16, 14, 12]),
89
+ },
90
+ };
91
+ const hist = computeThermometerHistory(s, 30);
92
+ // i=1..4 の 4 点
93
+ expect(hist.length).toBeGreaterThan(0);
94
+ // VIX が下がるにつれてスコアは上がる(リスクオン化)
95
+ expect(hist[hist.length - 1] > hist[0]).toBe(true);
96
+ });
97
+ it("4 成分すべて揃ってもスコア化できる", () => {
98
+ const len = 50;
99
+ const s = {
100
+ fetchedAt: "2026-05-01T00:00:00.000Z",
101
+ quotes: {
102
+ "^VIX": withCloses("^VIX", Array.from({ length: len }, () => 20)),
103
+ "^GSPC": withCloses("^GSPC", Array.from({ length: len }, (_, i) => 5500 + i * 5)),
104
+ "^TNX": withCloses("^TNX", Array.from({ length: len }, (_, i) => 4.5 + i * 0.001)),
105
+ "DX-Y.NYB": withCloses("DX-Y.NYB", Array.from({ length: len }, () => 105)),
106
+ },
107
+ };
108
+ const hist = computeThermometerHistory(s, 30);
109
+ expect(hist.length).toBe(30);
110
+ for (const v of hist) {
111
+ expect(v).toBeGreaterThanOrEqual(0);
112
+ expect(v).toBeLessThanOrEqual(100);
113
+ }
114
+ });
115
+ it("データ無しなら空配列", () => {
116
+ const s = { fetchedAt: "2026-05-01T00:00:00.000Z", quotes: {} };
117
+ expect(computeThermometerHistory(s, 30)).toEqual([]);
118
+ });
119
+ });
120
+ //# sourceMappingURL=thermometer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thermometer.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/thermometer.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeYieldSpread } from "../yield-spread.js";
3
+ function q(symbol, price, change = 0) {
4
+ return {
5
+ symbol,
6
+ price,
7
+ previousClose: price - change,
8
+ change,
9
+ changePercent: 0,
10
+ changeBp: change * 100,
11
+ fetchedAt: "",
12
+ sparkline: [],
13
+ };
14
+ }
15
+ describe("computeYieldSpread", () => {
16
+ it("^TNX - ^FVX を返す", () => {
17
+ const snap = {
18
+ fetchedAt: "",
19
+ quotes: {
20
+ "^TNX": q("^TNX", 4.5, 0.01),
21
+ "^FVX": q("^FVX", 4.2, 0.005),
22
+ },
23
+ };
24
+ const r = computeYieldSpread(snap);
25
+ expect(r.spread).toBeCloseTo(0.3, 5);
26
+ expect(r.spreadChangeBp).toBeCloseTo(0.5, 2);
27
+ });
28
+ it("片方欠損は null", () => {
29
+ const snap = {
30
+ fetchedAt: "",
31
+ quotes: { "^TNX": q("^TNX", 4.5) },
32
+ };
33
+ expect(computeYieldSpread(snap)).toBeNull();
34
+ });
35
+ it("error は null", () => {
36
+ const snap = {
37
+ fetchedAt: "",
38
+ quotes: {
39
+ "^TNX": q("^TNX", 4.5),
40
+ "^FVX": { error: "fail" },
41
+ },
42
+ };
43
+ expect(computeYieldSpread(snap)).toBeNull();
44
+ });
45
+ });
46
+ //# sourceMappingURL=yield-spread.test.js.map