| |
| |
| |
| |
|
|
| interface AdvancedFeatures { |
| |
| close_lag_1?: number |
| close_lag_2?: number |
| close_lag_3?: number |
| close_lag_5?: number |
| close_lag_10?: number |
| |
| |
| price_change_1d?: number |
| price_change_3d?: number |
| price_change_5d?: number |
| price_change_10d?: number |
| price_change_20d?: number |
| |
| |
| sma5_sma20_ratio?: number |
| price_sma20_ratio?: number |
| price_sma50_ratio?: number |
| |
| |
| volatility_5d?: number |
| volatility_10d?: number |
| volatility_20d?: number |
| price_range_5d?: number |
| price_range_10d?: number |
| |
| |
| momentum_5d?: number |
| momentum_10d?: number |
| momentum_20d?: number |
| |
| |
| volume_change_1d?: number |
| volume_change_5d?: number |
| volume_ma_5?: number |
| volume_ma_10?: number |
| volume_ma_ratio_5?: number |
| |
| |
| bb_position?: number |
| |
| |
| macd_hist_change?: number |
| } |
|
|
| interface PriceData { |
| date: string |
| open: number |
| high: number |
| low: number |
| close: number |
| volume: number |
| } |
|
|
| interface DataQuality { |
| score: number |
| length_factor: number |
| nan_factor: number |
| volume_factor: number |
| ohlc_factor: number |
| } |
|
|
| interface ConfidenceFactors { |
| r2_component: number |
| direction_component: number |
| mape_component: number |
| data_quality_component: number |
| volatility_component: number |
| model_bonus: number |
| } |
|
|
| |
| |
| |
| export function createAdvancedFeatures( |
| data: PriceData[], |
| indicators?: Record<string, unknown> |
| ): AdvancedFeatures { |
| const features: AdvancedFeatures = {} |
| |
| if (data.length < 30) return features |
| |
| const closes = data.map(d => d.close) |
| const volumes = data.map(d => d.volume) |
| const highs = data.map(d => d.high) |
| const lows = data.map(d => d.low) |
| |
| const lastIdx = data.length - 1 |
| |
| |
| if (lastIdx >= 1) features.close_lag_1 = closes[lastIdx - 1] |
| if (lastIdx >= 2) features.close_lag_2 = closes[lastIdx - 2] |
| if (lastIdx >= 3) features.close_lag_3 = closes[lastIdx - 3] |
| if (lastIdx >= 5) features.close_lag_5 = closes[lastIdx - 5] |
| if (lastIdx >= 10) features.close_lag_10 = closes[lastIdx - 10] |
| |
| |
| if (lastIdx >= 1) { |
| features.price_change_1d = (closes[lastIdx] - closes[lastIdx - 1]) / closes[lastIdx - 1] |
| } |
| if (lastIdx >= 3) { |
| features.price_change_3d = (closes[lastIdx] - closes[lastIdx - 3]) / closes[lastIdx - 3] |
| } |
| if (lastIdx >= 5) { |
| features.price_change_5d = (closes[lastIdx] - closes[lastIdx - 5]) / closes[lastIdx - 5] |
| } |
| if (lastIdx >= 10) { |
| features.price_change_10d = (closes[lastIdx] - closes[lastIdx - 10]) / closes[lastIdx - 10] |
| } |
| if (lastIdx >= 20) { |
| features.price_change_20d = (closes[lastIdx] - closes[lastIdx - 20]) / closes[lastIdx - 20] |
| } |
| |
| |
| const sma5 = calculateSMA(closes, 5) |
| const sma20 = calculateSMA(closes, 20) |
| const sma50 = calculateSMA(closes, 50) |
| |
| if (sma5 && sma20) { |
| features.sma5_sma20_ratio = sma5 / sma20 |
| } |
| if (sma20) { |
| features.price_sma20_ratio = closes[lastIdx] / sma20 |
| } |
| if (sma50) { |
| features.price_sma50_ratio = closes[lastIdx] / sma50 |
| } |
| |
| |
| features.volatility_5d = calculateVolatility(closes, 5) |
| features.volatility_10d = calculateVolatility(closes, 10) |
| features.volatility_20d = calculateVolatility(closes, 20) |
| |
| features.price_range_5d = calculatePriceRange(highs, lows, closes, 5) |
| features.price_range_10d = calculatePriceRange(highs, lows, closes, 10) |
| |
| |
| if (lastIdx >= 5) { |
| features.momentum_5d = closes[lastIdx] / closes[lastIdx - 5] - 1 |
| } |
| if (lastIdx >= 10) { |
| features.momentum_10d = closes[lastIdx] / closes[lastIdx - 10] - 1 |
| } |
| if (lastIdx >= 20) { |
| features.momentum_20d = closes[lastIdx] / closes[lastIdx - 20] - 1 |
| } |
| |
| |
| if (volumes.length >= 2) { |
| features.volume_change_1d = (volumes[lastIdx] - volumes[lastIdx - 1]) / volumes[lastIdx - 1] |
| } |
| if (volumes.length >= 5) { |
| features.volume_change_5d = (volumes[lastIdx] - volumes[lastIdx - 5]) / volumes[lastIdx - 5] |
| } |
| |
| features.volume_ma_5 = calculateSMA(volumes, 5) |
| features.volume_ma_10 = calculateSMA(volumes, 10) |
| |
| if (features.volume_ma_5) { |
| features.volume_ma_ratio_5 = volumes[lastIdx] / features.volume_ma_5 |
| } |
| |
| |
| if (typeof indicators?.bb_upper === 'number' && typeof indicators?.bb_lower === 'number') { |
| const bbRange = indicators.bb_upper - indicators.bb_lower |
| if (bbRange > 0) { |
| features.bb_position = (closes[lastIdx] - indicators.bb_lower) / bbRange |
| } |
| } |
| |
| |
| if (typeof indicators?.macd_hist === 'number' && typeof indicators?.macd_hist_prev === 'number') { |
| features.macd_hist_change = indicators.macd_hist - indicators.macd_hist_prev |
| } |
| |
| return features |
| } |
|
|
| |
| |
| |
| function calculateSMA(data: number[], period: number): number | undefined { |
| if (data.length < period) return undefined |
| |
| const slice = data.slice(-period) |
| const sum = slice.reduce((a, b) => a + b, 0) |
| return sum / period |
| } |
|
|
| |
| |
| |
| function calculateVolatility(prices: number[], period: number): number | undefined { |
| if (prices.length < period) return undefined |
| |
| const slice = prices.slice(-period) |
| const mean = slice.reduce((a, b) => a + b, 0) / period |
| const variance = slice.reduce((sum, price) => sum + Math.pow(price - mean, 2), 0) / period |
| return Math.sqrt(variance) |
| } |
|
|
| |
| |
| |
| function calculatePriceRange( |
| highs: number[], |
| lows: number[], |
| closes: number[], |
| period: number |
| ): number | undefined { |
| if (highs.length < period) return undefined |
| |
| const highSlice = highs.slice(-period) |
| const lowSlice = lows.slice(-period) |
| const currentClose = closes[closes.length - 1] |
| |
| const maxHigh = Math.max(...highSlice) |
| const minLow = Math.min(...lowSlice) |
| |
| return (maxHigh - minLow) / currentClose |
| } |
|
|
| |
| |
| |
| export function calculateDataQualityScore(data: PriceData[]): DataQuality { |
| |
| const length_factor = Math.min(1.0, data.length / 252) |
| |
| |
| const zeroVolumeCount = data.filter(d => d.volume === 0).length |
| const zeroVolumeRatio = zeroVolumeCount / data.length |
| const volume_factor = 1.0 - Math.min(0.3, zeroVolumeRatio) |
| |
| |
| const invalidOHLC = data.filter(d => |
| d.high < d.low || |
| d.high < d.open || |
| d.high < d.close || |
| d.low > d.open || |
| d.low > d.close |
| ).length |
| const invalidRatio = invalidOHLC / data.length |
| const ohlc_factor = 1.0 - Math.min(0.2, invalidRatio) |
| |
| |
| const nan_factor = 1.0 |
| |
| |
| const score = ( |
| length_factor * 0.4 + |
| nan_factor * 0.3 + |
| volume_factor * 0.2 + |
| ohlc_factor * 0.1 |
| ) |
| |
| return { |
| score: Math.max(0, Math.min(1, score)), |
| length_factor, |
| nan_factor, |
| volume_factor, |
| ohlc_factor |
| } |
| } |
|
|
| |
| |
| |
| export function calculateEnhancedConfidence(params: { |
| prediction_accuracy?: number // 0-1 |
| direction_accuracy?: number // 0-1 |
| mape?: number // Mean Absolute Percentage Error |
| data_quality_score: number // 0-1 |
| volatility: number // Annual volatility % |
| model_type: string |
| advanced_features_count?: number |
| }): { confidence: number; factors: ConfidenceFactors } { |
| |
| const { |
| prediction_accuracy = 0.5, |
| direction_accuracy = 0.5, |
| mape = 10, |
| data_quality_score, |
| volatility, |
| model_type, |
| advanced_features_count = 0 |
| } = params |
| |
| |
| const r2_component = Math.max(0, Math.min(100, prediction_accuracy * 100)) |
| |
| |
| const direction_component = direction_accuracy * 100 |
| |
| |
| const mape_component = Math.max(0, 100 - mape * 2) |
| |
| |
| const data_quality_component = data_quality_score * 100 |
| |
| |
| const volatility_component = Math.max(0, 100 - volatility * 5) |
| |
| |
| let model_bonus = 0 |
| if (model_type === 'Ensemble') { |
| model_bonus = 5 |
| } else if (model_type === 'Hybrid') { |
| model_bonus = 3 |
| } else if (model_type === 'XGBoost' || model_type === 'LightGBM') { |
| model_bonus = 2 |
| } else if (model_type === 'RandomForest') { |
| model_bonus = 1 |
| } |
| |
| |
| const features_bonus = Math.min(3, advanced_features_count / 10) |
| |
| |
| const confidence = ( |
| r2_component * 0.30 + |
| direction_component * 0.25 + |
| mape_component * 0.20 + |
| data_quality_component * 0.15 + |
| volatility_component * 0.10 + |
| model_bonus + |
| features_bonus |
| ) |
| |
| |
| const final_confidence = Math.max(30, Math.min(95, confidence)) |
| |
| return { |
| confidence: final_confidence, |
| factors: { |
| r2_component, |
| direction_component, |
| mape_component, |
| data_quality_component, |
| volatility_component, |
| model_bonus: model_bonus + features_bonus |
| } |
| } |
| } |
|
|
| |
| |
| |
| export interface ChartPattern { |
| type: string |
| description: string |
| signal: 'bullish' | 'bearish' | 'neutral' |
| strength: number |
| start_idx?: number |
| end_idx?: number |
| } |
|
|
| export function detectChartPatterns(data: PriceData[]): ChartPattern[] { |
| const patterns: ChartPattern[] = [] |
| |
| if (data.length < 40) return patterns |
| |
| const closes = data.map(d => d.close) |
| const window = Math.min(40, closes.length) |
| const lastPrices = closes.slice(-window) |
| |
| |
| const localMins: Array<{idx: number, val: number}> = [] |
| for (let i = 1; i < lastPrices.length - 1; i++) { |
| if (lastPrices[i] < lastPrices[i - 1] && lastPrices[i] < lastPrices[i + 1]) { |
| localMins.push({ idx: i, val: lastPrices[i] }) |
| } |
| } |
| |
| if (localMins.length >= 2) { |
| const lastTwoMins = localMins.slice(-2) |
| const [min1, min2] = lastTwoMins |
| |
| const priceDiffPct = Math.abs(min1.val - min2.val) / min1.val * 100 |
| const dayDiff = min2.idx - min1.idx |
| |
| if (priceDiffPct < 5 && dayDiff >= 5) { |
| patterns.push({ |
| type: 'double_bottom', |
| description: 'Çift Dip Formasyonu', |
| signal: 'bullish', |
| strength: 0.7, |
| start_idx: min1.idx, |
| end_idx: min2.idx |
| }) |
| } |
| } |
| |
| |
| if (localMins.length >= 3) { |
| const peaks: Array<{idx: number, val: number}> = [] |
| for (let i = 1; i < lastPrices.length - 1; i++) { |
| if (lastPrices[i] > lastPrices[i - 1] && lastPrices[i] > lastPrices[i + 1]) { |
| peaks.push({ idx: i, val: lastPrices[i] }) |
| } |
| } |
| |
| if (peaks.length >= 3) { |
| const lastThreePeaks = peaks.slice(-3) |
| const [left, head, right] = lastThreePeaks |
| |
| |
| if (head.val > left.val && head.val > right.val) { |
| const shouldersDiff = Math.abs(left.val - right.val) / left.val * 100 |
| if (shouldersDiff < 5) { |
| patterns.push({ |
| type: 'head_shoulders', |
| description: 'Omuz Baş Omuz', |
| signal: 'bearish', |
| strength: 0.75 |
| }) |
| } |
| } |
| } |
| } |
| |
| |
| const recentPrices = closes.slice(-20) |
| if (recentPrices.length >= 20) { |
| const firstHalf = recentPrices.slice(0, 10) |
| const secondHalf = recentPrices.slice(10) |
| |
| const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length |
| const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length |
| |
| const trend = (secondAvg - firstAvg) / firstAvg |
| |
| if (trend > 0.05) { |
| patterns.push({ |
| type: 'ascending_trend', |
| description: 'Yükselen Trend', |
| signal: 'bullish', |
| strength: Math.min(0.9, trend * 10) |
| }) |
| } else if (trend < -0.05) { |
| patterns.push({ |
| type: 'descending_trend', |
| description: 'Düşen Trend', |
| signal: 'bearish', |
| strength: Math.min(0.9, Math.abs(trend) * 10) |
| }) |
| } |
| } |
| |
| return patterns |
| } |
|
|
| |
| |
| |
| export function calculateComprehensiveMLScore(params: { |
| trend_score: number |
| momentum_score: number |
| volatility_score: number |
| pattern_score: number |
| advanced_features: AdvancedFeatures |
| chart_patterns: ChartPattern[] |
| data_quality: DataQuality |
| }): { |
| total_score: number |
| components: { |
| trend: number |
| momentum: number |
| volatility: number |
| pattern: number |
| features: number |
| quality: number |
| } |
| recommendation: 'BUY' | 'SELL' | 'HOLD' |
| strength: number |
| } { |
| const { |
| trend_score, |
| momentum_score, |
| volatility_score, |
| pattern_score, |
| advanced_features, |
| chart_patterns, |
| data_quality |
| } = params |
| |
| |
| const features_count = Object.keys(advanced_features).length |
| const features_score = Math.min(100, features_count * 2) |
| |
| |
| const bullish_patterns = chart_patterns.filter(p => p.signal === 'bullish') |
| const bearish_patterns = chart_patterns.filter(p => p.signal === 'bearish') |
| const patterns_impact = ( |
| bullish_patterns.reduce((sum, p) => sum + p.strength, 0) - |
| bearish_patterns.reduce((sum, p) => sum + p.strength, 0) |
| ) * 10 |
| |
| |
| const quality_multiplier = 0.5 + (data_quality.score * 0.5) |
| |
| |
| const components = { |
| trend: trend_score * 0.30, |
| momentum: momentum_score * 0.25, |
| volatility: volatility_score * 0.15, |
| pattern: pattern_score * 0.10, |
| features: features_score * 0.10, |
| quality: patterns_impact * 0.10 |
| } |
| |
| const raw_score = Object.values(components).reduce((a, b) => a + b, 0) |
| const total_score = raw_score * quality_multiplier |
| |
| |
| let recommendation: 'BUY' | 'SELL' | 'HOLD' |
| let strength: number |
| |
| if (total_score > 60) { |
| recommendation = 'BUY' |
| strength = Math.min(100, total_score) |
| } else if (total_score < 40) { |
| recommendation = 'SELL' |
| strength = Math.min(100, 100 - total_score) |
| } else { |
| recommendation = 'HOLD' |
| strength = 50 |
| } |
| |
| return { |
| total_score, |
| components, |
| recommendation, |
| strength |
| } |
| } |
|
|