Spaces:
Running
Running
File size: 3,108 Bytes
66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 ebfcd8b 66b7a93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | /**
* yahooWebSocket.js
* Simulated WebSocket stream class using an HTTP polling mechanism
* against Yahoo Finance. This serves as a key-free, drop-in replacement
* for Finnhub's WebSocket stream.
*/
import { fetchCandles } from './yahooFinanceAPI.js';
const TIMEFRAME_SECONDS = {
'1m': 60,
'5m': 300,
'15m': 900,
'1H': 3600,
'4H': 14400,
'1D': 86400,
};
export class YahooStream {
constructor() {
/** @type {any} */
this._intervalTimer = null;
/** @type {string|null} */
this._symbol = null;
/** @type {string|null} */
this._timeframe = null;
/** @type {{ onCandleUpdate?: Function, onTick?: Function }|null} */
this._callbacks = null;
this._isPolling = false;
}
/**
* Subscribe to real-time updates for a symbol.
*
* @param {string} symbol - Local symbol (XAUUSD, EURUSD, etc.)
* @param {string} timeframe - Local timeframe (1m, 5m, 15m, 1H, 4H, 1D)
* @param {{ onCandleUpdate?: (candle: object) => void, onTick?: (price: number) => void }} callbacks
*/
subscribe(symbol, timeframe, callbacks, options = {}) {
this.unsubscribe();
this._symbol = symbol;
this._timeframe = timeframe;
this._callbacks = callbacks;
this._stopped = false;
// Self-scheduling poll with exponential backoff: transient Yahoo errors / 429s
// slow the loop down instead of hammering. Optional stagger spreads multiple
// symbols so they don't all hit Yahoo on the same tick.
this._baseMs = options.pollMs || 6000;
this._maxMs = options.maxPollMs || 60000;
this._currentMs = this._baseMs;
this._timer = setTimeout(() => this._loop(), options.staggerMs || 0);
}
/** Disconnect the stream. */
unsubscribe() {
this._stopped = true;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
this._symbol = null;
this._timeframe = null;
this._callbacks = null;
}
/** @private */
async _loop() {
if (this._stopped || !this._symbol || !this._timeframe) return;
let ok = false;
try {
const candles = await fetchCandles(this._symbol, this._timeframe);
if (candles && candles.length > 0) {
ok = true;
const latestCandle = candles[candles.length - 1];
if (this._callbacks?.onTick) {
this._callbacks.onTick(latestCandle.close);
}
const duration = TIMEFRAME_SECONDS[this._timeframe] || 900;
const nowUnix = Math.floor(Date.now() / 1000);
const isClosed = (nowUnix - latestCandle.time) >= duration;
if (this._callbacks?.onCandleUpdate) {
this._callbacks.onCandleUpdate({ ...latestCandle, isClosed });
}
}
} catch (err) {
console.error(`[YahooStream] Polling error for ${this._symbol}:`, err.message);
}
// Reset cadence on success, back off on failure up to the cap.
this._currentMs = ok ? this._baseMs : Math.min(this._maxMs, Math.round(this._currentMs * 1.8));
if (!this._stopped) {
this._timer = setTimeout(() => this._loop(), this._currentMs);
}
}
}
export default YahooStream;
|