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

157 lines
5.1 KiB
JavaScript

const iconv = require('iconv-lite')
const { fetchYahoo } = require('../../marketsClient')
// State to prevent alert spam
// Map<alertKey, lastAlertTimestamp>
const lastAlertTimes = new Map()
const parseTencentLine = (line) => {
const m = line.match(/^v_(\w+)="([^"]*)/)
if (!m) return null
const symbol = m[1]
const body = m[2]
let parts = body.split('~')
if (parts.length < 3 && body.includes(',')) {
parts = body.split(',')
}
// Standard Tencent Format (verified for sh/sz/bj/us/hk):
// 1: Name
// 3: Current Price
// 4: Prev Close
let name = parts[1]
let price = parseFloat(parts[3])
let prevClose = parseFloat(parts[4])
if (isNaN(price) || isNaN(prevClose)) return null
const change = price - prevClose
const pct = prevClose ? (change / prevClose) * 100 : 0
let captured_at = parts[30] || ''
if (captured_at.length === 14 && !captured_at.includes('-')) {
captured_at = captured_at.replace(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/, '$1-$2-$3 $4:$5:$6')
}
return { symbol, name, price, prevClose, change, change_percent: pct, captured_at }
}
const fetchTencent = async (axios, symbols) => {
if (!symbols || symbols.length === 0) return []
const url = `http://qt.gtimg.cn/q=${symbols.join(',')}`
try {
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000, validateStatus: () => true })
if (!res || res.status >= 400) return []
const text = iconv.decode(Buffer.from(res.data), 'gb18030')
const lines = String(text || '').split(/;\s*/).filter(Boolean)
const out = []
for (const line of lines) {
const r = parseTencentLine(line)
if (r) out.push({ ...r, source: 'tencent' })
}
return out
} catch (e) {
return []
}
}
const checkAlerts = (data, config) => {
const alerts = []
// Helper to add alert with slot-based cooldown (AM/PM logic)
const addAlert = (key, title, message, level = 'warning') => {
// Get current Beijing time
const d = new Date()
const utc = d.getTime() + (d.getTimezoneOffset() * 60000)
const bjDate = new Date(utc + (3600000 * 8))
const year = bjDate.getFullYear()
const month = String(bjDate.getMonth() + 1).padStart(2, '0')
const day = String(bjDate.getDate()).padStart(2, '0')
const hour = bjDate.getHours()
let slot = ''
if (hour >= 0 && hour < 12) slot = 'am'
else slot = 'pm' // 12:00 - 23:59
const currentSlotKey = `${year}-${month}-${day}-${slot}`
const lastSlotKey = lastAlertTimes.get(key)
// Only alert if we haven't alerted in this slot for this specific key
if (lastSlotKey !== currentSlotKey) {
alerts.push({ id: key, title, message, level })
lastAlertTimes.set(key, currentSlotKey)
}
}
// 1. Drop Alerts (Integer steps: 2%, 3%, 4%...)
if (config.alerts.change_percent_down && data.change_percent <= config.alerts.change_percent_down) {
// Check integer steps
const currentDrop = Math.floor(Math.abs(data.change_percent))
const key = `drop_${currentDrop}`
if (currentDrop >= Math.abs(config.alerts.change_percent_down)) {
addAlert(key, `${config.name}跌幅预警`, `当前跌幅 ${data.change_percent.toFixed(2)}% (>= ${currentDrop}%)`)
}
}
// 2. Rise Alerts (Integer steps: 2%, 3%, 4%...)
if (config.alerts.change_percent_up && data.change_percent >= config.alerts.change_percent_up) {
const currentRise = Math.floor(data.change_percent)
const key = `rise_${currentRise}`
if (currentRise >= config.alerts.change_percent_up) {
addAlert(key, `${config.name}涨幅预警`, `当前涨幅 ${data.change_percent.toFixed(2)}% (>= ${currentRise}%)`)
}
}
return alerts
}
module.exports = {
run: async (context, config) => {
const { axios, logJSON } = context
let item = null
if (String(config.source || '').toLowerCase() === 'yahoo_probe') {
const quotes = await fetchYahoo([config.symbol])
item = quotes.find(q => q.symbol === config.symbol)
} else {
const quotes = await fetchTencent(axios, [config.symbol])
item = quotes.find(q => q.symbol === config.symbol)
}
// Fallback: try Tencent if Yahoo failed, or vice versa
if (!item) {
if (String(config.source || '').toLowerCase() === 'yahoo_probe') {
const fb = await fetchTencent(axios, [config.symbol])
item = fb.find(q => q.symbol === config.symbol)
} else {
const fb = await fetchYahoo([config.symbol])
item = fb.find(q => q.symbol === config.symbol)
}
}
if (!item) {
logJSON('skill.fetch.error', { skill: config.name, message: 'Data not found' }, 'market_skills')
return null
}
// Prepare data for DB
const data = [{
label: config.name,
symbol: config.symbol,
source: config.source,
name: item.name,
price: item.price,
prevClose: item.prevClose,
change: item.change,
change_percent: item.change_percent,
captured_at: item.captured_at,
market_status: config.market_status
}]
// Check alerts
const alerts = checkAlerts(item, config)
return { data, alerts }
}
}