prodigist_ReactiveNotes/src/services/OrderBlockAnalysis.ts
2025-02-01 09:31:17 +00:00

793 lines
No EOL
26 KiB
TypeScript

// src/services/OrderBlockAnalysis.ts
import { TFile } from 'obsidian';
import { MarketDataService, MarketData } from './marketDataService';
import { MarketDataStorage } from './marketDataStorage';
interface SwingPoint {
time: number;
price: number;
type: 'high' | 'low';
index: number;
}
interface TrendLeg {
startIndex: number;
endIndex: number;
startTime: number;
endTime: number;
direction: 'up' | 'down' | 'sideways';
strength: number;
startPrice: number;
endPrice: number;
swingPoints: SwingPoint[];
}
interface OrderBlock {
id: string;
type: 'bullish' | 'bearish';
startTime: number;
endTime: number;
highPrice: number;
lowPrice: number;
volume: number;
impulseMagnitude: number;
priceValueGap: number;
validationMetrics: {
trendAlignment: boolean;
hasBreakOfStructure: boolean;
gapQuality: number; // 0-1 score
impulseStrength: number; // Relative to average movement
};
}
interface AnalysisResult {
symbol: string;
timeframe: string;
analysisTime: number;
marketHours: {
start: number;
end: number;
};
marketData: MarketData[]; // Add this
trendLegs: TrendLeg[]; // Add this
trend: {
direction: 'up' | 'down' | 'none';
strength: number;
swingPoints: SwingPoint[];
};
orderBlocks: OrderBlock[];
}
export class OrderBlockAnalysisService {
private static readonly MARKET_HOURS = {
start: { hour: 9, minute: 30 },
end: { hour: 16, minute: 0 }
};
// Analysis specific params
private static readonly ANALYSIS_PARAMS = {
minSwingPoints: 4,
lookbackPeriods: 20,
minImpulseStrength: 1.5,
minGapSize: 0.1, // 10% of average candle size
trendStrengthThreshold: 0.6
};
private static readonly TREND_PARAMS = {
minSwingPoints: 2, // Reduced minimum swing points
trendThreshold: 0.4, // Lowered threshold
minMovementSize: 0.1, // Minimum % move
swingPointConfirmation: 1, // Candles to confirm swing
momentumThreshold: 0.6, // New: Momentum strength threshold
priceChangeThreshold: 0.001 // New: Minimum price change (0.1%)
};
constructor(
private storage: MarketDataStorage,
private marketDataService: MarketDataService
) {}
/**
* Main analysis function for a given symbol and timeframe
*/
async analyzeOrderBlocks(
symbol: string,
timeframe: string,
noteFile: TFile
): Promise<AnalysisResult> {
try{
const today = new Date();
const startTime = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate()-1,
OrderBlockAnalysisService.MARKET_HOURS.start.hour,
OrderBlockAnalysisService.MARKET_HOURS.start.minute
).getTime();
const endTime = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate()-1,
OrderBlockAnalysisService.MARKET_HOURS.end.hour,
OrderBlockAnalysisService.MARKET_HOURS.end.minute
).getTime();
const config = {
interval: timeframe as any,
startTime,
endTime
};
// Use the marketDataService instance for fetching
const data = await MarketDataService.getMarketData(symbol, config, this.storage);
// Apply market hours filter
const marketHoursData = this.filterMarketHours(data);
const trend = this.analyzeTrend(marketHoursData);
const orderBlocks = this.findOrderBlocks(marketHoursData, trend);
return {
symbol,
timeframe,
analysisTime: Date.now(),
marketHours: {
start: startTime,
end: endTime
},
marketData: marketHoursData,
trendLegs: this.analyzeTrendLegs(marketHoursData),
trend,
orderBlocks
};
} catch (error) {
console.error('Error analyzing order blocks:', error);
throw error;
}
}
/**
* Filter data for market hours only
*/
private filterMarketHours(data: MarketData[]): MarketData[] {
return data.filter(candle => {
const date = new Date(candle.time);
const hours = date.getHours();
const minutes = date.getMinutes();
if (hours < OrderBlockAnalysisService.MARKET_HOURS.start.hour ||
hours > OrderBlockAnalysisService.MARKET_HOURS.end.hour) return false;
if (hours === OrderBlockAnalysisService.MARKET_HOURS.start.hour &&
minutes < OrderBlockAnalysisService.MARKET_HOURS.start.minute) return false;
if (hours === OrderBlockAnalysisService.MARKET_HOURS.end.hour &&
minutes >= OrderBlockAnalysisService.MARKET_HOURS.end.minute) return false;
return true;
});
}
/**
* Enhanced trend analysis that considers both structure and momentum
*/
private analyzeTrend(data: MarketData[]): {
direction: 'up' | 'down' | 'none';
strength: number;
swingPoints: SwingPoint[];
} {
if (data.length < 10) return { direction: 'none', strength: 0, swingPoints: [] };
const atr = this.calculateATR(data, 14);
const swingPoints = this.findSignificantSwings(data, atr);
// Calculate overall momentum
const momentum = this.calculateMomentum(data);
// Calculate price movement
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
const absolutePriceChange = Math.abs(priceChange);
// Only proceed with detailed analysis if we have significant price movement
if (absolutePriceChange < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
return { direction: 'none', strength: 0, swingPoints };
}
// Get structural analysis
const structure = this.analyzeTrendStructure(swingPoints, atr);
// Combine structural and momentum analysis
const direction = this.determineOverallTrend(structure, momentum, priceChange);
const strength = this.calculateOverallStrength(structure.strength, momentum.strength);
return {
direction,
strength,
swingPoints
};
}
/**
* Calculate Average True Range
*/
private calculateATR(data: MarketData[], period: number): number {
if (data.length < period) return 0;
let tr = [];
for (let i = 1; i < data.length; i++) {
const high = data[i].high;
const low = data[i].low;
const prevClose = data[i-1].close;
tr.push(Math.max(
high - low,
Math.abs(high - prevClose),
Math.abs(low - prevClose)
));
}
// Calculate simple moving average of TR
const atr = tr.slice(-period).reduce((sum, val) => sum + val, 0) / period;
return atr;
}
/**
* Calculate price momentum
*/
private calculateMomentum(data: MarketData[]): { direction: 'up' | 'down'; strength: number } {
const closes = data.map(d => d.close);
let upMoves = 0;
let downMoves = 0;
// Count consecutive moves
for (let i = 1; i < closes.length; i++) {
if (closes[i] > closes[i - 1]) upMoves++;
if (closes[i] < closes[i - 1]) downMoves++;
}
const totalMoves = upMoves + downMoves;
const upStrength = upMoves / totalMoves;
const downStrength = downMoves / totalMoves;
return {
direction: upStrength > downStrength ? 'up' : 'down',
strength: Math.max(upStrength, downStrength)
};
}
/**
* Determine overall trend combining structure and momentum
*/
private determineOverallTrend(
structure: { direction: 'up' | 'down' | 'none'; strength: number },
momentum: { direction: 'up' | 'down'; strength: number },
priceChange: number
): 'up' | 'down' | 'none' {
// Strong momentum overrides structure
if (momentum.strength > OrderBlockAnalysisService.TREND_PARAMS.momentumThreshold) {
return momentum.direction;
}
// Strong structure with confirming price change
if (structure.direction !== 'none' &&
Math.sign(priceChange) === (structure.direction === 'up' ? 1 : -1)) {
return structure.direction;
}
// Default to momentum direction if price change confirms it
if (Math.sign(priceChange) === (momentum.direction === 'up' ? 1 : -1)) {
return momentum.direction;
}
return 'none';
}
/**
* Calculate overall trend strength
*/
private calculateOverallStrength(structureStrength: number, momentumStrength: number): number {
return (structureStrength * 0.6 + momentumStrength * 0.4);
}
/**
* Find significant swing points using ATR for context
*/
private findSignificantSwings(data: MarketData[], atr: number): SwingPoint[] {
const swingPoints: SwingPoint[] = [];
const minMove = atr * 0.3; // Reduced from 0.5 to catch more potential swings
for (let i = 2; i < data.length - 2; i++) {
const current = data[i];
const before = data.slice(i - 2, i);
const after = data.slice(i + 1, i + 3);
// Check for swing high with relaxed conditions
if (before.every(c => c.high <= current.high) &&
after[0].high < current.high) {
swingPoints.push({
type: 'high',
price: current.high,
time: current.time,
index: i
});
}
// Check for swing low with relaxed conditions
if (before.every(c => c.low >= current.low) &&
after[0].low > current.low) {
swingPoints.push({
type: 'low',
price: current.low,
time: current.time,
index: i
});
}
}
return swingPoints;
}
/**
* Analyze trend structure using swing points
*/
private analyzeTrendStructure(
swingPoints: SwingPoint[],
atr: number
): { direction: 'up' | 'down' | 'none'; strength: number; } {
const highs = swingPoints.filter(p => p.type === 'high')
.sort((a, b) => a.time - b.time);
const lows = swingPoints.filter(p => p.type === 'low')
.sort((a, b) => a.time - b.time);
if (highs.length < 2 || lows.length < 2) {
return { direction: 'none', strength: 0 };
}
// Calculate trend metrics
const hhSequence = this.calculateSequenceStrength(highs, 'up', atr);
const lhSequence = this.calculateSequenceStrength(highs, 'down', atr);
const hlSequence = this.calculateSequenceStrength(lows, 'up', atr);
const llSequence = this.calculateSequenceStrength(lows, 'down', atr);
// Determine trend
const upStrength = (hhSequence + hlSequence) / 2;
const downStrength = (lhSequence + llSequence) / 2;
if (upStrength > OrderBlockAnalysisService.TREND_PARAMS.trendThreshold &&
upStrength > downStrength) {
return {
direction: 'up',
strength: upStrength
};
}
if (downStrength > OrderBlockAnalysisService.TREND_PARAMS.trendThreshold &&
downStrength > upStrength) {
return {
direction: 'down',
strength: downStrength
};
}
return { direction: 'none', strength: 0 };
}
private analyzeTrendLegs(data: MarketData[]): TrendLeg[] {
const trendLegs: TrendLeg[] = [];
const atr = this.calculateATR(data, 14);
let currentLegStart = 0;
// Minimum number of candles to consider a valid trend leg
const MIN_LEG_LENGTH = 5;
// Loop through data to identify trend changes
for (let i = MIN_LEG_LENGTH; i < data.length - MIN_LEG_LENGTH; i++) {
const currentSegment = data.slice(currentLegStart, i + 1);
const nextSegment = data.slice(i - MIN_LEG_LENGTH, i + MIN_LEG_LENGTH);
// Check for trend change
if (this.isTrendChange(currentSegment, nextSegment, atr)) {
// Add completed trend leg
if (i - currentLegStart >= MIN_LEG_LENGTH) {
const legData = data.slice(currentLegStart, i);
const trendLeg = this.analyzeSingleTrendLeg(legData, currentLegStart, i);
trendLegs.push(trendLeg);
}
currentLegStart = i;
}
}
// Add final trend leg
if (data.length - currentLegStart >= MIN_LEG_LENGTH) {
const finalLegData = data.slice(currentLegStart);
const finalTrendLeg = this.analyzeSingleTrendLeg(
finalLegData,
currentLegStart,
data.length - 1
);
trendLegs.push(finalTrendLeg);
}
return trendLegs;
}
private isTrendChange(
currentSegment: MarketData[],
nextSegment: MarketData[],
atr: number
): boolean {
const currentDirection = this.determineTrendDirection(currentSegment);
const nextDirection = this.determineTrendDirection(nextSegment);
// Check for direction change
if (currentDirection !== nextDirection) {
// Verify change is significant (> 1 ATR)
const priceChange = Math.abs(
nextSegment[nextSegment.length - 1].close - nextSegment[0].close
);
return priceChange > atr;
}
return false;
}
private analyzeSingleTrendLeg(
data: MarketData[],
startIndex: number,
endIndex: number
): TrendLeg {
const swingPoints = this.findSignificantSwings(data, this.calculateATR(data, 14));
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
const momentum = this.calculateMomentum(data);
// Determine trend direction
let direction: 'up' | 'down' | 'sideways';
if (Math.abs(priceChange) < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
direction = 'sideways';
} else {
direction = priceChange > 0 ? 'up' : 'down';
}
// Calculate trend strength based on:
// 1. Price change magnitude
// 2. Momentum consistency
// 3. Swing point alignment
const priceStrength = Math.min(Math.abs(priceChange) * 10, 1); // Scale price change
const momentumStrength = momentum.strength;
const swingStrength = this.calculateSwingAlignment(swingPoints, direction);
const strength = (
priceStrength * 0.4 +
momentumStrength * 0.4 +
swingStrength * 0.2
) * 100; // Convert to percentage
return {
startIndex,
endIndex,
startTime: data[0].time,
endTime: data[data.length - 1].time,
direction,
strength,
startPrice: data[0].close,
endPrice: data[data.length - 1].close,
swingPoints
};
}
private calculateSwingAlignment(
swingPoints: SwingPoint[],
direction: 'up' | 'down' | 'sideways'
): number {
if (direction === 'sideways' || swingPoints.length < 2) return 0;
let aligned = 0;
let total = 0;
for (let i = 1; i < swingPoints.length; i++) {
if (direction === 'up') {
if (swingPoints[i].price > swingPoints[i - 1].price) aligned++;
} else {
if (swingPoints[i].price < swingPoints[i - 1].price) aligned++;
}
total++;
}
return total > 0 ? aligned / total : 0;
}
private determineTrendDirection(data: MarketData[]): 'up' | 'down' | 'sideways' {
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
if (Math.abs(priceChange) < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
return 'sideways';
}
return priceChange > 0 ? 'up' : 'down';
}
/**
* Calculate sequence strength relative to ATR
*/
private calculateSequenceStrength(
points: SwingPoint[],
direction: 'up' | 'down',
atr: number
): number {
if (points.length < 2) return 0;
let validMoves = 0;
let totalMoves = 0;
for (let i = 1; i < points.length; i++) {
const move = direction === 'up'
? points[i].price - points[i-1].price
: points[i-1].price - points[i].price;
if (move > 0 && Math.abs(move) > atr * 0.5) {
validMoves++;
}
totalMoves++;
}
return totalMoves > 0 ? validMoves / totalMoves : 0;
}
/**
* Calculate trend strength (0-1)
*/
private calculateTrendStrength(
points: SwingPoint[],
direction: 'up' | 'down'
): number {
if (points.length < 2) return 0;
let increasing = 0;
let total = 0;
for (let i = 1; i < points.length; i++) {
const current = points[i].price;
const previous = points[i - 1].price;
if (direction === 'up' && current > previous) increasing++;
if (direction === 'down' && current < previous) increasing++;
total++;
}
return total > 0 ? increasing / total : 0;
}
/**
* Find order blocks
*/
private findOrderBlocks(
data: MarketData[],
trend: { direction: 'up' | 'down' | 'none'; strength: number; }
): OrderBlock[] {
const orderBlocks: OrderBlock[] = [];
// Only proceed if we have a clear trend
if (trend.direction === 'none' || trend.strength < OrderBlockAnalysisService.ANALYSIS_PARAMS.trendStrengthThreshold) {
return orderBlocks;
}
// Use a sliding window of 7 candles (3 before, current, 3 after)
for (let i = 3; i < data.length - 3; i++) {
const window = {
before: data.slice(i - 3, i),
current: data[i],
after: data.slice(i + 1, i + 4)
};
// Check for potential order block
const orderBlock = this.validateOrderBlock(window, trend.direction);
if (orderBlock) {
orderBlocks.push(orderBlock);
}
}
return this.filterOverlappingBlocks(orderBlocks);
}
/**
* Validate potential order block
*/
private validateOrderBlock(
window: {
before: MarketData[];
current: MarketData;
after: MarketData[];
},
trendDirection: 'up' | 'down'
): OrderBlock | null {
const { before, current, after } = window;
// Check for bullish order block in uptrend
if (trendDirection === 'up') {
const isBearishCandle = current.close < current.open;
const hasImpulsiveMove = this.hasImpulsiveMove(after, 'up');
const pvg = this.calculatePriceValueGap([...before, current, ...after]);
if (isBearishCandle && hasImpulsiveMove && pvg > OrderBlockAnalysisService.ANALYSIS_PARAMS.minGapSize) {
return {
id: `OB_${current.time}`,
type: 'bullish',
startTime: current.time,
endTime: after[0].time,
highPrice: current.high,
lowPrice: current.low,
volume: current.volume,
impulseMagnitude: this.calculateImpulseMagnitude([...before, current, ...after]),
priceValueGap: pvg,
validationMetrics: {
trendAlignment: true,
hasBreakOfStructure: this.hasBreakOfStructure([...before, current, ...after], 'up'),
gapQuality: this.calculateGapQuality(pvg, current),
impulseStrength: this.calculateImpulseStrength(after, before)
}
};
}
}
// Check for bearish order block in downtrend
if (trendDirection === 'down') {
const isBullishCandle = current.close > current.open;
const hasImpulsiveMove = this.hasImpulsiveMove(after, 'down');
const pvg = this.calculatePriceValueGap([...before, current, ...after]);
if (isBullishCandle && hasImpulsiveMove && pvg > OrderBlockAnalysisService.ANALYSIS_PARAMS.minGapSize) {
return {
id: `OB_${current.time}`,
type: 'bearish',
startTime: current.time,
endTime: after[0].time,
highPrice: current.high,
lowPrice: current.low,
volume: current.volume,
impulseMagnitude: this.calculateImpulseMagnitude([...before, current, ...after]),
priceValueGap: pvg,
validationMetrics: {
trendAlignment: true,
hasBreakOfStructure: this.hasBreakOfStructure([...before, current, ...after], 'down'),
gapQuality: this.calculateGapQuality(pvg, current),
impulseStrength: this.calculateImpulseStrength(after, before)
}
};
}
}
return null;
}
/**
* Calculate price value gap
*/
private calculatePriceValueGap(candles: MarketData[]): number {
let maxGap = 0;
for (let i = 1; i < candles.length - 1; i++) {
const current = candles[i];
const next = candles[i + 1];
// Check for gap between candles
if (next.low > current.high) {
maxGap = Math.max(maxGap, next.low - current.high);
}
if (next.high < current.low) {
maxGap = Math.max(maxGap, current.low - next.high);
}
}
return maxGap;
}
/**
* Check for impulsive move
*/
private hasImpulsiveMove(candles: MarketData[], direction: 'up' | 'down'): boolean {
const moves = candles.map(c => Math.abs(c.close - c.open));
const avgMove = moves.reduce((sum, move) => sum + move, 0) / moves.length;
if (direction === 'up') {
return candles[0].close > candles[0].open &&
avgMove > OrderBlockAnalysisService.ANALYSIS_PARAMS.minImpulseStrength;
} else {
return candles[0].close < candles[0].open &&
avgMove > OrderBlockAnalysisService.ANALYSIS_PARAMS.minImpulseStrength;
}
}
/**
* Calculate impulse magnitude
*/
private calculateImpulseMagnitude(candles: MarketData[]): number {
const moves = candles.map(c => ({
move: Math.abs(c.close - c.open),
volume: c.volume
}));
// Volume-weighted average move
const weightedMoves = moves.map(m => m.move * (m.volume / Math.max(...moves.map(x => x.volume))));
return weightedMoves.reduce((sum, move) => sum + move, 0) / weightedMoves.length;
}
/**
* Calculate gap quality (0-1)
*/
private calculateGapQuality(gap: number, candle: MarketData): number {
const candleSize = Math.abs(candle.high - candle.low);
return Math.min(gap / candleSize, 1);
}
/**
* Calculate impulse strength
*/
private calculateImpulseStrength(after: MarketData[], before: MarketData[]): number {
const afterMoves = after.map(c => Math.abs(c.close - c.open));
const beforeMoves = before.map(c => Math.abs(c.close - c.open));
const avgAfter = afterMoves.reduce((sum, move) => sum + move, 0) / afterMoves.length;
const avgBefore = beforeMoves.reduce((sum, move) => sum + move, 0) / beforeMoves.length;
return avgAfter / avgBefore;
}
/**
* Check for break of structure
*/
private hasBreakOfStructure(candles: MarketData[], direction: 'up' | 'down'): boolean {
const middle = Math.floor(candles.length / 2);
const before = candles.slice(0, middle);
const after = candles.slice(middle);
if (direction === 'up') {
const beforeHigh = Math.max(...before.map(c => c.high));
const afterHigh = Math.max(...after.map(c => c.high));
return afterHigh > beforeHigh;
} else {
const beforeLow = Math.min(...before.map(c => c.low));
const afterLow = Math.min(...after.map(c => c.low));
return afterLow < beforeLow;
}
}
/**
* Filter overlapping order blocks
*/
private filterOverlappingBlocks(blocks: OrderBlock[]): OrderBlock[] {
return blocks.filter((block, index) => {
// Check if this block overlaps with any stronger blocks
const hasStrongerOverlap = blocks.some((otherBlock, otherIndex) => {
if (index === otherIndex) return false;
const timeOverlap = block.startTime <= otherBlock.endTime &&
block.endTime >= otherBlock.startTime;
const priceOverlap = block.lowPrice <= otherBlock.highPrice &&
block.highPrice >= otherBlock.lowPrice;
return timeOverlap && priceOverlap &&
otherBlock.validationMetrics.impulseStrength >
block.validationMetrics.impulseStrength;
});
return !hasStrongerOverlap;
});
}
private getTodayMarketOpen(): number {
const now = new Date();
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
OrderBlockAnalysisService.MARKET_HOURS.start.hour,
OrderBlockAnalysisService.MARKET_HOURS.start.minute
).getTime();
}
private getTodayMarketClose(): number {
const now = new Date();
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
OrderBlockAnalysisService.MARKET_HOURS.end.hour,
OrderBlockAnalysisService.MARKET_HOURS.end.minute
).getTime();
}
}
export type { AnalysisResult, OrderBlock, SwingPoint };