This commit is contained in:
Boki 2025-06-22 17:55:51 -04:00
parent d858222af7
commit 7d9044ab29
202 changed files with 10755 additions and 10972 deletions

View file

@ -37,25 +37,25 @@ export type {
HasClose,
HasOHLC,
HasVolume,
HasTimestamp
HasTimestamp,
} from '@stock-bot/types';
// Export working calculation functions
export * from './basic-calculations';
// Export working technical indicators (building one by one)
export {
sma,
ema,
rsi,
macd,
bollingerBands,
atr,
obv,
stochastic,
williamsR,
cci,
mfi,
export {
sma,
ema,
rsi,
macd,
bollingerBands,
atr,
obv,
stochastic,
williamsR,
cci,
mfi,
vwma,
momentum,
roc,
@ -80,7 +80,7 @@ export {
balanceOfPower,
trix,
massIndex,
coppockCurve
coppockCurve,
} from './technical-indicators';
export * from './risk-metrics';
export * from './performance-metrics';

View file

@ -1,3 +1,5 @@
import { ulcerIndex } from './risk-metrics';
/**
* Performance Metrics and Analysis
* Comprehensive performance measurement tools for trading strategies and portfolios
@ -18,7 +20,6 @@ export interface PortfolioMetrics {
alpha: number;
volatility: number;
}
import { ulcerIndex } from './risk-metrics';
export interface TradePerformance {
totalTrades: number;
@ -156,8 +157,10 @@ export function analyzeDrawdowns(
}
const first = equityCurve[0];
if (!first) {return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };}
if (!first) {
return { maxDrawdown: 0, maxDrawdownDuration: 0, averageDrawdown: 0, drawdownPeriods: [] };
}
let peak = first.value;
let peakDate = first.date;
let maxDrawdown = 0;
@ -175,18 +178,21 @@ export function analyzeDrawdowns(
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
if (!current) {continue;}
if (!current) {
continue;
}
if (current.value > peak) {
// New peak - end any current drawdown
if (currentDrawdownStart) {
const prev = equityCurve[i - 1];
if (!prev) {continue;}
if (!prev) {
continue;
}
const drawdownMagnitude = (peak - prev.value) / peak;
const duration = Math.floor(
(prev.date.getTime() - currentDrawdownStart.getTime()) /
(1000 * 60 * 60 * 24)
(prev.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)
);
drawdownPeriods.push({
@ -217,8 +223,10 @@ export function analyzeDrawdowns(
// Handle ongoing drawdown
if (currentDrawdownStart) {
const lastPoint = equityCurve[equityCurve.length - 1];
if (!lastPoint) {return { maxDrawdown, maxDrawdownDuration, averageDrawdown: 0, drawdownPeriods };}
if (!lastPoint) {
return { maxDrawdown, maxDrawdownDuration, averageDrawdown: 0, drawdownPeriods };
}
const drawdownMagnitude = (peak - lastPoint.value) / peak;
const duration = Math.floor(
(lastPoint.date.getTime() - currentDrawdownStart.getTime()) / (1000 * 60 * 60 * 24)
@ -378,8 +386,10 @@ export function strategyPerformanceAttribution(
for (let i = 0; i < sectorWeights.length; i++) {
const portfolioWeight = sectorWeights[i];
const sectorReturn = sectorReturns[i];
if (portfolioWeight === undefined || sectorReturn === undefined) {continue;}
if (portfolioWeight === undefined || sectorReturn === undefined) {
continue;
}
const benchmarkWeight = 1 / sectorWeights.length; // Assuming equal benchmark weights
// Allocation effect: (portfolio weight - benchmark weight) * (benchmark sector return - benchmark return)
@ -483,16 +493,31 @@ export function calculateStrategyMetrics(
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
const previous = equityCurve[i - 1];
if (!current || !previous) {continue;}
if (!current || !previous) {
continue;
}
const ret = (current.value - previous.value) / previous.value;
returns.push(ret);
}
const lastPoint = equityCurve[equityCurve.length - 1];
const firstPoint = equityCurve[0];
if (!lastPoint || !firstPoint) {return { totalValue: 0, totalReturn: 0, totalReturnPercent: 0, dailyReturn: 0, dailyReturnPercent: 0, maxDrawdown: 0, sharpeRatio: 0, beta: 0, alpha: 0, volatility: 0 };}
if (!lastPoint || !firstPoint) {
return {
totalValue: 0,
totalReturn: 0,
totalReturnPercent: 0,
dailyReturn: 0,
dailyReturnPercent: 0,
maxDrawdown: 0,
sharpeRatio: 0,
beta: 0,
alpha: 0,
volatility: 0,
};
}
const totalValue = lastPoint.value;
const totalReturn = totalValue - firstPoint.value;
const totalReturnPercent = (totalReturn / firstPoint.value) * 100;
@ -562,12 +587,10 @@ export function informationRatio(portfolioReturns: number[], benchmarkReturns: n
throw new Error('Portfolio and benchmark returns must have the same length.');
}
const excessReturns = portfolioReturns.map(
(portfolioReturn, index) => {
const benchmark = benchmarkReturns[index];
return benchmark !== undefined ? portfolioReturn - benchmark : 0;
}
);
const excessReturns = portfolioReturns.map((portfolioReturn, index) => {
const benchmark = benchmarkReturns[index];
return benchmark !== undefined ? portfolioReturn - benchmark : 0;
});
const trackingError = calculateVolatility(excessReturns);
const avgExcessReturn = excessReturns.reduce((sum, ret) => sum + ret, 0) / excessReturns.length;
@ -602,8 +625,10 @@ export function captureRatio(
for (let i = 0; i < portfolioReturns.length; i++) {
const benchmarkReturn = benchmarkReturns[i];
const portfolioReturn = portfolioReturns[i];
if (benchmarkReturn === undefined || portfolioReturn === undefined) {continue;}
if (benchmarkReturn === undefined || portfolioReturn === undefined) {
continue;
}
if (benchmarkReturn > 0) {
upCapture += portfolioReturn;
upMarketPeriods++;
@ -733,17 +758,21 @@ export function timeWeightedRateOfReturn(
if (cashFlows.length < 2) {
return 0;
}
const first = cashFlows[0];
if (!first) {return 0;}
if (!first) {
return 0;
}
let totalReturn = 1;
let previousValue = first.value;
for (let i = 1; i < cashFlows.length; i++) {
const current = cashFlows[i];
if (!current) {continue;}
if (!current) {
continue;
}
const periodReturn =
(current.value - previousValue - current.amount) / (previousValue + current.amount);
totalReturn *= 1 + periodReturn;
@ -762,10 +791,12 @@ export function moneyWeightedRateOfReturn(
if (cashFlows.length === 0) {
return 0;
}
const first = cashFlows[0];
if (!first) {return 0;}
if (!first) {
return 0;
}
// Approximate MWRR using Internal Rate of Return (IRR)
// This requires a numerical method or library for accurate IRR calculation
// This is a simplified example and may not be accurate for all cases
@ -826,8 +857,10 @@ function calculateBeta(portfolioReturns: number[], marketReturns: number[]): num
for (let i = 0; i < portfolioReturns.length; i++) {
const portfolioReturn = portfolioReturns[i];
const marketReturn = marketReturns[i];
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
if (portfolioReturn === undefined || marketReturn === undefined) {
continue;
}
const portfolioDiff = portfolioReturn - portfolioMean;
const marketDiff = marketReturn - marketMean;

View file

@ -71,14 +71,18 @@ export function maxDrawdown(equityCurve: number[]): number {
let maxDD = 0;
const first = equityCurve[0];
if (first === undefined) {return 0;}
if (first === undefined) {
return 0;
}
let peak = first;
for (let i = 1; i < equityCurve.length; i++) {
const current = equityCurve[i];
if (current === undefined) {continue;}
if (current === undefined) {
continue;
}
if (current > peak) {
peak = current;
} else {
@ -150,8 +154,10 @@ export function beta(portfolioReturns: number[], marketReturns: number[]): numbe
for (let i = 0; i < n; i++) {
const portfolioReturn = portfolioReturns[i];
const marketReturn = marketReturns[i];
if (portfolioReturn === undefined || marketReturn === undefined) {continue;}
if (portfolioReturn === undefined || marketReturn === undefined) {
continue;
}
const portfolioDiff = portfolioReturn - portfolioMean;
const marketDiff = marketReturn - marketMean;
@ -187,12 +193,13 @@ export function treynorRatio(
riskFreeRate: number = 0
): number {
const portfolioBeta = beta(portfolioReturns, marketReturns);
if (portfolioBeta === 0) {
return 0;
}
const portfolioMean = portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
const portfolioMean =
portfolioReturns.reduce((sum, ret) => sum + ret, 0) / portfolioReturns.length;
return (portfolioMean - riskFreeRate) / portfolioBeta;
}
@ -412,7 +419,9 @@ export function riskContribution(
for (let i = 0; i < n; i++) {
let marginalContribution = 0;
const row = covarianceMatrix[i];
if (!row) {continue;}
if (!row) {
continue;
}
for (let j = 0; j < n; j++) {
const weight = weights[j];
@ -442,8 +451,10 @@ export function ulcerIndex(equityCurve: Array<{ value: number; date: Date }>): n
let sumSquaredDrawdown = 0;
const first = equityCurve[0];
if (!first) {return 0;}
if (!first) {
return 0;
}
let peak = first.value;
for (const point of equityCurve) {

View file

@ -540,7 +540,9 @@ export function adx(
for (let i = 1; i < ohlcv.length; i++) {
const current = ohlcv[i];
const previous = ohlcv[i - 1];
if (!current || !previous) {continue;}
if (!current || !previous) {
continue;
}
// True Range
const tr = Math.max(
@ -575,8 +577,10 @@ export function adx(
const atr = atrValues[i];
const plusDMSmoothed = smoothedPlusDM[i];
const minusDMSmoothed = smoothedMinusDM[i];
if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {continue;}
if (atr === undefined || plusDMSmoothed === undefined || minusDMSmoothed === undefined) {
continue;
}
const diPlus = atr > 0 ? (plusDMSmoothed / atr) * 100 : 0;
const diMinus = atr > 0 ? (minusDMSmoothed / atr) * 100 : 0;
@ -602,17 +606,15 @@ export function adx(
/**
* Parabolic SAR
*/
export function parabolicSAR(
ohlcv: OHLCV[],
step: number = 0.02,
maxStep: number = 0.2
): number[] {
export function parabolicSAR(ohlcv: OHLCV[], step: number = 0.02, maxStep: number = 0.2): number[] {
if (ohlcv.length < 2) {
return [];
}
const first = ohlcv[0];
if (!first) {return [];}
if (!first) {
return [];
}
const result: number[] = [];
let trend = 1; // 1 for uptrend, -1 for downtrend
@ -625,7 +627,9 @@ export function parabolicSAR(
for (let i = 1; i < ohlcv.length; i++) {
const curr = ohlcv[i];
const prev = ohlcv[i - 1];
if (!curr || !prev) {continue;}
if (!curr || !prev) {
continue;
}
// Calculate new SAR
sar = sar + acceleration * (extremePoint - sar);
@ -834,32 +838,37 @@ export function ultimateOscillator(
// Calculate BP and TR
for (let i = 0; i < ohlcv.length; i++) {
const current = ohlcv[i]!;
if (i === 0) {
bp.push(current.close - current.low);
tr.push(current.high - current.low);
} else {
const previous = ohlcv[i - 1]!;
bp.push(current.close - Math.min(current.low, previous.close));
tr.push(Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
));
tr.push(
Math.max(
current.high - current.low,
Math.abs(current.high - previous.close),
Math.abs(current.low - previous.close)
)
);
}
}
const result: number[] = [];
for (let i = Math.max(period1, period2, period3) - 1; i < ohlcv.length; i++) {
const avg1 = bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg2 = bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg3 = bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg1 =
bp.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period1 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg2 =
bp.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period2 + 1, i + 1).reduce((a, b) => a + b, 0);
const avg3 =
bp.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0) /
tr.slice(i - period3 + 1, i + 1).reduce((a, b) => a + b, 0);
const uo = 100 * ((4 * avg1) + (2 * avg2) + avg3) / (4 + 2 + 1);
const uo = (100 * (4 * avg1 + 2 * avg2 + avg3)) / (4 + 2 + 1);
result.push(uo);
}
@ -880,7 +889,7 @@ export function easeOfMovement(ohlcv: OHLCV[], period: number = 14): number[] {
const current = ohlcv[i]!;
const previous = ohlcv[i - 1]!;
const distance = ((current.high + current.low) / 2) - ((previous.high + previous.low) / 2);
const distance = (current.high + current.low) / 2 - (previous.high + previous.low) / 2;
const boxHeight = current.high - current.low;
const volume = current.volume;
@ -1028,7 +1037,14 @@ export function klingerVolumeOscillator(
const prevTypicalPrice = (previous.high + previous.low + previous.close) / 3;
const trend = typicalPrice > prevTypicalPrice ? 1 : -1;
const vf = current.volume * trend * Math.abs((2 * ((current.close - current.low) - (current.high - current.close))) / (current.high - current.low)) * 100;
const vf =
current.volume *
trend *
Math.abs(
(2 * (current.close - current.low - (current.high - current.close))) /
(current.high - current.low)
) *
100;
volumeForce.push(vf);
}
@ -1137,7 +1153,7 @@ export function stochasticRSI(
smoothD: number = 3
): { k: number[]; d: number[] } {
const rsiValues = rsi(prices, rsiPeriod);
if (rsiValues.length < stochPeriod) {
return { k: [], d: [] };
}
@ -1266,17 +1282,17 @@ export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
// Calculate high-low ranges
const ranges = ohlcv.map(candle => candle.high - candle.low);
// Calculate 9-period EMA of ranges
const ema9 = ema(ranges, 9);
// Calculate 9-period EMA of the EMA (double smoothing)
const emaEma9 = ema(ema9, 9);
// Calculate ratio
const ratios: number[] = [];
const minLength = Math.min(ema9.length, emaEma9.length);
for (let i = 0; i < minLength; i++) {
const singleEMA = ema9[i];
const doubleEMA = emaEma9[i];
@ -1299,9 +1315,9 @@ export function massIndex(ohlcv: OHLCV[], period: number = 25): number[] {
* Coppock Curve
*/
export function coppockCurve(
prices: number[],
shortROC: number = 11,
longROC: number = 14,
prices: number[],
shortROC: number = 11,
longROC: number = 14,
wma: number = 10
): number[] {
const roc1 = roc(prices, shortROC);

View file

@ -1,96 +1,94 @@
/**
* Enhanced fetch wrapper with proxy support and automatic debug logging
* Drop-in replacement for native fetch with additional features
*/
export interface BunRequestInit extends RequestInit {
proxy?: string;
}
export interface FetchOptions extends RequestInit {
logger?: any;
proxy?: string | null;
timeout?: number;
}
export async function fetch(
input: RequestInfo | URL,
options?: FetchOptions
): Promise<Response> {
const logger = options?.logger || console;
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url;
// Build request options
const requestOptions: RequestInit = {
method: options?.method || 'GET',
headers: options?.headers || {},
body: options?.body,
signal: options?.signal,
credentials: options?.credentials,
cache: options?.cache,
redirect: options?.redirect,
referrer: options?.referrer,
referrerPolicy: options?.referrerPolicy,
integrity: options?.integrity,
keepalive: options?.keepalive,
mode: options?.mode,
};
// Handle proxy for Bun
if (options?.proxy) {
// Bun supports proxy via fetch options
(requestOptions as BunRequestInit).proxy = options.proxy;
}
// Handle timeout
if (options?.timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
requestOptions.signal = controller.signal;
try {
const response = await performFetch(input, requestOptions, logger, url);
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
return performFetch(input, requestOptions, logger, url);
}
async function performFetch(
input: RequestInfo | URL,
requestOptions: RequestInit,
logger: any,
url: string
): Promise<Response> {
logger.debug('HTTP request', {
method: requestOptions.method,
url,
headers: requestOptions.headers,
proxy: (requestOptions as BunRequestInit).proxy || null
});
try {
const response = await globalThis.fetch(input, requestOptions);
logger.debug('HTTP response', {
url,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries())
});
return response;
} catch (error) {
logger.debug('HTTP error', {
url,
error: error instanceof Error ? error.message : String(error),
name: error instanceof Error ? error.name : 'Unknown'
});
throw error;
}
}
/**
* Enhanced fetch wrapper with proxy support and automatic debug logging
* Drop-in replacement for native fetch with additional features
*/
export interface BunRequestInit extends RequestInit {
proxy?: string;
}
export interface FetchOptions extends RequestInit {
logger?: any;
proxy?: string | null;
timeout?: number;
}
export async function fetch(input: RequestInfo | URL, options?: FetchOptions): Promise<Response> {
const logger = options?.logger || console;
const url =
typeof input === 'string' ? input : input instanceof URL ? input.href : (input as Request).url;
// Build request options
const requestOptions: RequestInit = {
method: options?.method || 'GET',
headers: options?.headers || {},
body: options?.body,
signal: options?.signal,
credentials: options?.credentials,
cache: options?.cache,
redirect: options?.redirect,
referrer: options?.referrer,
referrerPolicy: options?.referrerPolicy,
integrity: options?.integrity,
keepalive: options?.keepalive,
mode: options?.mode,
};
// Handle proxy for Bun
if (options?.proxy) {
// Bun supports proxy via fetch options
(requestOptions as BunRequestInit).proxy = options.proxy;
}
// Handle timeout
if (options?.timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
requestOptions.signal = controller.signal;
try {
const response = await performFetch(input, requestOptions, logger, url);
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
return performFetch(input, requestOptions, logger, url);
}
async function performFetch(
input: RequestInfo | URL,
requestOptions: RequestInit,
logger: any,
url: string
): Promise<Response> {
logger.debug('HTTP request', {
method: requestOptions.method,
url,
headers: requestOptions.headers,
proxy: (requestOptions as BunRequestInit).proxy || null,
});
try {
const response = await globalThis.fetch(input, requestOptions);
logger.debug('HTTP response', {
url,
status: response.status,
statusText: response.statusText,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
});
return response;
} catch (error) {
logger.debug('HTTP error', {
url,
error: error instanceof Error ? error.message : String(error),
name: error instanceof Error ? error.name : 'Unknown',
});
throw error;
}
}

View file

@ -3,7 +3,7 @@
* These functions demonstrate how to use generic types with OHLCV data
*/
import type { OHLCV, HasClose, HasOHLC, HasVolume } from '@stock-bot/types';
import type { HasClose, HasOHLC, HasVolume, OHLCV } from '@stock-bot/types';
/**
* Extract close prices from any data structure that has a close field
@ -16,7 +16,9 @@ export function extractCloses<T extends HasClose>(data: T[]): number[] {
/**
* Extract OHLC prices from any data structure that has OHLC fields
*/
export function extractOHLC<T extends HasOHLC>(data: T[]): {
export function extractOHLC<T extends HasOHLC>(
data: T[]
): {
opens: number[];
highs: number[];
lows: number[];
@ -43,12 +45,12 @@ export function extractVolumes<T extends HasVolume>(data: T[]): number[] {
export function calculateSMA<T extends HasClose>(data: T[], period: number): number[] {
const closes = extractCloses(data);
const result: number[] = [];
for (let i = period - 1; i < closes.length; i++) {
const sum = closes.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
result.push(sum / period);
}
return result;
}
@ -64,7 +66,7 @@ export function calculateTypicalPrice<T extends HasOHLC>(data: T[]): number[] {
*/
export function calculateTrueRange<T extends HasOHLC>(data: T[]): number[] {
const result: number[] = [];
for (let i = 0; i < data.length; i++) {
if (i === 0) {
result.push(data[i]!.high - data[i]!.low);
@ -79,7 +81,7 @@ export function calculateTrueRange<T extends HasOHLC>(data: T[]): number[] {
result.push(tr);
}
}
return result;
}
@ -89,7 +91,7 @@ export function calculateTrueRange<T extends HasOHLC>(data: T[]): number[] {
export function calculateReturns<T extends HasClose>(data: T[]): number[] {
const closes = extractCloses(data);
const returns: number[] = [];
for (let i = 1; i < closes.length; i++) {
const current = closes[i]!;
const previous = closes[i - 1]!;
@ -99,7 +101,7 @@ export function calculateReturns<T extends HasClose>(data: T[]): number[] {
returns.push(0);
}
}
return returns;
}
@ -109,7 +111,7 @@ export function calculateReturns<T extends HasClose>(data: T[]): number[] {
export function calculateLogReturns<T extends HasClose>(data: T[]): number[] {
const closes = extractCloses(data);
const logReturns: number[] = [];
for (let i = 1; i < closes.length; i++) {
const current = closes[i]!;
const previous = closes[i - 1]!;
@ -119,7 +121,7 @@ export function calculateLogReturns<T extends HasClose>(data: T[]): number[] {
logReturns.push(0);
}
}
return logReturns;
}
@ -130,19 +132,19 @@ export function calculateVWAP<T extends HasOHLC & HasVolume>(data: T[]): number[
const result: number[] = [];
let cumulativeVolumePrice = 0;
let cumulativeVolume = 0;
for (const item of data) {
const typicalPrice = (item.high + item.low + item.close) / 3;
cumulativeVolumePrice += typicalPrice * item.volume;
cumulativeVolume += item.volume;
if (cumulativeVolume > 0) {
result.push(cumulativeVolumePrice / cumulativeVolume);
} else {
result.push(typicalPrice);
}
}
return result;
}
@ -156,11 +158,7 @@ export function filterBySymbol(data: OHLCV[], symbol: string): OHLCV[] {
/**
* Filter OHLCV data by time range
*/
export function filterByTimeRange(
data: OHLCV[],
startTime: number,
endTime: number
): OHLCV[] {
export function filterByTimeRange(data: OHLCV[], startTime: number, endTime: number): OHLCV[] {
return data.filter(item => item.timestamp >= startTime && item.timestamp <= endTime);
}
@ -169,14 +167,14 @@ export function filterByTimeRange(
*/
export function groupBySymbol(data: OHLCV[]): Record<string, OHLCV[]> {
const grouped: Record<string, OHLCV[]> = {};
for (const item of data) {
if (!grouped[item.symbol]) {
grouped[item.symbol] = [];
}
grouped[item.symbol]!.push(item);
}
return grouped;
}
@ -186,6 +184,6 @@ export function groupBySymbol(data: OHLCV[]): Record<string, OHLCV[]> {
export function convertTimestamps(data: OHLCV[]): Array<OHLCV & { date: Date }> {
return data.map(item => ({
...item,
date: new Date(item.timestamp)
date: new Date(item.timestamp),
}));
}
}

View file

@ -1,30 +1,30 @@
/**
* User Agent utility for generating random user agents
*/
// Simple list of common user agents to avoid external dependency
const USER_AGENTS = [
// Chrome on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
// Chrome on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
// Firefox on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:119.0) Gecko/20100101 Firefox/119.0',
// Firefox on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) Gecko/20100101 Firefox/120.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) Gecko/20100101 Firefox/119.0',
// Safari on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
// Edge on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0',
];
export function getRandomUserAgent(): string {
const index = Math.floor(Math.random() * USER_AGENTS.length);
return USER_AGENTS[index]!;
}
/**
* User Agent utility for generating random user agents
*/
// Simple list of common user agents to avoid external dependency
const USER_AGENTS = [
// Chrome on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
// Chrome on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
// Firefox on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:119.0) Gecko/20100101 Firefox/119.0',
// Firefox on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) Gecko/20100101 Firefox/120.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) Gecko/20100101 Firefox/119.0',
// Safari on Mac
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
// Edge on Windows
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0',
];
export function getRandomUserAgent(): string {
const index = Math.floor(Math.random() * USER_AGENTS.length);
return USER_AGENTS[index]!;
}