149 lines
4.7 KiB
TypeScript
149 lines
4.7 KiB
TypeScript
export type TimeframeId = '1m' | '5m' | '15m' | '1h' | '4h' | '1d';
|
|
export type Direction = 'long' | 'short';
|
|
|
|
export type CandleBar = {
|
|
time: number;
|
|
open: number;
|
|
high: number;
|
|
low: number;
|
|
close: number;
|
|
};
|
|
|
|
export type PredPoint = {
|
|
time: number;
|
|
value: number;
|
|
};
|
|
|
|
export type Signal = {
|
|
direction: Direction;
|
|
confidence: number;
|
|
entry: number;
|
|
tp: number;
|
|
sl: number;
|
|
};
|
|
|
|
export type ChartData = {
|
|
candles: CandleBar[];
|
|
prediction: PredPoint[];
|
|
signal: Signal;
|
|
currentPrice: number;
|
|
change24h: number;
|
|
change24hPct: number;
|
|
};
|
|
|
|
export const SYMBOLS = [
|
|
{ id: 'BTCUSDT', label: 'BTC/USDT', short: 'BTC', base: 103500, dec: 1 },
|
|
{ id: 'ETHUSDT', label: 'ETH/USDT', short: 'ETH', base: 3800, dec: 2 },
|
|
{ id: 'SOLUSDT', label: 'SOL/USDT', short: 'SOL', base: 176, dec: 3 },
|
|
{ id: 'BNBUSDT', label: 'BNB/USDT', short: 'BNB', base: 585, dec: 2 },
|
|
{ id: 'XRPUSDT', label: 'XRP/USDT', short: 'XRP', base: 0.55, dec: 5 },
|
|
{ id: 'ADAUSDT', label: 'ADA/USDT', short: 'ADA', base: 0.43, dec: 5 },
|
|
{ id: 'DOGEUSDT', label: 'DOGE/USDT', short: 'DOGE', base: 0.131, dec: 5 },
|
|
{ id: 'LINKUSDT', label: 'LINK/USDT', short: 'LINK', base: 18.5, dec: 3 }
|
|
] as const;
|
|
|
|
export const TIMEFRAMES = [
|
|
{ id: '1m' as TimeframeId, label: '1м', sec: 60 },
|
|
{ id: '5m' as TimeframeId, label: '5м', sec: 300 },
|
|
{ id: '15m' as TimeframeId, label: '15м', sec: 900 },
|
|
{ id: '1h' as TimeframeId, label: '1ч', sec: 3600 },
|
|
{ id: '4h' as TimeframeId, label: '4ч', sec: 14400 },
|
|
{ id: '1d' as TimeframeId, label: '1д', sec: 86400 }
|
|
];
|
|
|
|
// Simple LCG PRNG — deterministic per seed so switching back gives the same chart
|
|
function rng(seed: number) {
|
|
let s = seed >>> 0;
|
|
return () => {
|
|
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
return s / 0x100000000;
|
|
};
|
|
}
|
|
|
|
const HISTORY = 160;
|
|
const HORIZON = 19;
|
|
|
|
export function generateChartData(symbolId: string, tfId: TimeframeId): ChartData {
|
|
const sym = SYMBOLS.find((s) => s.id === symbolId) ?? SYMBOLS[0];
|
|
const tf = TIMEFRAMES.find((t) => t.id === tfId) ?? TIMEFRAMES[1];
|
|
|
|
const seed =
|
|
[...symbolId].reduce((a, c, i) => a + c.charCodeAt(0) * (i + 1), 0) +
|
|
[...tfId].reduce((a, c, i) => a + c.charCodeAt(0) * (i + 1), 0);
|
|
const rand = rng(seed);
|
|
|
|
const now = Math.floor(Date.now() / tf.sec) * tf.sec;
|
|
const startTime = now - HISTORY * tf.sec;
|
|
|
|
const vol = sym.base * 0.004;
|
|
let price = sym.base * (0.88 + rand() * 0.24);
|
|
const trendBias = (rand() - 0.5) * 0.001;
|
|
|
|
const candles: CandleBar[] = [];
|
|
|
|
for (let i = 0; i < HISTORY; i++) {
|
|
const open = price;
|
|
const move = (rand() - 0.5 + trendBias) * vol * 2;
|
|
const close = Math.max(open + move, open * 0.001);
|
|
const bodyH = Math.abs(close - open);
|
|
const wk = 0.3 + rand() * 0.8;
|
|
const high = Math.max(open, close) + bodyH * wk + rand() * vol * 0.2;
|
|
const low = Math.min(open, close) - bodyH * wk * 0.7 - rand() * vol * 0.2;
|
|
|
|
const round = (n: number) => parseFloat(n.toFixed(Math.min(sym.dec + 2, 8)));
|
|
candles.push({
|
|
time: startTime + i * tf.sec,
|
|
open: round(open),
|
|
high: round(high),
|
|
low: round(low),
|
|
close: round(close)
|
|
});
|
|
price = close;
|
|
}
|
|
|
|
const currentPrice = candles[candles.length - 1].close;
|
|
|
|
// 24h change — look back ~288 candles for 5m, scaled to timeframe
|
|
const lookback = Math.min(Math.round(86400 / tf.sec), HISTORY - 1);
|
|
const oldPrice = candles[HISTORY - 1 - lookback].close;
|
|
const change24h = currentPrice - oldPrice;
|
|
const change24hPct = (change24h / oldPrice) * 100;
|
|
|
|
// ML prediction — starts at last candle and extends HORIZON candles forward
|
|
const predDir = rand() > 0.42 ? 1 : -1;
|
|
const predStrength = (0.006 + rand() * 0.018) * sym.base;
|
|
|
|
const prediction: PredPoint[] = [{ time: candles[HISTORY - 1].time, value: currentPrice }];
|
|
|
|
for (let i = 1; i <= HORIZON; i++) {
|
|
const t = i / HORIZON;
|
|
const noise = (rand() - 0.5) * vol * 0.6;
|
|
const pPrice = currentPrice + predDir * predStrength * t + noise;
|
|
const round = (n: number) => parseFloat(n.toFixed(Math.min(sym.dec + 2, 8)));
|
|
prediction.push({ time: now + i * tf.sec, value: round(Math.max(pPrice, currentPrice * 0.1)) });
|
|
}
|
|
|
|
const finalPred = prediction[prediction.length - 1].value;
|
|
const predRange = Math.abs(finalPred - currentPrice);
|
|
const direction: Direction = predDir > 0 ? 'long' : 'short';
|
|
const confidence = Math.round(52 + rand() * 33);
|
|
|
|
const round = (n: number) => parseFloat(n.toFixed(Math.min(sym.dec + 2, 8)));
|
|
const entry = round(currentPrice);
|
|
const tp = round(
|
|
direction === 'long' ? currentPrice + predRange * 1.3 : currentPrice - predRange * 1.3
|
|
);
|
|
const sl = round(
|
|
direction === 'long' ? currentPrice - predRange * 0.65 : currentPrice + predRange * 0.65
|
|
);
|
|
|
|
return {
|
|
candles,
|
|
prediction,
|
|
signal: { direction, confidence, entry, tp, sl },
|
|
currentPrice,
|
|
change24h,
|
|
change24hPct
|
|
};
|
|
}
|