Законченный MVP

This commit is contained in:
sbb45
2026-05-28 21:03:35 +05:00
parent a566bab1ae
commit 55583e2101
30 changed files with 1883 additions and 143 deletions
+66
View File
@@ -0,0 +1,66 @@
export type Post = {
id: number;
slug: string;
date: string;
title: string;
description: string;
tags: string[];
content: string;
};
export const posts: Post[] = [
{
id: 1,
slug: 'osnovy-upravleniya-riskami',
date: '2026-05-18',
title: 'Основы управления рисками',
description:
'Как использовать уровни Stop Loss и Take Profit для грамотного управления капиталом при работе с прогнозами.',
tags: ['RISK', 'TRADING', 'EDUCATION'],
content: `
<p>Управление рисками — фундамент любой торговой стратегии. Без него даже точный ML-прогноз не поможет сохранить капитал.</p>
<h2>Stop Loss: где ваша позиция неправа</h2>
<p>Stop Loss — уровень, при достижении которого позиция закрывается автоматически. Это не признание ошибки, а часть стратегии. На графиках Flamy AI уровень SL рассчитывается моделью на основе исторической волатильности инструмента.</p>
<h2>Take Profit: когда забирать прибыль</h2>
<p>Take Profit — целевой уровень закрытия позиции с прибылью. TP рассчитывается пропорционально прогнозному движению с учётом Risk:Reward ratio.</p>
<h2>Risk:Reward Ratio</h2>
<p>Оптимальное соотношение риска к доходности — не менее <strong>1:2</strong>. Это означает, что потенциальная прибыль должна минимум вдвое превышать риск.</p>
<ul>
<li>SL: не более 1% от капитала на сделку</li>
<li>TP: 2% и выше от точки входа</li>
<li>Одна сделка: не более 2% депозита</li>
</ul>
<h2>Почему это важно при работе с ML-прогнозами</h2>
<p>ML-модель не даёт гарантий. Уверенность 80% означает, что 20% прогнозов будут неточными. Управление рисками — страховка на эти 20%.</p>
`
},
{
id: 2,
slug: 'kak-chitat-grafiki-flamy-ai',
date: '2026-05-17',
title: 'Как читать графики Flamy AI',
description:
'Краткое объяснение свечей, прогнозной зоны, TP и SL на графиках Flamy AI.',
tags: ['ML', 'FORECAST', 'CHARTS'],
content: `
<p>Графики Flamy AI содержат три слоя: исторические свечи, прогнозную зону и уровни торгового плана. Разберём каждый из них.</p>
<h2>Исторические свечи</h2>
<p>Левая часть графика — реальные OHLC-свечи. Зелёные — бычьи (close &gt; open), красные — медвежьи. Это основа для анализа модели.</p>
<h2>Прогнозная зона</h2>
<p>Правая часть — прогнозные свечи на горизонт <strong>19 периодов</strong> вперёд. Это не точные значения, а вероятностный коридор движения цены на ближайшие 95 минут.</p>
<h2>Уровни Entry, TP и SL</h2>
<p>Если модель публикует режим TRADE_PLAN, на графике появляются три горизонтальные линии:</p>
<ul>
<li><strong>Entry</strong> — рекомендуемый уровень входа в позицию</li>
<li><strong>TP</strong> — цель Take Profit</li>
<li><strong>SL</strong> — Stop Loss для ограничения убытков</li>
</ul>
<h2>Таймфрейм и горизонт</h2>
<p>Все графики работают на таймфрейме <strong>5 минут</strong>. Горизонт прогноза — 19 свечей = 95 минут вперёд. Прогнозы обновляются каждый час автоматически.</p>
`
}
];
export function getPostBySlug(slug: string): Post | undefined {
return posts.find((p) => p.slug === slug);
}
+246
View File
@@ -0,0 +1,246 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
createChart,
CandlestickSeries,
LineSeries,
CrosshairMode,
LineStyle,
type IChartApi,
type ISeriesApi,
type MouseEventParams,
type UTCTimestamp,
} from 'lightweight-charts';
import type { ChartData, CandleBar } from '$lib/stores/chartStore';
type Props = {
data: ChartData;
decimals: number;
height?: number;
};
let { data, decimals, height = 520 }: Props = $props();
let container = $state<HTMLDivElement>(null!);
let chart: IChartApi | undefined;
let candleSeries: ISeriesApi<'Candlestick'> | undefined;
let predSeries: ISeriesApi<'Line'> | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let priceLines: any[] = [];
type Tooltip = {
visible: boolean;
x: number;
y: number;
time: string;
open: number;
high: number;
low: number;
close: number;
isUp: boolean;
pred?: number;
};
let tip = $state<Tooltip>({
visible: false,
x: 0,
y: 0,
time: '',
open: 0,
high: 0,
low: 0,
close: 0,
isUp: true,
});
function fmt(n: number): string {
if (n >= 10000) return n.toLocaleString('en-US', { maximumFractionDigits: 1 });
if (n >= 100) return n.toLocaleString('en-US', { maximumFractionDigits: 2 });
if (n >= 1) return n.toLocaleString('en-US', { maximumFractionDigits: 4 });
return n.toLocaleString('en-US', { maximumFractionDigits: 6 });
}
function applyData(d: ChartData) {
if (!chart || !candleSeries || !predSeries) return;
candleSeries.setData(d.candles.map((c) => ({ ...c, time: c.time as UTCTimestamp })));
predSeries.setData(d.prediction.map((p) => ({ ...p, time: p.time as UTCTimestamp })));
// Remove old price lines then re-add
priceLines.forEach((pl) => candleSeries!.removePriceLine(pl));
priceLines = [
candleSeries.createPriceLine({
price: d.signal.entry,
color: '#fe4b07',
lineWidth: 1,
lineStyle: LineStyle.Solid,
axisLabelVisible: true,
title: 'Entry',
}),
candleSeries.createPriceLine({
price: d.signal.tp,
color: '#26a69a',
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'TP',
}),
candleSeries.createPriceLine({
price: d.signal.sl,
color: '#ef5350',
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'SL',
}),
];
chart.timeScale().fitContent();
}
onMount(() => {
chart = createChart(container, {
width: container.clientWidth,
height,
layout: {
background: { color: '#09080a' },
textColor: '#8a887f',
fontFamily: "'JetBrains Mono', monospace",
fontSize: 11,
attributionLogo: false,
},
grid: {
vertLines: { color: '#1a191e' },
horzLines: { color: '#1a191e' },
},
crosshair: {
mode: CrosshairMode.Normal,
vertLine: {
color: '#3d3b42',
labelBackgroundColor: '#1a191e',
style: LineStyle.Dashed,
width: 1,
},
horzLine: {
color: '#3d3b42',
labelBackgroundColor: '#1a191e',
style: LineStyle.Dashed,
width: 1,
},
},
rightPriceScale: {
borderColor: '#1a191e',
scaleMargins: { top: 0.06, bottom: 0.04 },
},
timeScale: {
borderColor: '#1a191e',
timeVisible: true,
secondsVisible: false,
rightOffset: 24,
},
});
candleSeries = chart.addSeries(CandlestickSeries, {
upColor: '#26a69a',
downColor: '#ef5350',
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
});
predSeries = chart.addSeries(LineSeries, {
color: '#fe4b07',
lineWidth: 2,
lineStyle: LineStyle.Dashed,
crosshairMarkerVisible: true,
crosshairMarkerRadius: 4,
crosshairMarkerBorderColor: '#fe4b07',
crosshairMarkerBackgroundColor: '#09080a',
priceLineVisible: false,
lastValueVisible: true,
});
applyData(data);
chart.subscribeCrosshairMove((param: MouseEventParams) => {
if (!param.point || !param.time || !candleSeries || !param.seriesData.has(candleSeries)) {
tip = { ...tip, visible: false };
return;
}
const c = param.seriesData.get(candleSeries) as CandleBar;
if (!c) { tip = { ...tip, visible: false }; return; }
const pred = predSeries && param.seriesData.has(predSeries)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? (param.seriesData.get(predSeries) as any)?.value
: undefined;
const ts = param.time as number;
const d = new Date(ts * 1000);
const timeStr = d.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })
+ ' ' + d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
tip = {
visible: true,
x: param.point.x,
y: param.point.y,
time: timeStr,
open: c.open,
high: c.high,
low: c.low,
close: c.close,
isUp: c.close >= c.open,
pred,
};
});
const ro = new ResizeObserver(() => {
chart?.applyOptions({
width: container.clientWidth,
height: container.clientHeight,
});
});
ro.observe(container);
return () => {
ro.disconnect();
chart?.remove();
};
});
// React to data prop changes after mount
$effect(() => {
const d = data;
void decimals; // track but unused — parent re-generates data on change
if (!chart || !candleSeries || !predSeries) return;
applyData(d);
});
</script>
<div class="relative w-full" bind:this={container} style="height: {height}px">
{#if tip.visible}
{@const maxX = container?.clientWidth ?? 500}
{@const left = tip.x + 160 > maxX ? tip.x - 168 : tip.x + 14}
{@const top = Math.max(tip.y - 90, 8)}
<div
class="pointer-events-none absolute z-20 min-w-36 rounded-xl border border-white/8
bg-bg-c/95 px-3 py-2.5 font-mono text-[0.6875rem] shadow-2xl backdrop-blur-sm"
style="left: {left}px; top: {top}px"
>
<p class="mb-2 text-[0.5625rem] tracking-widest text-desc uppercase">{tip.time}</p>
{#each [['O', tip.open], ['H', tip.high], ['L', tip.low], ['C', tip.close]] as [lbl, val]}
<div class="flex justify-between gap-5">
<span class="text-desc">{lbl}</span>
<span class={tip.isUp ? 'text-emerald-400' : 'text-red-400'}>{fmt(val as number)}</span>
</div>
{/each}
{#if tip.pred !== undefined}
<div class="mt-1.5 flex justify-between gap-5 border-t border-white/8 pt-1.5">
<span class="text-primary text-[0.5625rem] tracking-widest uppercase">ML</span>
<span class="text-primary">{fmt(tip.pred)}</span>
</div>
{/if}
</div>
{/if}
</div>
@@ -62,7 +62,7 @@
}
</script>
<div class="w-full max-w-170 z-20 mx-auto lg:mx-0" style="perspective: 1000px;">
<div class="w-full max-w-170 z-20 mx-auto xl:mx-0" style="perspective: 1000px;">
<div
class="overflow-hidden rounded-2xl border border-white/10 bg-zinc-950 shadow-[0_40px_100px_rgba(0,0,0,0.7),0_0_0_1px_rgba(255,255,255,0.04)] will-change-transform"
style="transform: perspective(1000px) rotateY(-6deg) rotateX(3deg); transform-style: preserve-3d;"
+2 -2
View File
@@ -14,7 +14,7 @@
<div class="mx-auto max-w-400 px-5 py-20 sm:px-10">
<div class="grid grid-cols-2 gap-10 sm:grid-cols-4 sm:justify-between">
<div class="col-span-2 flex flex-col gap-4 sm:col-span-1">
<div class="col-span-2 flex flex-col gap-4 lg:col-span-1">
<Logo />
<p class="max-w-55 text-base leading-[1.2] text-zinc-600">
@@ -34,7 +34,7 @@
{/each}
</div>
<div class="col-span-2 flex flex-col gap-1.5 sm:col-span-2 ml-auto">
<div class="col-span-2 flex flex-col gap-1.5 sm:col-span-2 lg:ml-auto">
<span class="font-mono text-base tracking-widest text-zinc-600 uppercase">Дисклеймер</span>
<p class="max-w-85 text-sm leading-relaxed text-zinc-600">
Материалы являются результатом работы исследовательской ML-модели и используются в
+20 -8
View File
@@ -1,22 +1,35 @@
<script lang="ts">
import { onMount } from 'svelte';
import { gsap } from 'gsap';
import { cn } from '$lib/utils';
import Logo from '$lib/components/ui/Logo.svelte';
const menuLinks = [
{ id: 0, href: 'reg', text: 'Главная' },
{ id: 1, href: 'reg', text: 'О проекте' },
{ id: 2, href: 'reg', text: 'Блог' },
{ id: 3, href: 'reg', text: 'Открыть дашборд', isButton: true }
{ id: 0, href: '/', text: 'Главная' },
{ id: 1, href: '/about', text: 'О проекте' },
{ id: 2, href: '/blog', text: 'Блог' },
{ id: 3, href: '/dashboard', text: 'Открыть дашборд', isButton: true }
];
let mobileOpen = $state(false);
let header = $state<HTMLElement>(null!);
onMount(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
gsap.from(header, {
y: -10,
opacity: 0,
duration: 0.5,
ease: 'power2.out',
clearProps: 'transform,opacity',
});
});
</script>
<header class="fixed right-0 left-0 border-b border-b-bg-h bg-bg-e z-9999">
<div class="mx-auto flex max-w-400 items-center justify-between px-5 py-5 sm:px-10 lg:px-0">
<header bind:this={header} class="fixed right-0 left-0 border-b border-b-bg-h bg-bg-e z-9999">
<div class="mx-auto flex max-w-400 items-center justify-between px-5 py-5 sm:px-10 lg:px-5">
<Logo />
<!-- Десктопная навигация -->
<nav class="hidden lg:block">
<ul class="flex items-center justify-center gap-5.5 transition-colors duration-300">
{#each menuLinks as link (link.id)}
@@ -32,7 +45,6 @@
</ul>
</nav>
<!-- Мобильная кнопка гамбургера -->
<button
class="flex flex-col justify-center gap-1.5 p-1 lg:hidden"
onclick={() => (mobileOpen = !mobileOpen)}
+4 -2
View File
@@ -6,10 +6,11 @@
href?: string;
target?: string;
variant?: 'primary' | 'secondary';
class?: string;
children: Snippet;
};
let { href, target = '_self', variant = 'primary', children }: Props = $props();
let { href, target = '_self', variant = 'primary', class: className, children }: Props = $props();
</script>
<svelte:element
@@ -20,7 +21,8 @@
class={cn(
'flex items-center justify-center gap-2 w-fit rounded-xl px-6 py-3 font-display text-lg font-medium transition-colors duration-300',
variant === 'primary' && 'bg-primary hover:bg-primary-h',
variant === 'secondary' && 'bg-bg border hover:text-desc'
variant === 'secondary' && 'bg-bg border hover:text-desc',
className
)}
>
{@render children()}
+168
View File
@@ -0,0 +1,168 @@
/**
* Svelte actions wrapping GSAP + ScrollTrigger.
*
* Each action returns { destroy } for automatic cleanup when the element
* leaves the DOM. gsap.context() scopes all tweens so they're properly
* killed in destroy, preventing memory leaks on navigation.
*
* ScrollTrigger is registered once on first import (browser-only).
*/
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
function prefersReducedMotion(): boolean {
return typeof window !== 'undefined'
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false;
}
// ─── Types ──────────────────────────────────────────────────────────────────
export type FadeUpParams = {
y?: number;
delay?: number;
duration?: number;
start?: string;
};
export type RevealLeftParams = {
x?: number;
delay?: number;
duration?: number;
start?: string;
};
export type StaggerParams = {
y?: number;
x?: number;
delay?: number;
duration?: number;
stagger?: number;
start?: string;
selector?: string;
};
// ─── Actions ────────────────────────────────────────────────────────────────
/**
* Fade up a single element when it scrolls into view.
* use:scrollFadeUp or use:scrollFadeUp={{ y: 24, delay: 0.15 }}
*/
export function scrollFadeUp(el: HTMLElement, params: FadeUpParams = {}) {
if (prefersReducedMotion()) return;
const { y = 36, delay = 0, duration = 0.65, start = 'top 84%' } = params;
gsap.set(el, { opacity: 0, y });
const ctx = gsap.context(() => {
gsap.to(el, {
opacity: 1,
y: 0,
duration,
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
});
});
return { destroy: () => ctx.revert() };
}
/**
* Slide in from the left when element scrolls into view — good for headings.
* use:scrollRevealLeft
*/
export function scrollRevealLeft(el: HTMLElement, params: RevealLeftParams = {}) {
if (prefersReducedMotion()) return;
const { x = -48, delay = 0, duration = 0.7, start = 'top 82%' } = params;
gsap.set(el, { opacity: 0, x });
const ctx = gsap.context(() => {
gsap.to(el, {
opacity: 1,
x: 0,
duration,
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
});
});
return { destroy: () => ctx.revert() };
}
/**
* Stagger-animate direct children (or custom selector) of a container
* when it scrolls into view.
* use:scrollStagger or use:scrollStagger={{ stagger: 0.08, selector: 'li' }}
*/
export function scrollStagger(el: HTMLElement, params: StaggerParams = {}) {
if (prefersReducedMotion()) return;
const {
y = 32,
x = 0,
delay = 0,
duration = 0.65,
stagger = 0.1,
start = 'top 80%',
selector = ':scope > *',
} = params;
const targets = el.querySelectorAll<HTMLElement>(selector);
if (!targets.length) return;
gsap.set(targets, { opacity: 0, y, x });
const ctx = gsap.context(() => {
gsap.to(targets, {
opacity: 1,
y: 0,
x: 0,
duration,
delay,
ease: 'power3.out',
stagger: { each: stagger, ease: 'power1.inOut' },
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
});
});
return { destroy: () => ctx.revert() };
}
/**
* Scale + fade in a single element — good for cards and panels.
* use:scrollScaleIn
*/
export function scrollScaleIn(el: HTMLElement, params: { delay?: number; start?: string } = {}) {
if (prefersReducedMotion()) return;
const { delay = 0, start = 'top 82%' } = params;
gsap.set(el, { opacity: 0, scale: 0.94, y: 20 });
const ctx = gsap.context(() => {
gsap.to(el, {
opacity: 1,
scale: 1,
y: 0,
duration: 0.7,
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
});
});
return { destroy: () => ctx.revert() };
}
+141
View File
@@ -0,0 +1,141 @@
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 },
];
let pPrice = currentPrice;
for (let i = 1; i <= HORIZON; i++) {
const t = i / HORIZON;
const noise = (rand() - 0.5) * vol * 0.6;
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,
};
}