@gabriel3615/ta_analysis
Version:
stock ta analysis
109 lines (95 loc) • 3.13 kB
text/typescript
import type { Candle } from '../../types.js';
import {
Backtester,
type BacktestResult,
type Strategy,
} from './Backtester.js';
export type ParamGrid = Record<string, number[] | string[] | boolean[]>;
export interface GridSearchConfig {
initialCapital?: number;
positionSizing?: number;
maxConcurrentPositions?: number;
commissionPerTrade?: number;
slippageBps?: number;
allowShort?: boolean;
}
export interface GridSearchResultItem {
params: Record<string, any>;
result: BacktestResult;
}
export interface GridSearchSummaryMetrics {
bestByTotalPnL: GridSearchResultItem | null;
bestBySharpe: GridSearchResultItem | null;
bestByMaxDrawdown: GridSearchResultItem | null; // lowest dd
}
export interface GridSearchOutput {
runs: GridSearchResultItem[];
summary: GridSearchSummaryMetrics;
}
function cartesianProduct<T>(arrays: T[][]): T[][] {
return arrays.reduce<T[][]>(
(acc, arr) => {
const next: T[][] = [];
for (const a of acc) {
for (const b of arr) {
next.push([...a, b]);
}
}
return next;
},
[[]]
);
}
function buildParamCombos(grid: ParamGrid): Record<string, any>[] {
const keys = Object.keys(grid);
if (keys.length === 0) return [{}];
const values = keys.map(k => grid[k] as any[]);
const combos = cartesianProduct(values);
return combos.map(vals => {
const obj: Record<string, any> = {};
vals.forEach((v, idx) => (obj[keys[idx]] = v));
return obj;
});
}
export type StrategyFactory = (params: Record<string, any>) => Strategy;
export async function runGridSearch(
candles: Candle[],
grid: ParamGrid,
strategyFactory: StrategyFactory,
backtesterInstance?: Backtester // Allow passing a pre-configured backtester
): Promise<GridSearchOutput> {
const paramCombos = buildParamCombos(grid);
const runs: GridSearchResultItem[] = [];
for (const params of paramCombos) {
const strategy = strategyFactory(params);
const backtester = backtesterInstance ?? new Backtester(); // Use provided instance or create a default one
const result = await backtester.run(candles, strategy);
runs.push({ params, result });
}
const bestByTotalPnL = runs.length
? runs.reduce((a, b) => (a.result.totalPnL > b.result.totalPnL ? a : b))
: null;
const bestBySharpe = runs.length
? runs.reduce((a, b) =>
a.result.metrics.sharpe > b.result.metrics.sharpe ? a : b
)
: null;
const bestByMaxDrawdown = runs.length
? runs.reduce((a, b) =>
a.result.metrics.maxDrawdown < b.result.metrics.maxDrawdown ? a : b
)
: null;
return { runs, summary: { bestByTotalPnL, bestBySharpe, bestByMaxDrawdown } };
}
export function formatMetricsPanel(run: GridSearchResultItem): string {
const m = run.result.metrics;
return [
`PnL: ${run.result.totalPnL.toFixed(2)}`,
`WinRate: ${(m.winRate * 100).toFixed(2)}%`,
`AvgWin: ${m.avgWin.toFixed(2)}`,
`AvgLoss: ${m.avgLoss.toFixed(2)}`,
`MaxDD: ${(m.maxDrawdown * 100).toFixed(2)}%`,
`Sharpe: ${m.sharpe.toFixed(2)}`,
`Trades: ${m.tradeCount}`,
].join(' | ');
}