/**
* Price Channel Indicator
* Creates support and resistance channels based on highest high and lowest low
*/
export interface PriceChannelData {
// Channel boundaries
upperChannel: number // Highest high in period
lowerChannel: number // Lowest low in period
middleChannel: number // Midpoint of channel
// Channel width
channelWidth: number // Width of channel (%)
channelHeight: number // Absolute height of channel
// Current price position
position: 'upper_third' | 'middle_third' | 'lower_third' | 'above_channel' | 'below_channel'
// Distance from channels
distanceFromUpper: number // % distance from upper channel
distanceFromLower: number // % distance from lower channel
// Trend analysis
trend: 'uptrend' | 'downtrend' | 'sideways'
// Breakout signals
upperBreakout: boolean // Price broke above upper channel
lowerBreakout: boolean // Price broke below lower channel
// Support/resistance strength
upperStrength: number // Upper channel strength (0-100)
lowerStrength: number // Lower channel strength (0-100)
}
/**
* Calculate Price Channel
* @param highs Array of high prices
* @param lows Array of low prices
* @param closes Array of close prices
* @param period Period for channel calculation (default 20)
* @returns PriceChannelData object
*/
export function calculatePriceChannel(
highs: number[],
lows: number[],
closes: number[],
period: number = 20
): PriceChannelData | null {
// Minimum 3 data points required
if (highs.length < 3 || lows.length < 3 || closes.length < 3) {
return null
}
// Use adaptive period
const effectivePeriod = Math.min(period, highs.length)
// Use the most recent 'effectivePeriod' data points
const recentHighs = highs.slice(-effectivePeriod)
const recentLows = lows.slice(-effectivePeriod)
// Calculate channel boundaries
const upperChannel = Math.max(...recentHighs)
const lowerChannel = Math.min(...recentLows)
const middleChannel = (upperChannel + lowerChannel) / 2
// Calculate channel dimensions
const channelHeight = upperChannel - lowerChannel
const channelWidth = channelHeight > 0 ? (channelHeight / middleChannel) * 100 : 0
// Get current price
const currentPrice = closes[closes.length - 1]
const currentHigh = highs[highs.length - 1]
const currentLow = lows[lows.length - 1]
// Determine position within channel
let position: 'upper_third' | 'middle_third' | 'lower_third' | 'above_channel' | 'below_channel'
if (currentPrice > upperChannel) {
position = 'above_channel'
} else if (currentPrice < lowerChannel) {
position = 'below_channel'
} else {
const channelRange = upperChannel - lowerChannel
const positionFromBottom = currentPrice - lowerChannel
const relativePosition = positionFromBottom / channelRange
if (relativePosition > 0.67) {
position = 'upper_third'
} else if (relativePosition > 0.33) {
position = 'middle_third'
} else {
position = 'lower_third'
}
}
// Calculate distances
const distanceFromUpper = ((currentPrice - upperChannel) / upperChannel) * 100
const distanceFromLower = ((lowerChannel - currentPrice) / lowerChannel) * 100
// Determine trend based on channel slope
let trend: 'uptrend' | 'downtrend' | 'sideways' = 'sideways'
if (highs.length >= effectivePeriod * 2 && lows.length >= effectivePeriod * 2) {
// Compare with previous channel
const prevHighs = highs.slice(-effectivePeriod * 2, -effectivePeriod)
const prevLows = lows.slice(-effectivePeriod * 2, -effectivePeriod)
const prevUpperChannel = Math.max(...prevHighs)
const prevLowerChannel = Math.min(...prevLows)
if (upperChannel > prevUpperChannel && lowerChannel > prevLowerChannel) {
trend = 'uptrend'
} else if (upperChannel < prevUpperChannel && lowerChannel < prevLowerChannel) {
trend = 'downtrend'
}
}
// Check for breakouts
const upperBreakout = currentHigh > upperChannel * 1.001 // Small tolerance for floating point
const lowerBreakout = currentLow < lowerChannel * 0.999
// Calculate support/resistance strength based on touches
let upperTouches = 0
let lowerTouches = 0
for (let i = 1; i < recentHighs.length; i++) {
if (Math.abs(recentHighs[i] - upperChannel) / upperChannel < 0.001) {
upperTouches++
}
if (Math.abs(recentLows[i] - lowerChannel) / lowerChannel < 0.001) {
lowerTouches++
}
}
const upperStrength = Math.min(100, (upperTouches / period) * 200)
const lowerStrength = Math.min(100, (lowerTouches / period) * 200)
return {
upperChannel,
lowerChannel,
middleChannel,
channelWidth,
channelHeight,
position,
distanceFromUpper,
distanceFromLower,
trend,
upperBreakout,
lowerBreakout,
upperStrength,
lowerStrength
}
}
/**
* Get Price Channel signal
* @param channel PriceChannelData object
* @returns Trading signal
*/
export function getPriceChannelSignal(channel: PriceChannelData): 'buy' | 'sell' | 'neutral' {
const { position, upperBreakout, lowerBreakout, trend } = channel
// Breakout signals have highest priority
if (upperBreakout && trend === 'uptrend') {
return 'buy' // Continuation breakout
} else if (lowerBreakout && trend === 'downtrend') {
return 'sell' // Continuation breakout
} else if (upperBreakout) {
return 'sell' // Reversal breakout
} else if (lowerBreakout) {
return 'buy' // Reversal breakout
}
// Position-based signals
if (position === 'upper_third' && trend === 'downtrend') {
return 'sell' // Overbought in downtrend
} else if (position === 'lower_third' && trend === 'uptrend') {
return 'buy' // Oversold in uptrend
} else if (position === 'below_channel') {
return 'buy' // Extreme oversold
} else if (position === 'above_channel') {
return 'sell' // Extreme overbought
}
return 'neutral'
}
/**
* Calculate channel efficiency ratio
* @param channel PriceChannelData object
* @param closes Array of closing prices
* @returns Efficiency ratio (0-100)
*/
export function getChannelEfficiencyRatio(channel: PriceChannelData, closes: number[]): number {
const recentCloses = closes.slice(-20) // Last 20 periods
let efficientMoves = 0
for (let i = 1; i < recentCloses.length; i++) {
const move = recentCloses[i] - recentCloses[i - 1]
const channelRange = channel.upperChannel - channel.lowerChannel
if (Math.abs(move) > channelRange * 0.1) { // Moves larger than 10% of channel
efficientMoves++
}
}
return (efficientMoves / recentCloses.length) * 100
}