bitbank-lab-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/assets/lightweight-charts.standalone.js +7 -0
- package/bin/bitbank-lab-mcp.js +20 -0
- package/lib/cache.ts +70 -0
- package/lib/candle-utils.ts +48 -0
- package/lib/candle-validate.ts +434 -0
- package/lib/conversions.ts +25 -0
- package/lib/datetime.ts +157 -0
- package/lib/depth-analysis.ts +51 -0
- package/lib/error.ts +15 -0
- package/lib/formatter.ts +296 -0
- package/lib/get-depth.ts +111 -0
- package/lib/http.ts +132 -0
- package/lib/indicator-config.ts +39 -0
- package/lib/indicator_buffer.ts +41 -0
- package/lib/indicators.ts +579 -0
- package/lib/logger.ts +120 -0
- package/lib/ma-snapshot-utils.ts +277 -0
- package/lib/math.ts +89 -0
- package/lib/pattern-diagrams.ts +562 -0
- package/lib/result.ts +104 -0
- package/lib/validate.ts +154 -0
- package/lib/volatility.ts +132 -0
- package/package.json +79 -0
- package/src/env.ts +4 -0
- package/src/handlers/analyzeCandlePatternsHandler.ts +383 -0
- package/src/handlers/analyzeFibonacciHandler.ts +54 -0
- package/src/handlers/analyzeIndicatorsHandler.ts +682 -0
- package/src/handlers/analyzeMarketSignalHandler.ts +272 -0
- package/src/handlers/analyzeMyPortfolioHandler.ts +800 -0
- package/src/handlers/detectPatternsHandler.ts +77 -0
- package/src/handlers/detectPatternsViewsHandler.ts +518 -0
- package/src/handlers/getTickersJpyHandler.ts +145 -0
- package/src/handlers/getVolatilityMetricsHandler.ts +234 -0
- package/src/handlers/portfolio/calc.ts +549 -0
- package/src/handlers/portfolio/fetch.ts +318 -0
- package/src/handlers/portfolio/types.ts +170 -0
- package/src/handlers/renderChartSvgHandler.ts +69 -0
- package/src/handlers/runBacktestHandler.ts +70 -0
- package/src/http.ts +107 -0
- package/src/private/auth.ts +104 -0
- package/src/private/client.ts +298 -0
- package/src/private/config.ts +25 -0
- package/src/private/confirmation.ts +185 -0
- package/src/private/schemas.ts +866 -0
- package/src/prompts.ts +2296 -0
- package/src/resources/app-resources.ts +79 -0
- package/src/schema/analysis.ts +942 -0
- package/src/schema/backtest.ts +100 -0
- package/src/schema/base.ts +88 -0
- package/src/schema/candle-validate.ts +135 -0
- package/src/schema/chart.ts +399 -0
- package/src/schema/index.ts +11 -0
- package/src/schema/indicators.ts +125 -0
- package/src/schema/market-data.ts +298 -0
- package/src/schema/patterns.ts +382 -0
- package/src/schema/types.ts +97 -0
- package/src/schemas.d.ts +37 -0
- package/src/schemas.ts +7 -0
- package/src/server.ts +405 -0
- package/src/tool-definition.ts +44 -0
- package/src/tool-registry.ts +174 -0
- package/src/types/express-shim.d.ts +9 -0
- package/src/types/schemas.generated.d.ts +23 -0
- package/tools/analyze_bb_snapshot.ts +385 -0
- package/tools/analyze_candle_patterns.ts +810 -0
- package/tools/analyze_currency_strength.ts +273 -0
- package/tools/analyze_ema_snapshot.ts +183 -0
- package/tools/analyze_fibonacci.ts +530 -0
- package/tools/analyze_ichimoku_snapshot.ts +606 -0
- package/tools/analyze_indicators.ts +691 -0
- package/tools/analyze_market_signal.ts +665 -0
- package/tools/analyze_mtf_fibonacci.ts +273 -0
- package/tools/analyze_mtf_sma.ts +175 -0
- package/tools/analyze_sma_snapshot.ts +146 -0
- package/tools/analyze_stoch_snapshot.ts +276 -0
- package/tools/analyze_support_resistance.ts +817 -0
- package/tools/analyze_volume_profile.ts +546 -0
- package/tools/chart/ichimoku-cloud.ts +113 -0
- package/tools/chart/render-depth.ts +139 -0
- package/tools/chart/render-sub-panels.ts +208 -0
- package/tools/chart/svg-utils.ts +102 -0
- package/tools/detect_macd_cross.ts +691 -0
- package/tools/detect_patterns.ts +424 -0
- package/tools/detect_whale_events.ts +181 -0
- package/tools/get_candles.ts +487 -0
- package/tools/get_flow_metrics.ts +596 -0
- package/tools/get_orderbook.ts +540 -0
- package/tools/get_ticker.ts +132 -0
- package/tools/get_tickers_jpy.ts +240 -0
- package/tools/get_transactions.ts +209 -0
- package/tools/get_volatility_metrics.ts +302 -0
- package/tools/patterns/aftermath.ts +212 -0
- package/tools/patterns/config.ts +151 -0
- package/tools/patterns/detect_doubles.ts +650 -0
- package/tools/patterns/detect_hs.ts +635 -0
- package/tools/patterns/detect_pennants.ts +373 -0
- package/tools/patterns/detect_triangles.ts +820 -0
- package/tools/patterns/detect_triples.ts +633 -0
- package/tools/patterns/detect_wedges.ts +1072 -0
- package/tools/patterns/helpers.ts +517 -0
- package/tools/patterns/index.ts +40 -0
- package/tools/patterns/regression.ts +153 -0
- package/tools/patterns/smoothing.ts +168 -0
- package/tools/patterns/swing.ts +91 -0
- package/tools/patterns/types.ts +193 -0
- package/tools/prepare_chart_data.ts +294 -0
- package/tools/prepare_depth_data.ts +189 -0
- package/tools/private/analyze_my_portfolio.ts +21 -0
- package/tools/private/cancel_order.ts +127 -0
- package/tools/private/cancel_orders.ts +121 -0
- package/tools/private/create_order.ts +236 -0
- package/tools/private/get_margin_positions.ts +134 -0
- package/tools/private/get_margin_status.ts +155 -0
- package/tools/private/get_margin_trade_history.ts +156 -0
- package/tools/private/get_my_assets.ts +207 -0
- package/tools/private/get_my_deposit_withdrawal.ts +500 -0
- package/tools/private/get_my_orders.ts +157 -0
- package/tools/private/get_my_trade_history.ts +229 -0
- package/tools/private/get_order.ts +95 -0
- package/tools/private/get_orders_info.ts +90 -0
- package/tools/private/preview_cancel_order.ts +172 -0
- package/tools/private/preview_cancel_orders.ts +137 -0
- package/tools/private/preview_order.ts +292 -0
- package/tools/render_candle_pattern_diagram.ts +389 -0
- package/tools/render_chart_svg.ts +799 -0
- package/tools/render_depth_svg.ts +274 -0
- package/tools/trading_process/index.ts +7 -0
- package/tools/trading_process/lib/backtest_engine.ts +252 -0
- package/tools/trading_process/lib/equity.ts +131 -0
- package/tools/trading_process/lib/fetch_candles.ts +181 -0
- package/tools/trading_process/lib/sma.ts +62 -0
- package/tools/trading_process/lib/strategies/bb_breakout.ts +141 -0
- package/tools/trading_process/lib/strategies/index.ts +52 -0
- package/tools/trading_process/lib/strategies/macd_cross.ts +256 -0
- package/tools/trading_process/lib/strategies/rsi.ts +133 -0
- package/tools/trading_process/lib/strategies/sma_cross.ts +214 -0
- package/tools/trading_process/lib/strategies/types.ts +118 -0
- package/tools/trading_process/lib/svg_to_png.ts +64 -0
- package/tools/trading_process/render_backtest_chart_generic.ts +729 -0
- package/tools/trading_process/run_backtest.ts +243 -0
- package/tools/trading_process/types.ts +85 -0
- package/tools/validate_candle_data.ts +260 -0
- package/tsconfig.json +17 -0
- package/ui/cancel-confirm/dist/cancel-confirm.html +99 -0
- package/ui/order-confirm/dist/order-confirm.html +99 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* strategies/rsi.ts - RSI(相対力指数)戦略
|
|
3
|
+
*
|
|
4
|
+
* エントリー: RSI が oversold 以下から上昇(売られすぎから回復)
|
|
5
|
+
* エグジット: RSI が overbought 以上に到達(買われすぎ)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { rsi } from '../../../../lib/indicators.js';
|
|
9
|
+
import type { Candle } from '../../types.js';
|
|
10
|
+
import type { Overlay, ParamValidationResult, Signal, Strategy } from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RSI戦略のデフォルトパラメータ
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_PARAMS = {
|
|
16
|
+
period: 14,
|
|
17
|
+
overbought: 70,
|
|
18
|
+
oversold: 30,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* RSIを計算(lib/indicators.ts への委譲)
|
|
23
|
+
*
|
|
24
|
+
* @param closes 終値配列(古い順)
|
|
25
|
+
* @param period RSI期間
|
|
26
|
+
* @returns RSI配列(0-100、先頭period個はNaN)
|
|
27
|
+
*/
|
|
28
|
+
export function calculateRSI(closes: number[], period: number): number[] {
|
|
29
|
+
return rsi(closes, period);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* パラメータのバリデーション
|
|
34
|
+
*/
|
|
35
|
+
export function validateParams(params: Record<string, number>): ParamValidationResult {
|
|
36
|
+
const errors: string[] = [];
|
|
37
|
+
const normalized = { ...DEFAULT_PARAMS, ...params };
|
|
38
|
+
|
|
39
|
+
if (normalized.period < 2) {
|
|
40
|
+
errors.push('period must be at least 2');
|
|
41
|
+
}
|
|
42
|
+
if (normalized.overbought <= normalized.oversold) {
|
|
43
|
+
errors.push('overbought must be greater than oversold');
|
|
44
|
+
}
|
|
45
|
+
if (normalized.oversold < 0 || normalized.oversold > 100) {
|
|
46
|
+
errors.push('oversold must be between 0 and 100');
|
|
47
|
+
}
|
|
48
|
+
if (normalized.overbought < 0 || normalized.overbought > 100) {
|
|
49
|
+
errors.push('overbought must be between 0 and 100');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
valid: errors.length === 0,
|
|
54
|
+
errors,
|
|
55
|
+
normalizedParams: normalized,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* RSI戦略
|
|
61
|
+
*/
|
|
62
|
+
export const rsiStrategy: Strategy = {
|
|
63
|
+
name: 'RSI',
|
|
64
|
+
type: 'rsi',
|
|
65
|
+
requiredBars: 20,
|
|
66
|
+
defaultParams: DEFAULT_PARAMS,
|
|
67
|
+
|
|
68
|
+
generate(candles: Candle[], params: Record<string, number>): Signal[] {
|
|
69
|
+
const { period, overbought, oversold } = { ...DEFAULT_PARAMS, ...params };
|
|
70
|
+
const closes = candles.map((c) => c.close);
|
|
71
|
+
const rsi = calculateRSI(closes, period);
|
|
72
|
+
|
|
73
|
+
const signals: Signal[] = [];
|
|
74
|
+
const startIdx = period + 1; // RSIが有効 + 前日比較用
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < candles.length; i++) {
|
|
77
|
+
if (i < startIdx) {
|
|
78
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const prevRSI = rsi[i - 1];
|
|
83
|
+
const currRSI = rsi[i];
|
|
84
|
+
|
|
85
|
+
if (Number.isNaN(prevRSI) || Number.isNaN(currRSI)) {
|
|
86
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// エントリー: RSI が oversold 以下から上抜け
|
|
91
|
+
if (prevRSI <= oversold && currRSI > oversold) {
|
|
92
|
+
signals.push({
|
|
93
|
+
time: candles[i].time,
|
|
94
|
+
action: 'buy',
|
|
95
|
+
reason: `RSI crossed above ${oversold} (oversold exit): ${currRSI.toFixed(1)}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// エグジット: RSI が overbought 以上に到達
|
|
99
|
+
else if (currRSI >= overbought) {
|
|
100
|
+
signals.push({
|
|
101
|
+
time: candles[i].time,
|
|
102
|
+
action: 'sell',
|
|
103
|
+
reason: `RSI reached overbought (${overbought}): ${currRSI.toFixed(1)}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// シグナルなし
|
|
107
|
+
else {
|
|
108
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return signals;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
getOverlays(candles: Candle[], params: Record<string, number>): Overlay[] {
|
|
116
|
+
const { period, overbought: _overbought, oversold: _oversold } = { ...DEFAULT_PARAMS, ...params };
|
|
117
|
+
const closes = candles.map((c) => c.close);
|
|
118
|
+
const rsi = calculateRSI(closes, period);
|
|
119
|
+
|
|
120
|
+
// RSIは別のスケールなのでバンドとして表現(実際はサブチャートに表示すべき)
|
|
121
|
+
// ここでは簡易的にRSIの値をオーバーレイとして返す
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: 'line',
|
|
125
|
+
name: `RSI(${period})`,
|
|
126
|
+
color: '#a855f7', // purple
|
|
127
|
+
data: rsi, // 注: これは0-100のスケールで、価格チャートには直接描画できない
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default rsiStrategy;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* strategies/sma_cross.ts - SMAクロスオーバー戦略
|
|
3
|
+
*
|
|
4
|
+
* エントリー: 短期SMA > 長期SMA にクロス(ゴールデンクロス)
|
|
5
|
+
* エグジット: 短期SMA < 長期SMA にクロス(デッドクロス)
|
|
6
|
+
*
|
|
7
|
+
* オプションフィルター:
|
|
8
|
+
* - sma_filter_period: SMAトレンドフィルター(例: 200)。価格がSMA上の場合のみ買い
|
|
9
|
+
* - rsi_filter_period: RSIフィルター期間(例: 14)。0で無効
|
|
10
|
+
* - rsi_filter_max: RSI上限(例: 70)。RSIがこの値未満の場合のみ買い
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Candle } from '../../types.js';
|
|
14
|
+
import { calculateSMA } from '../sma.js';
|
|
15
|
+
import { calculateRSI } from './rsi.js';
|
|
16
|
+
import type { Overlay, ParamValidationResult, Signal, Strategy } from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SMAクロスオーバー戦略のデフォルトパラメータ
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_PARAMS: Record<string, number> = {
|
|
22
|
+
short: 5,
|
|
23
|
+
long: 20,
|
|
24
|
+
// フィルター(0 = 無効)
|
|
25
|
+
sma_filter_period: 0,
|
|
26
|
+
rsi_filter_period: 0,
|
|
27
|
+
rsi_filter_max: 100, // RSI < この値 の場合のみ買い(100=フィルター無効)
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* パラメータのバリデーション
|
|
32
|
+
*/
|
|
33
|
+
export function validateParams(params: Record<string, number>): ParamValidationResult {
|
|
34
|
+
const errors: string[] = [];
|
|
35
|
+
const normalized = { ...DEFAULT_PARAMS, ...params };
|
|
36
|
+
|
|
37
|
+
if (normalized.short >= normalized.long) {
|
|
38
|
+
errors.push('short must be less than long');
|
|
39
|
+
}
|
|
40
|
+
if (normalized.short < 2) {
|
|
41
|
+
errors.push('short must be at least 2');
|
|
42
|
+
}
|
|
43
|
+
if (normalized.long < 3) {
|
|
44
|
+
errors.push('long must be at least 3');
|
|
45
|
+
}
|
|
46
|
+
if (normalized.sma_filter_period < 0) {
|
|
47
|
+
errors.push('sma_filter_period must be >= 0');
|
|
48
|
+
}
|
|
49
|
+
if (normalized.rsi_filter_period < 0) {
|
|
50
|
+
errors.push('rsi_filter_period must be >= 0');
|
|
51
|
+
}
|
|
52
|
+
if (normalized.rsi_filter_max < 0 || normalized.rsi_filter_max > 100) {
|
|
53
|
+
errors.push('rsi_filter_max must be 0-100');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
valid: errors.length === 0,
|
|
58
|
+
errors,
|
|
59
|
+
normalizedParams: normalized,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* フィルター条件の説明文を生成
|
|
65
|
+
*/
|
|
66
|
+
function describeFilters(params: Record<string, number>): string {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
if (params.sma_filter_period > 0) {
|
|
69
|
+
parts.push(`SMA${params.sma_filter_period} trend filter`);
|
|
70
|
+
}
|
|
71
|
+
if (params.rsi_filter_period > 0 && params.rsi_filter_max < 100) {
|
|
72
|
+
parts.push(`RSI(${params.rsi_filter_period})<${params.rsi_filter_max}`);
|
|
73
|
+
}
|
|
74
|
+
return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* SMAクロスオーバー戦略
|
|
79
|
+
*/
|
|
80
|
+
export const smaCrossStrategy: Strategy = {
|
|
81
|
+
name: 'SMA Crossover',
|
|
82
|
+
type: 'sma_cross',
|
|
83
|
+
requiredBars: 30, // 最低でもlong期間 + バッファ
|
|
84
|
+
defaultParams: DEFAULT_PARAMS,
|
|
85
|
+
|
|
86
|
+
generate(candles: Candle[], params: Record<string, number>): Signal[] {
|
|
87
|
+
const p = { ...DEFAULT_PARAMS, ...params };
|
|
88
|
+
const { short: shortPeriod, long: longPeriod } = p;
|
|
89
|
+
const closes = candles.map((c) => c.close);
|
|
90
|
+
|
|
91
|
+
const smaShort = calculateSMA(closes, shortPeriod);
|
|
92
|
+
const smaLong = calculateSMA(closes, longPeriod);
|
|
93
|
+
|
|
94
|
+
// フィルター用の指標を事前計算
|
|
95
|
+
const smaFilter = p.sma_filter_period > 0 ? calculateSMA(closes, p.sma_filter_period) : null;
|
|
96
|
+
const rsi = p.rsi_filter_period > 0 ? calculateRSI(closes, p.rsi_filter_period) : null;
|
|
97
|
+
|
|
98
|
+
const signals: Signal[] = [];
|
|
99
|
+
// SMAフィルターが有効な場合、開始インデックスをその分遅らせる
|
|
100
|
+
const baseStartIdx = longPeriod;
|
|
101
|
+
const startIdx = smaFilter ? Math.max(baseStartIdx, p.sma_filter_period) : baseStartIdx;
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < candles.length; i++) {
|
|
104
|
+
if (i < startIdx) {
|
|
105
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const prevShort = smaShort[i - 1];
|
|
110
|
+
const prevLong = smaLong[i - 1];
|
|
111
|
+
const currShort = smaShort[i];
|
|
112
|
+
const currLong = smaLong[i];
|
|
113
|
+
|
|
114
|
+
// NaN チェック
|
|
115
|
+
if (Number.isNaN(prevShort) || Number.isNaN(prevLong) || Number.isNaN(currShort) || Number.isNaN(currLong)) {
|
|
116
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ゴールデンクロス: short が long を上抜け
|
|
121
|
+
if (prevShort <= prevLong && currShort > currLong) {
|
|
122
|
+
// フィルター適用(買いシグナルのみにフィルターを適用)
|
|
123
|
+
let filtered = false;
|
|
124
|
+
|
|
125
|
+
// SMAトレンドフィルター: 価格がSMA上の場合のみ
|
|
126
|
+
if (smaFilter && !Number.isNaN(smaFilter[i]) && closes[i] < smaFilter[i]) {
|
|
127
|
+
filtered = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// RSIフィルター
|
|
131
|
+
if (rsi && !Number.isNaN(rsi[i]) && rsi[i] >= p.rsi_filter_max) {
|
|
132
|
+
filtered = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (filtered) {
|
|
136
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
137
|
+
} else {
|
|
138
|
+
const filterDesc = describeFilters(p);
|
|
139
|
+
signals.push({
|
|
140
|
+
time: candles[i].time,
|
|
141
|
+
action: 'buy',
|
|
142
|
+
reason: `Golden Cross: SMA(${shortPeriod}) > SMA(${longPeriod})${filterDesc}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// デッドクロス: short が long を下抜け(エグジットなのでフィルター適用しない)
|
|
147
|
+
else if (prevShort >= prevLong && currShort < currLong) {
|
|
148
|
+
signals.push({
|
|
149
|
+
time: candles[i].time,
|
|
150
|
+
action: 'sell',
|
|
151
|
+
reason: `Dead Cross: SMA(${shortPeriod}) < SMA(${longPeriod})`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// シグナルなし
|
|
155
|
+
else {
|
|
156
|
+
signals.push({ time: candles[i].time, action: 'hold' });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return signals;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
getOverlays(candles: Candle[], params: Record<string, number>): Overlay[] {
|
|
164
|
+
const p = { ...DEFAULT_PARAMS, ...params };
|
|
165
|
+
const { short: shortPeriod, long: longPeriod } = p;
|
|
166
|
+
const closes = candles.map((c) => c.close);
|
|
167
|
+
|
|
168
|
+
const smaShort = calculateSMA(closes, shortPeriod);
|
|
169
|
+
const smaLong = calculateSMA(closes, longPeriod);
|
|
170
|
+
|
|
171
|
+
const overlays: Overlay[] = [
|
|
172
|
+
{
|
|
173
|
+
type: 'line' as const,
|
|
174
|
+
name: `SMA(${shortPeriod})`,
|
|
175
|
+
color: '#fbbf24', // yellow(Closeの青と区別)
|
|
176
|
+
data: smaShort,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'line' as const,
|
|
180
|
+
name: `SMA(${longPeriod})`,
|
|
181
|
+
color: '#ef4444', // red
|
|
182
|
+
data: smaLong,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// SMAフィルターが有効な場合、SMAラインを価格チャートに表示
|
|
187
|
+
if (p.sma_filter_period > 0) {
|
|
188
|
+
const smaFilter = calculateSMA(closes, p.sma_filter_period);
|
|
189
|
+
overlays.push({
|
|
190
|
+
type: 'line' as const,
|
|
191
|
+
name: `SMA${p.sma_filter_period} (filter)`,
|
|
192
|
+
color: '#8b5cf6', // purple
|
|
193
|
+
data: smaFilter,
|
|
194
|
+
panel: 'price' as const,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// RSIフィルターが有効な場合、RSIラインをインジケータパネルに表示
|
|
199
|
+
if (p.rsi_filter_period > 0 && p.rsi_filter_max < 100) {
|
|
200
|
+
const rsi = calculateRSI(closes, p.rsi_filter_period);
|
|
201
|
+
overlays.push({
|
|
202
|
+
type: 'line' as const,
|
|
203
|
+
name: `RSI(${p.rsi_filter_period})`,
|
|
204
|
+
color: '#a78bfa',
|
|
205
|
+
data: rsi,
|
|
206
|
+
panel: 'indicator' as const,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return overlays;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export default smaCrossStrategy;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* strategies/types.ts - 汎用バックテスト戦略の型定義
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Candle } from '../../types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* トレードシグナル
|
|
9
|
+
*/
|
|
10
|
+
export interface Signal {
|
|
11
|
+
/** シグナル発生時刻 */
|
|
12
|
+
time: string;
|
|
13
|
+
/** シグナルの種類 */
|
|
14
|
+
action: 'buy' | 'sell' | 'hold';
|
|
15
|
+
/** シグナルの理由(オプション) */
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 戦略タイプ
|
|
21
|
+
*/
|
|
22
|
+
export type StrategyType = 'sma_cross' | 'rsi' | 'macd_cross' | 'bb_breakout';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 戦略設定
|
|
26
|
+
*/
|
|
27
|
+
export interface StrategyConfig {
|
|
28
|
+
type: StrategyType;
|
|
29
|
+
params: Record<string, number>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* オーバーレイデータ(チャート描画用)
|
|
34
|
+
*/
|
|
35
|
+
export interface OverlayLine {
|
|
36
|
+
type: 'line';
|
|
37
|
+
name: string;
|
|
38
|
+
color: string;
|
|
39
|
+
data: number[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OverlayBand {
|
|
43
|
+
type: 'band';
|
|
44
|
+
name: string;
|
|
45
|
+
color: string;
|
|
46
|
+
fillColor: string;
|
|
47
|
+
data: { upper: number[]; middle?: number[]; lower: number[] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OverlayMarker {
|
|
51
|
+
type: 'marker';
|
|
52
|
+
name: string;
|
|
53
|
+
color: string;
|
|
54
|
+
data: Array<{ index: number; value: number; label?: string }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface OverlayHistogram {
|
|
58
|
+
type: 'histogram';
|
|
59
|
+
name: string;
|
|
60
|
+
positiveColor: string;
|
|
61
|
+
negativeColor: string;
|
|
62
|
+
data: number[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* オーバーレイの描画先パネル
|
|
67
|
+
* - 'price': 価格チャート上に描画(デフォルト)
|
|
68
|
+
* - 'indicator': 独立したインジケータサブパネルに描画(MACD, RSI 等)
|
|
69
|
+
*/
|
|
70
|
+
export type OverlayPanel = 'price' | 'indicator';
|
|
71
|
+
|
|
72
|
+
export type Overlay =
|
|
73
|
+
| (OverlayLine & { panel?: OverlayPanel })
|
|
74
|
+
| (OverlayBand & { panel?: OverlayPanel })
|
|
75
|
+
| (OverlayMarker & { panel?: OverlayPanel })
|
|
76
|
+
| (OverlayHistogram & { panel?: OverlayPanel });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 戦略インターフェース
|
|
80
|
+
*/
|
|
81
|
+
export interface Strategy {
|
|
82
|
+
/** 戦略名 */
|
|
83
|
+
name: string;
|
|
84
|
+
/** 戦略タイプ */
|
|
85
|
+
type: StrategyType;
|
|
86
|
+
/** 計算に必要な最低バー数 */
|
|
87
|
+
requiredBars: number;
|
|
88
|
+
/** デフォルトパラメータ */
|
|
89
|
+
defaultParams: Record<string, number>;
|
|
90
|
+
/**
|
|
91
|
+
* シグナル生成
|
|
92
|
+
* @param candles ローソク足データ
|
|
93
|
+
* @param params 戦略パラメータ
|
|
94
|
+
* @returns シグナル配列(各バーに対応)
|
|
95
|
+
*/
|
|
96
|
+
generate(candles: Candle[], params: Record<string, number>): Signal[];
|
|
97
|
+
/**
|
|
98
|
+
* オーバーレイデータ取得(チャート描画用)
|
|
99
|
+
* @param candles ローソク足データ
|
|
100
|
+
* @param params 戦略パラメータ
|
|
101
|
+
* @returns オーバーレイ配列
|
|
102
|
+
*/
|
|
103
|
+
getOverlays(candles: Candle[], params: Record<string, number>): Overlay[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 戦略パラメータのバリデーション結果
|
|
108
|
+
*/
|
|
109
|
+
export interface ParamValidationResult {
|
|
110
|
+
valid: boolean;
|
|
111
|
+
errors: string[];
|
|
112
|
+
normalizedParams: Record<string, number>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 戦略レジストリ型
|
|
117
|
+
*/
|
|
118
|
+
export type StrategyRegistry = Map<StrategyType, Strategy>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG to PNG conversion utility using sharp
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import sharp from 'sharp';
|
|
8
|
+
import { dayjs } from '../../../lib/datetime.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert SVG string to PNG and save to file
|
|
12
|
+
* @param svg - SVG string
|
|
13
|
+
* @param outputPath - Output file path (should end with .png)
|
|
14
|
+
* @param options - Optional settings
|
|
15
|
+
* @returns Promise<string> - The output file path
|
|
16
|
+
*/
|
|
17
|
+
export async function svgToPng(
|
|
18
|
+
svg: string,
|
|
19
|
+
outputPath: string,
|
|
20
|
+
options: {
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
density?: number; // DPI for SVG rendering
|
|
24
|
+
} = {},
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
const { density = 150 } = options;
|
|
27
|
+
|
|
28
|
+
// Ensure output directory exists
|
|
29
|
+
const dir = dirname(outputPath);
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Convert SVG to PNG using sharp
|
|
35
|
+
const svgBuffer = Buffer.from(svg, 'utf-8');
|
|
36
|
+
|
|
37
|
+
let pipeline = sharp(svgBuffer, { density });
|
|
38
|
+
|
|
39
|
+
// Resize if dimensions specified
|
|
40
|
+
if (options.width || options.height) {
|
|
41
|
+
pipeline = pipeline.resize(options.width, options.height, {
|
|
42
|
+
fit: 'inside',
|
|
43
|
+
withoutEnlargement: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await pipeline.png().toFile(outputPath);
|
|
48
|
+
|
|
49
|
+
return outputPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a unique filename for backtest chart
|
|
54
|
+
*/
|
|
55
|
+
export function generateBacktestChartFilename(
|
|
56
|
+
pair: string,
|
|
57
|
+
timeframe: string,
|
|
58
|
+
strategy: string,
|
|
59
|
+
format: 'png' | 'svg' = 'png',
|
|
60
|
+
): string {
|
|
61
|
+
const timestamp = dayjs().format('YYYY-MM-DDTHH-mm-ss');
|
|
62
|
+
const safePair = pair.replace('_', '');
|
|
63
|
+
return `backtest_${safePair}_${timeframe}_${strategy}_${timestamp}.${format}`;
|
|
64
|
+
}
|