@gabriel3615/ta_analysis
Version:
stock ta analysis
210 lines (194 loc) • 5.48 kB
text/typescript
import type { Candle } from '../../../types.js';
import { structureConfig } from './structureConfig.js';
import type {
StructureEvent,
StructureResult,
StructureTrend,
Swing,
} from './structureTypes.js';
function findPivots(data: Candle[]): {
highs: (number | null)[];
lows: (number | null)[];
} {
const { leftBars, rightBars } = structureConfig.pivot;
const highs: (number | null)[] = Array(data.length).fill(null);
const lows: (number | null)[] = Array(data.length).fill(null);
for (let i = leftBars; i < data.length - rightBars; i++) {
let isHigh = true;
for (let j = i - leftBars; j < i + rightBars + 1; j++) {
if (data[j].high > data[i].high) {
isHigh = false;
break;
}
}
if (isHigh) highs[i] = data[i].high;
let isLow = true;
for (let j = i - leftBars; j < i + rightBars + 1; j++) {
if (data[j].low < data[i].low) {
isLow = false;
break;
}
}
if (isLow) lows[i] = data[i].low;
}
return { highs, lows };
}
function buildExternalSwings(
data: Candle[],
pivots: { highs: (number | null)[]; lows: (number | null)[] }
): Swing[] {
const swings: Swing[] = [];
const minMove = structureConfig.thresholds.minSwingDistancePercent;
let lastIndex = -1;
let lastHigh = -Infinity;
let lastLow = Infinity;
for (let i = 0; i < data.length; i++) {
const h = pivots.highs[i];
const l = pivots.lows[i];
if (h != null) {
if (lastIndex >= 0) {
const move = Math.abs((h - lastLow) / lastLow);
if (move >= minMove)
swings.push({
startIndex: lastIndex,
endIndex: i,
high: h,
low: lastLow,
});
}
lastIndex = i;
lastHigh = h;
lastLow = data[i].low;
}
if (l != null) {
if (lastIndex >= 0) {
const move = Math.abs((lastHigh - l) / lastHigh);
if (move >= minMove)
swings.push({
startIndex: lastIndex,
endIndex: i,
high: lastHigh,
low: l,
});
}
lastIndex = i;
lastLow = l;
lastHigh = data[i].high;
}
}
return swings;
}
function detectStructureEvents(
data: Candle[],
swings: Swing[],
timeframe: 'weekly' | 'daily' | '1hour'
): StructureEvent | undefined {
if (swings.length < 2) return;
const { breakThresholdPercent, equalTolerancePercent } =
structureConfig.thresholds;
const lastSwing = swings[swings.length - 1];
const prevSwing = swings[swings.length - 2];
const close = data[data.length - 1].close;
// BOS upward
const brokeHigh = close > lastSwing.high * (1 + breakThresholdPercent);
const brokeLow = close < lastSwing.low * (1 - breakThresholdPercent);
if (brokeHigh) {
return {
type: 'BOS',
direction: 'bullish',
index: data.length - 1,
price: close,
timeframe,
};
}
if (brokeLow) {
return {
type: 'BOS',
direction: 'bearish',
index: data.length - 1,
price: close,
timeframe,
};
}
// Equal highs/lows
const eqHigh =
Math.abs(lastSwing.high - prevSwing.high) / prevSwing.high <
equalTolerancePercent;
const eqLow =
Math.abs(lastSwing.low - prevSwing.low) / prevSwing.low <
equalTolerancePercent;
if (eqHigh)
return {
type: 'EqualHighs',
direction: 'neutral',
index: lastSwing.endIndex,
price: lastSwing.high,
timeframe,
};
if (eqLow)
return {
type: 'EqualLows',
direction: 'neutral',
index: lastSwing.endIndex,
price: lastSwing.low,
timeframe,
};
// CHOCH: 简化 - 最近一段与前段方向相反并逼近另一侧边界
const wasUp =
prevSwing.high >= lastSwing.high && prevSwing.low < lastSwing.low;
const wasDown =
prevSwing.low <= lastSwing.low && prevSwing.high > lastSwing.high;
if (wasUp && brokeLow)
return {
type: 'CHOCH',
direction: 'bearish',
index: data.length - 1,
price: close,
timeframe,
};
if (wasDown && brokeHigh)
return {
type: 'CHOCH',
direction: 'bullish',
index: data.length - 1,
price: close,
timeframe,
};
}
function determineTrend(swings: Swing[]): StructureTrend {
if (swings.length < 3) return 'sideways';
const last = swings.slice(-3);
const highs = last.map(s => s.high);
const lows = last.map(s => s.low);
const up =
highs[2] > highs[1] &&
highs[1] > highs[0] &&
lows[2] > lows[1] &&
lows[1] > lows[0];
const down =
highs[2] < highs[1] &&
highs[1] < highs[0] &&
lows[2] < lows[1] &&
lows[1] < lows[0];
return up ? 'up' : down ? 'down' : 'sideways';
}
export function analyzeMarketStructure(
data: Candle[],
timeframe: 'weekly' | 'daily' | '1hour'
): StructureResult {
const pivots = findPivots(data);
const swings = buildExternalSwings(data, pivots);
const trend = determineTrend(swings);
const evt = detectStructureEvents(data, swings, timeframe);
const last = swings.at(-1);
const prev = swings.at(-2);
const keyLevels: number[] = [];
if (last) {
keyLevels.push(last.high, last.low);
if (prev) keyLevels.push(prev.high, prev.low);
}
const summary = evt
? `${timeframe} 结构: ${trend},检测到 ${evt.type}${evt.direction !== 'neutral' ? ' ' + evt.direction : ''}`
: `${timeframe} 结构: ${trend}`;
return { timeframe, trend, lastEvent: evt, keyLevels, summary };
}