Files
Toolbox/src/server/market_skills/shenhua_299/index.js
T

202 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const calculateMA = (data, period) => {
if (!data || data.length < period) return null
const subset = data.slice(-period)
const sum = subset.reduce((a, b) => a + b, 0)
return sum / period
}
const fetchKLine = async (axios, symbol) => {
const url = `http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${symbol},day,,,300,qfq`
try {
const res = await axios.get(url, { timeout: 10000 })
if (!res || !res.data || res.data.code !== 0) return null
const d = res.data.data[symbol]
if (!d) return null
const rawData = d.qfqday || d.day
if (!rawData || !Array.isArray(rawData)) return null
return rawData.map(item => ({
date: item[0],
close: parseFloat(item[2])
}))
} catch (e) {
return null
}
}
const buildAlerts = ({
latestClose,
prevClose,
ma10,
ma20,
ma60,
ma90,
ma250,
prevMA20,
prevMA60,
prevMA90,
prevMA250
}) => {
const alerts = []
const addAlert = (id, title, message, level = 'warning') => {
alerts.push({ id, title, message, level })
}
const isCrossDown = (prevC, prevMa, currC, currMa) => prevC >= prevMa && currC < currMa
const isCrossUp = (prevC, prevMa, currC, currMa) => prevC <= prevMa && currC > currMa
const isBelowMA250 = latestClose < ma250
const isCrossDownMA250 = isCrossDown(prevClose, prevMA250, latestClose, ma250)
const isCrossUpMA250 = isCrossUp(prevClose, prevMA250, latestClose, ma250)
if (isCrossDownMA250) {
addAlert('below_ma250', '中国神华跌破年线!立即停止加仓,建议减仓', '中国神华跌破年线!立即停止加仓,建议减仓', 'error')
}
if (isCrossUpMA250) {
addAlert('above_ma250', '中国神华回到年线之上,可恢复加仓策略', '中国神华回到年线之上,可恢复加仓策略')
}
if (!isBelowMA250) {
if (isCrossDown(prevClose, prevMA20, latestClose, ma20)) {
addAlert('below_ma20', '中国神华-跌破20日线,需加仓 20%', '中国神华-跌破20日线,需加仓 20%')
}
if (isCrossDown(prevClose, prevMA60, latestClose, ma60)) {
addAlert('below_ma60', '中国神华-跌破60日线,需加仓 40%', '中国神华-跌破60日线,需加仓 40%')
}
if (isCrossDown(prevClose, prevMA90, latestClose, ma90)) {
addAlert('below_ma90', '中国神华-跌破90日线,需加仓剩余 40%', '中国神华-跌破90日线,需加仓剩余 40%')
}
}
if (isCrossUp(prevClose, prevMA20, latestClose, ma20)) {
addAlert('above_ma20', '中国神华-站上20日线,需卖出 20%', '中国神华-站上20日线,需卖出 20%')
}
if (isCrossUp(prevClose, prevMA60, latestClose, ma60)) {
addAlert('above_ma60', '中国神华-站上60日线,需卖出 40%', '中国神华-站上60日线,需卖出 40%')
}
if (isCrossUp(prevClose, prevMA90, latestClose, ma90)) {
addAlert('above_ma90', '中国神华-站上90日线,需卖出剩余 40%', '中国神华-站上90日线,需卖出剩余 40%')
}
if (ma10 > ma20) {
addAlert('trend_bull', '趋势提示', '中国神华处于上涨趋势,建议持有底仓 + 已建波段仓,不做 T')
}
return alerts
}
module.exports = {
run: async (context, config) => {
const { axios, logJSON } = context
const symbol = config.symbol
const sourceSymbol = config.source_symbol || 'sh601088'
const klineData = await fetchKLine(axios, sourceSymbol)
if (!klineData || klineData.length < 250) {
logJSON('skill.shenhua_299.error', { message: 'Insufficient data', length: klineData ? klineData.length : 0 }, 'market_skills')
return null
}
const closes = klineData.map(d => d.close)
const latestClose = closes[closes.length - 1]
const prevClose = closes[closes.length - 2]
const latestDate = klineData[klineData.length - 1].date
const ma10 = calculateMA(closes, 10)
const ma20 = calculateMA(closes, 20)
const ma60 = calculateMA(closes, 60)
const ma90 = calculateMA(closes, 90)
const ma250 = calculateMA(closes, 250)
const closesPrev = closes.slice(0, -1)
const prevMA20 = calculateMA(closesPrev, 20)
const prevMA60 = calculateMA(closesPrev, 60)
const prevMA90 = calculateMA(closesPrev, 90)
const prevMA250 = calculateMA(closesPrev, 250)
if (!ma10 || !ma20 || !ma60 || !ma90 || !ma250 || !prevMA20 || !prevMA60 || !prevMA90 || !prevMA250) {
logJSON('skill.shenhua_299.error', { message: 'MA calculation failed' }, 'market_skills')
return null
}
const change = latestClose - prevClose
const changePercent = (change / prevClose) * 100
const trendText = ma10 > ma20 ? '多头排列' : '非多头'
const yearLineText = latestClose < ma250 ? '年线下方' : '年线之上'
const isCrossDown = (prevC, prevMa, currC, currMa) => prevC >= prevMa && currC < currMa
const isCrossUp = (prevC, prevMa, currC, currMa) => prevC <= prevMa && currC > currMa
const isBelowMA250 = latestClose < ma250
const crossDownMA20 = isCrossDown(prevClose, prevMA20, latestClose, ma20)
const crossDownMA60 = isCrossDown(prevClose, prevMA60, latestClose, ma60)
const crossDownMA90 = isCrossDown(prevClose, prevMA90, latestClose, ma90)
const crossUpMA20 = isCrossUp(prevClose, prevMA20, latestClose, ma20)
const crossUpMA60 = isCrossUp(prevClose, prevMA60, latestClose, ma60)
const crossUpMA90 = isCrossUp(prevClose, prevMA90, latestClose, ma90)
const crossUpMA250 = isCrossUp(prevClose, prevMA250, latestClose, ma250)
const statusDownMA20 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA20 ? '已触发(加仓20%)' : '未触发')
const statusDownMA60 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA60 ? '已触发(加仓40%)' : '未触发')
const statusDownMA90 = isBelowMA250 ? '暂停(年线下方)' : (crossDownMA90 ? '已触发(加仓剩余40%)' : '未触发')
const statusUpMA20 = crossUpMA20 ? '已触发(卖出20%)' : '未触发'
const statusUpMA60 = crossUpMA60 ? '已触发(卖出40%)' : '未触发'
const statusUpMA90 = crossUpMA90 ? '已触发(卖出剩余40%)' : '未触发'
const statusTrend = ma10 > ma20 ? '已满足(上涨趋势)' : '未满足'
const statusMA250 = isBelowMA250 ? '已触发(停止加仓/建议减仓)' : (crossUpMA250 ? '已触发(恢复加仓)' : '未触发')
const htmlPayload = `
<div style="font-size: 14px; margin-top: 8px;">
价格:${latestClose.toFixed(2)} (${change >= 0 ? '+' : ''}${change.toFixed(2)} / ${changePercent.toFixed(2)}%)
</div>
<div style="margin: 8px 0; border-top: 1px solid #eee; width: 100%;"></div>
<div style="font-size: 13px; color: #666; line-height: 1.6;">
MA10:${ma10.toFixed(2)} | MA20:${ma20.toFixed(2)} | MA60:${ma60.toFixed(2)}<br/>
MA90:${ma90.toFixed(2)} | MA250:${ma250.toFixed(2)}<br/>
趋势:${trendText} | 年线状态:${yearLineText}
</div>
<div style="margin: 8px 0; border-top: 1px solid #eee; width: 100%;"></div>
<div style="font-size: 13px; color: #666; line-height: 1.6;">
20日线跌破:${statusDownMA20} | 站上:${statusUpMA20}<br/>
60日线跌破:${statusDownMA60} | 站上:${statusUpMA60}<br/>
90日线跌破:${statusDownMA90} | 站上:${statusUpMA90}<br/>
MA10>MA20:${statusTrend} | MA250:${statusMA250}
</div>
`
const data = [{
label: config.name,
symbol: symbol,
displaySymbol: sourceSymbol,
isMaCard: true,
render_type: 'html',
html: htmlPayload,
ma10: ma10.toFixed(2),
ma20: ma20.toFixed(2),
ma60: ma60.toFixed(2),
ma90: ma90.toFixed(2),
ma250: ma250.toFixed(2),
source: config.source || 'tencent',
name: config.name,
price: latestClose,
prevClose: prevClose,
change: change,
change_percent: changePercent,
market_status: `${trendText} (${latestDate})`
}]
const alerts = buildAlerts({
latestClose,
prevClose,
ma10,
ma20,
ma60,
ma90,
ma250,
prevMA20,
prevMA60,
prevMA90,
prevMA250
})
return { data, alerts }
}
}