format
This commit is contained in:
parent
d858222af7
commit
7d9044ab29
202 changed files with 10755 additions and 10972 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]!;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue