mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 18:41:01 +08:00
- Chart improvements: professional styling, popular symbols quick selection, simplified B/S legend - Data source migration: use CoinAnk API exclusively for all kline data - Code cleanup: remove Binance WebSocket cache and related code (websocket_client.go, combined_streams.go, monitor.go) - Log optimization: reduce hook spam, suppress 404 errors, increase P&L diff threshold - Lighter integration: add order sync functionality, fix market order precision - Remove ticker merge logic for simplicity
217 lines
5.0 KiB
TypeScript
217 lines
5.0 KiB
TypeScript
// 技术指标计算工具
|
||
|
||
export interface Kline {
|
||
time: number
|
||
open: number
|
||
high: number
|
||
low: number
|
||
close: number
|
||
volume?: number
|
||
}
|
||
|
||
// 简单移动平均线 (SMA)
|
||
export function calculateSMA(data: Kline[], period: number): Array<{ time: number; value: number }> {
|
||
const result: Array<{ time: number; value: number }> = []
|
||
|
||
for (let i = period - 1; i < data.length; i++) {
|
||
let sum = 0
|
||
for (let j = 0; j < period; j++) {
|
||
sum += data[i - j].close
|
||
}
|
||
result.push({
|
||
time: data[i].time,
|
||
value: sum / period,
|
||
})
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 指数移动平均线 (EMA)
|
||
export function calculateEMA(data: Kline[], period: number): Array<{ time: number; value: number }> {
|
||
const result: Array<{ time: number; value: number }> = []
|
||
const multiplier = 2 / (period + 1)
|
||
|
||
// 第一个EMA值使用SMA
|
||
let ema = 0
|
||
for (let i = 0; i < period; i++) {
|
||
ema += data[i].close
|
||
}
|
||
ema = ema / period
|
||
result.push({ time: data[period - 1].time, value: ema })
|
||
|
||
// 后续EMA值
|
||
for (let i = period; i < data.length; i++) {
|
||
ema = (data[i].close - ema) * multiplier + ema
|
||
result.push({ time: data[i].time, value: ema })
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// MACD 指标
|
||
export interface MACDData {
|
||
time: number
|
||
macd: number
|
||
signal: number
|
||
histogram: number
|
||
}
|
||
|
||
export function calculateMACD(
|
||
data: Kline[],
|
||
fastPeriod = 12,
|
||
slowPeriod = 26,
|
||
signalPeriod = 9
|
||
): MACDData[] {
|
||
const fastEMA = calculateEMA(data, fastPeriod)
|
||
const slowEMA = calculateEMA(data, slowPeriod)
|
||
|
||
// 计算MACD线
|
||
const macdLine: Array<{ time: number; value: number }> = []
|
||
for (let i = 0; i < slowEMA.length; i++) {
|
||
const fastValue = fastEMA.find(e => e.time === slowEMA[i].time)
|
||
if (fastValue) {
|
||
macdLine.push({
|
||
time: slowEMA[i].time,
|
||
value: fastValue.value - slowEMA[i].value,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 计算信号线(MACD的EMA)
|
||
const signalLine = calculateEMAFromValues(macdLine, signalPeriod)
|
||
|
||
// 生成MACD数据
|
||
const result: MACDData[] = []
|
||
for (let i = 0; i < signalLine.length; i++) {
|
||
const macdValue = macdLine.find(m => m.time === signalLine[i].time)
|
||
if (macdValue) {
|
||
result.push({
|
||
time: signalLine[i].time,
|
||
macd: macdValue.value,
|
||
signal: signalLine[i].value,
|
||
histogram: macdValue.value - signalLine[i].value,
|
||
})
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 从值数组计算EMA(辅助函数)
|
||
function calculateEMAFromValues(
|
||
data: Array<{ time: number; value: number }>,
|
||
period: number
|
||
): Array<{ time: number; value: number }> {
|
||
const result: Array<{ time: number; value: number }> = []
|
||
const multiplier = 2 / (period + 1)
|
||
|
||
if (data.length < period) return []
|
||
|
||
// 第一个EMA值使用SMA
|
||
let ema = 0
|
||
for (let i = 0; i < period; i++) {
|
||
ema += data[i].value
|
||
}
|
||
ema = ema / period
|
||
result.push({ time: data[period - 1].time, value: ema })
|
||
|
||
// 后续EMA值
|
||
for (let i = period; i < data.length; i++) {
|
||
ema = (data[i].value - ema) * multiplier + ema
|
||
result.push({ time: data[i].time, value: ema })
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// RSI 指标
|
||
export function calculateRSI(data: Kline[], period = 14): Array<{ time: number; value: number }> {
|
||
const result: Array<{ time: number; value: number }> = []
|
||
|
||
if (data.length < period + 1) return []
|
||
|
||
// 计算价格变化
|
||
const changes: number[] = []
|
||
for (let i = 1; i < data.length; i++) {
|
||
changes.push(data[i].close - data[i - 1].close)
|
||
}
|
||
|
||
// 计算初始平均涨跌幅
|
||
let avgGain = 0
|
||
let avgLoss = 0
|
||
for (let i = 0; i < period; i++) {
|
||
if (changes[i] > 0) {
|
||
avgGain += changes[i]
|
||
} else {
|
||
avgLoss += Math.abs(changes[i])
|
||
}
|
||
}
|
||
avgGain = avgGain / period
|
||
avgLoss = avgLoss / period
|
||
|
||
// 计算RSI
|
||
for (let i = period; i < changes.length; i++) {
|
||
const currentChange = changes[i]
|
||
|
||
if (currentChange > 0) {
|
||
avgGain = (avgGain * (period - 1) + currentChange) / period
|
||
avgLoss = (avgLoss * (period - 1)) / period
|
||
} else {
|
||
avgGain = (avgGain * (period - 1)) / period
|
||
avgLoss = (avgLoss * (period - 1) + Math.abs(currentChange)) / period
|
||
}
|
||
|
||
const rs = avgGain / avgLoss
|
||
const rsi = 100 - 100 / (1 + rs)
|
||
|
||
result.push({
|
||
time: data[i + 1].time,
|
||
value: rsi,
|
||
})
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 布林带
|
||
export interface BollingerBands {
|
||
time: number
|
||
upper: number
|
||
middle: number
|
||
lower: number
|
||
}
|
||
|
||
export function calculateBollingerBands(
|
||
data: Kline[],
|
||
period = 20,
|
||
stdDev = 2
|
||
): BollingerBands[] {
|
||
const result: BollingerBands[] = []
|
||
|
||
for (let i = period - 1; i < data.length; i++) {
|
||
// 计算SMA
|
||
let sum = 0
|
||
for (let j = 0; j < period; j++) {
|
||
sum += data[i - j].close
|
||
}
|
||
const sma = sum / period
|
||
|
||
// 计算标准差
|
||
let variance = 0
|
||
for (let j = 0; j < period; j++) {
|
||
variance += Math.pow(data[i - j].close - sma, 2)
|
||
}
|
||
const std = Math.sqrt(variance / period)
|
||
|
||
result.push({
|
||
time: data[i].time,
|
||
upper: sma + stdDev * std,
|
||
middle: sma,
|
||
lower: sma - stdDev * std,
|
||
})
|
||
}
|
||
|
||
return result
|
||
}
|