/**
* Market Structure Analysis
* detectChangeOfCharacter function
*/
import { HistoricalDataPoint } from '../types'
export interface ChangeOfCharacterResult {
structure: 'bullish' | 'bearish' | 'neutral'
coc: 'bullish' | 'bearish' | 'none'
lastSwingHigh: number | null
lastSwingLow: number | null
structureStrength: number
reversalSignal: boolean
swingHighs: Array<{ price: number; index: number; timestamp: number }>
swingLows: Array<{ price: number; index: number; timestamp: number }>
timestamp: number
}
/**
* Detect Change of Character (COC) in market structure
* COC indicates shift in market structure - possible trend reversal
*/
export function detectChangeOfCharacter(
historicalData: HistoricalDataPoint[],
currentPrice: number
): ChangeOfCharacterResult | null {
// Minimum 10 data points required (reduced from 20)
if (!historicalData || historicalData.length < 10 || !currentPrice || currentPrice <= 0) {
return null
}
const highs = historicalData.map(d => d.high)
const lows = historicalData.map(d => d.low)
const closes = historicalData.map(d => d.close)
// Detect swing highs and lows (similar to calculateSupportResistance)
const swingHighs: Array<{ price: number; index: number; timestamp: number }> = []
const swingLows: Array<{ price: number; index: number; timestamp: number }> = []
// Adaptive swing detection - use 2 candles for smaller datasets, 3 for larger
const lookback = closes.length >= 20 ? 3 : 2
for (let i = lookback; i < closes.length - lookback; i++) {
// Swing high: higher than previous and next candles
let isSwingHigh = true
let isSwingLow = true
for (let j = 1; j <= lookback; j++) {
if (highs[i] <= highs[i - j] || highs[i] <= highs[i + j]) {
isSwingHigh = false
}
if (lows[i] >= lows[i - j] || lows[i] >= lows[i + j]) {
isSwingLow = false
}
}
if (isSwingHigh) {
swingHighs.push({
price: highs[i],
index: i,
timestamp: historicalData[i].time || Date.now()
})
}
if (isSwingLow) {
swingLows.push({
price: lows[i],
index: i,
timestamp: historicalData[i].time || Date.now()
})
}
}
if (swingHighs.length < 2 || swingLows.length < 2) {
return {
structure: 'neutral',
coc: 'none',
lastSwingHigh: swingHighs.length > 0 ? swingHighs[swingHighs.length - 1].price : null,
lastSwingLow: swingLows.length > 0 ? swingLows[swingLows.length - 1].price : null,
structureStrength: 0,
reversalSignal: false,
swingHighs: [],
swingLows: [],
timestamp: Date.now()
}
}
// Get recent swing points (last 5 swings)
const recentSwingHighs = swingHighs.slice(-5)
const recentSwingLows = swingLows.slice(-5)
// Determine market structure
let structure: 'bullish' | 'bearish' | 'neutral' = 'neutral'
let coc: 'bullish' | 'bearish' | 'none' = 'none'
let reversalSignal = false
// Analyze structure: HH (Higher High), HL (Higher Low), LH (Lower High), LL (Lower Low)
if (recentSwingHighs.length >= 2 && recentSwingLows.length >= 2) {
const lastSwingHigh = recentSwingHighs[recentSwingHighs.length - 1]
const prevSwingHigh = recentSwingHighs[recentSwingHighs.length - 2]
const lastSwingLow = recentSwingLows[recentSwingLows.length - 1]
const prevSwingLow = recentSwingLows[recentSwingLows.length - 2]
// Bullish structure: HH and HL
const isHigherHigh = lastSwingHigh.price > prevSwingHigh.price
const isHigherLow = lastSwingLow.price > prevSwingLow.price
const isLowerHigh = lastSwingHigh.price < prevSwingHigh.price
const isLowerLow = lastSwingLow.price < prevSwingLow.price
if (isHigherHigh && isHigherLow) {
structure = 'bullish' // HH + HL = uptrend
} else if (isLowerHigh && isLowerLow) {
structure = 'bearish' // LH + LL = downtrend
} else if (isHigherHigh && isLowerLow) {
structure = 'bullish' // HH + LL = potential reversal to uptrend
coc = 'bullish' // Bullish COC: LL → breaks to HH
reversalSignal = true
} else if (isLowerHigh && isHigherLow) {
structure = 'bearish' // LH + HL = potential reversal to downtrend
coc = 'bearish' // Bearish COC: HH → breaks to LL
reversalSignal = true
} else {
structure = 'neutral'
}
}
// Check if price broke structure (confirmation of COC)
if (recentSwingHighs.length > 0 && recentSwingLows.length > 0) {
const lastSwingHigh = recentSwingHighs[recentSwingHighs.length - 1]
const lastSwingLow = recentSwingLows[recentSwingLows.length - 1]
// Bullish COC confirmation: price breaks above last swing high after making LL
if (structure === 'bullish' && coc === 'bullish' && currentPrice > lastSwingHigh.price) {
reversalSignal = true
}
// Bearish COC confirmation: price breaks below last swing low after making HH
if (structure === 'bearish' && coc === 'bearish' && currentPrice < lastSwingLow.price) {
reversalSignal = true
}
}
// Calculate structure strength (0-100)
let structureStrength = 0
if (recentSwingHighs.length >= 3 && recentSwingLows.length >= 3) {
// Check consistency of swings
let consistentHighs = 0
let consistentLows = 0
for (let i = 1; i < recentSwingHighs.length; i++) {
if (recentSwingHighs[i].price > recentSwingHighs[i - 1].price) {
consistentHighs++
}
}
for (let i = 1; i < recentSwingLows.length; i++) {
if (recentSwingLows[i].price > recentSwingLows[i - 1].price) {
consistentLows++
}
}
// Strength based on consistency
const highConsistency = recentSwingHighs.length > 1 ? consistentHighs / (recentSwingHighs.length - 1) : 0
const lowConsistency = recentSwingLows.length > 1 ? consistentLows / (recentSwingLows.length - 1) : 0
structureStrength = ((highConsistency + lowConsistency) / 2) * 100
}
return {
structure: structure, // 'bullish' | 'bearish' | 'neutral'
coc: coc, // 'bullish' | 'bearish' | 'none'
lastSwingHigh: recentSwingHighs.length > 0 ? recentSwingHighs[recentSwingHighs.length - 1].price : null,
lastSwingLow: recentSwingLows.length > 0 ? recentSwingLows[recentSwingLows.length - 1].price : null,
structureStrength: structureStrength, // 0-100
reversalSignal: reversalSignal, // boolean
swingHighs: recentSwingHighs,
swingLows: recentSwingLows,
timestamp: Date.now()
}
}