Законченный MVP
This commit is contained in:
@@ -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 > 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);
|
||||
}
|
||||
@@ -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>
|
||||
+1
-1
@@ -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;"
|
||||
@@ -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-модели и используются в
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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() };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,33 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import DashboardStatus from '$lib/components/DashboardStatus.svelte';
|
||||
import DashboardStatus from '$lib/components/home/DashboardStatus.svelte';
|
||||
import { scrollRevealLeft, scrollFadeUp, scrollStagger } from '$lib/gsap/actions';
|
||||
</script>
|
||||
|
||||
<div class="relative bg-bg-e">
|
||||
<section
|
||||
class="flex flex-col items-start gap-10 px-5 py-16
|
||||
sm:px-10 sm:py-20
|
||||
lg:flex-row lg:items-center lg:justify-center lg:gap-40 lg:h-[70vh] lg:py-0"
|
||||
lg:h-[70vh] lg:flex-row lg:items-center lg:justify-center lg:gap-40 lg:py-0"
|
||||
>
|
||||
<div>
|
||||
<h2 class="font-display text-4xl font-bold sm:text-5xl lg:text-7xl">Дашборд</h2>
|
||||
<p class="mt-6 max-w-full text-base text-desc sm:text-lg sm:mt-8 lg:mt-8 lg:max-w-140 lg:text-xl">
|
||||
<h2
|
||||
use:scrollRevealLeft
|
||||
class="font-display text-4xl font-bold sm:text-5xl lg:text-7xl"
|
||||
>Дашборд</h2>
|
||||
|
||||
<p
|
||||
use:scrollFadeUp={{ delay: 0.1 }}
|
||||
class="mt-6 max-w-full text-base text-desc sm:mt-8 sm:text-lg lg:mt-8 lg:max-w-140 lg:text-xl"
|
||||
>
|
||||
Все инструменты одним взглядом. Интерактивные графики, уровни входа и выхода, статистика
|
||||
winrate — доступно без регистрации.
|
||||
</p>
|
||||
<ul class="mt-5 mb-8 flex flex-col gap-2 text-base text-desc sm:text-lg lg:text-xl lg:gap-1 lg:mt-6 lg:mb-10">
|
||||
|
||||
<ul
|
||||
use:scrollStagger={{ selector: 'li', stagger: 0.07, y: 16, delay: 0.05 }}
|
||||
class="mt-5 mb-8 flex flex-col gap-2 text-base text-desc sm:text-lg lg:mt-6 lg:mb-10 lg:gap-1 lg:text-xl"
|
||||
>
|
||||
<li class="flex items-center gap-2">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full bg-primary"></span>
|
||||
Интерактивные свечные графики с прогнозом
|
||||
@@ -29,11 +41,14 @@
|
||||
Поиск и фильтрация по направлению
|
||||
</li>
|
||||
</ul>
|
||||
<Button href="/dashboard">Перейти к дашборду</Button>
|
||||
|
||||
<div use:scrollFadeUp={{ delay: 0.2 }}>
|
||||
<Button href="/dashboard">Перейти к дашборду</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DashboardStatus />
|
||||
<div use:scrollFadeUp={{ y: 24, delay: 0.15 }} class="w-full max-w-140 shrink-0">
|
||||
<DashboardStatus />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="noise"></div>
|
||||
</div>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { scrollFadeUp, scrollStagger } from '$lib/gsap/actions';
|
||||
|
||||
type Post = {
|
||||
slug: string;
|
||||
date: string;
|
||||
@@ -25,8 +27,11 @@
|
||||
{#if posts.length > 0}
|
||||
<div class="border-b border-white/6">
|
||||
<section class="px-5 py-24 sm:px-10 sm:py-32">
|
||||
<div class="mb-12 flex items-end justify-between">
|
||||
<h2 class="font-display text-4xl font-black uppercase tracking-tighter">БЛОГ</h2>
|
||||
<div
|
||||
use:scrollFadeUp={{ y: 20 }}
|
||||
class="mb-12 flex items-end justify-between"
|
||||
>
|
||||
<h2 class="font-display text-4xl font-black tracking-tighter uppercase">БЛОГ</h2>
|
||||
<a
|
||||
href="/blog"
|
||||
class="font-display text-[0.75rem] font-bold tracking-wider text-desc transition-colors duration-150 hover:text-white"
|
||||
@@ -36,6 +41,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:scrollStagger={{ selector: 'a', stagger: 0.1, y: 32 }}
|
||||
class="grid grid-cols-1 overflow-hidden rounded-xl border border-white/6 sm:grid-cols-[repeat(auto-fit,minmax(20rem,1fr))]"
|
||||
>
|
||||
{#each posts as post, i}
|
||||
@@ -45,17 +51,20 @@
|
||||
{i > 0 ? 'border-t border-white/6 sm:border-t-0 sm:border-l' : ''}"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<time datetime={post.date} class="font-mono text-[0.6875rem] tracking-widest text-zinc-600">
|
||||
<time
|
||||
datetime={post.date}
|
||||
class="font-mono text-[0.6875rem] tracking-widest text-zinc-600"
|
||||
>
|
||||
{fmtDate(post.date)}
|
||||
</time>
|
||||
{#if post.tags?.length}
|
||||
<span class="font-mono text-[0.625rem] uppercase tracking-widest text-zinc-600">
|
||||
<span class="font-mono text-[0.625rem] tracking-widest text-zinc-600 uppercase">
|
||||
{post.tags[0]}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<h3
|
||||
class="font-display font-black leading-tight tracking-tight text-[clamp(1.25rem,2.5vw,1.75rem)]"
|
||||
class="font-display text-[clamp(1.25rem,2.5vw,1.75rem)] leading-tight font-black tracking-tight"
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
|
||||
let section: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const heading = section.querySelector<HTMLElement>('[data-cta-heading]');
|
||||
const btnWrap = section.querySelector<HTMLElement>('[data-cta-btn]');
|
||||
const disclaimer = section.querySelector<HTMLElement>('[data-cta-note]');
|
||||
|
||||
if (heading) {
|
||||
gsap.set(heading, { opacity: 0, scale: 0.88, y: 32 });
|
||||
gsap.to(heading, {
|
||||
opacity: 1, scale: 1, y: 0,
|
||||
duration: 0.8, ease: 'power3.out',
|
||||
clearProps: 'transform,opacity',
|
||||
scrollTrigger: { trigger: section, start: 'top 75%', once: true },
|
||||
});
|
||||
}
|
||||
if (btnWrap) {
|
||||
gsap.set(btnWrap, { opacity: 0, y: 20 });
|
||||
gsap.to(btnWrap, {
|
||||
opacity: 1, y: 0,
|
||||
duration: 0.6, delay: 0.2, ease: 'power3.out',
|
||||
clearProps: 'transform,opacity',
|
||||
scrollTrigger: { trigger: section, start: 'top 75%', once: true },
|
||||
});
|
||||
}
|
||||
if (disclaimer) {
|
||||
gsap.set(disclaimer, { opacity: 0 });
|
||||
gsap.to(disclaimer, {
|
||||
opacity: 1,
|
||||
duration: 0.6, delay: 0.4,
|
||||
clearProps: 'opacity',
|
||||
scrollTrigger: { trigger: section, start: 'top 75%', once: true },
|
||||
});
|
||||
}
|
||||
}, section);
|
||||
|
||||
return () => ctx.revert();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative overflow-hidden">
|
||||
<section
|
||||
bind:this={section}
|
||||
class="z-20 flex min-h-[70vh] flex-col items-center justify-center overflow-hidden px-5 text-center sm:px-10 lg:h-[80vh] lg:min-h-0"
|
||||
>
|
||||
<div class="relative flex flex-col items-center justify-center gap-6 lg:gap-8">
|
||||
<h2
|
||||
data-cta-heading
|
||||
class="font-display text-[clamp(2.5rem,10vw,9rem)] leading-[0.9] font-black tracking-tighter uppercase"
|
||||
>
|
||||
ОТКРОЙТЕ<br /><span class="text-primary">ДАШБОРД</span>
|
||||
</h2>
|
||||
<div data-cta-btn>
|
||||
<Button href="/dashboard">Открыть дашборд</Button>
|
||||
</div>
|
||||
<p data-cta-note class="font-mono text-sm leading-[1.2] text-zinc-600 sm:text-lg">
|
||||
Материалы носят исследовательский характер.<br />Не являются инвестиционными рекомендациями.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute -top-210 left-0 z-10 h-320 w-140 rotate-80 rounded-full bg-primary/20 blur-3xl md:-top-180"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 -bottom-210 z-10 h-320 w-140 rotate-80 rounded-full bg-primary/20 blur-3xl md:-bottom-180"
|
||||
></div>
|
||||
</div>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { scrollRevealLeft, scrollStagger } from '$lib/gsap/actions';
|
||||
|
||||
type Feature = { num: string; title: string; desc: string };
|
||||
|
||||
const features: Feature[] = [
|
||||
@@ -37,16 +39,23 @@
|
||||
|
||||
<div class="border-b border-white/6">
|
||||
<section class="px-5 py-24 sm:px-10 sm:py-36">
|
||||
<h2 class="mb-16 font-display text-4xl font-black uppercase tracking-tighter">ВОЗМОЖНОСТИ</h2>
|
||||
<h2
|
||||
use:scrollRevealLeft
|
||||
class="mb-16 font-display text-4xl font-black tracking-tighter uppercase"
|
||||
>ВОЗМОЖНОСТИ</h2>
|
||||
|
||||
<div class="border-t border-white/6">
|
||||
<div
|
||||
use:scrollStagger={{ selector: '[data-feature-row]', stagger: 0.07, y: 28 }}
|
||||
class="border-t border-white/6"
|
||||
>
|
||||
{#each features as f (f.num)}
|
||||
<div
|
||||
data-feature-row
|
||||
class="group cursor-default border-b border-white/6 py-6 transition-colors duration-150 hover:bg-primary/2.5
|
||||
md:grid md:grid-cols-[80px_1fr_1fr] md:items-center md:gap-8 md:py-8"
|
||||
>
|
||||
<span
|
||||
class="block font-display text-3xl font-black leading-none tracking-tighter text-primary opacity-20 transition-opacity duration-150 group-hover:opacity-100
|
||||
class="block font-display text-3xl leading-none font-black tracking-tighter text-primary opacity-20 transition-opacity duration-150 group-hover:opacity-100
|
||||
md:text-[clamp(2rem,4vw,3.5rem)]"
|
||||
>
|
||||
{f.num}
|
||||
@@ -63,7 +72,5 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="noise"></div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { gsap } from 'gsap';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import DashboardMockup from '$lib/components/home/DashboardMockup.svelte';
|
||||
|
||||
let section: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const words = section.querySelectorAll<HTMLElement>('[data-hero-word]');
|
||||
const desc = section.querySelector<HTMLElement>('[data-hero-desc]');
|
||||
const cta = section.querySelector<HTMLElement>('[data-hero-cta]');
|
||||
const mock = section.querySelector<HTMLElement>('[data-hero-mockup]');
|
||||
|
||||
gsap.set(words, { opacity: 0, y: 44, skewX: -6 });
|
||||
gsap.set(desc, { opacity: 0, y: 24 });
|
||||
gsap.set(cta, { opacity: 0, y: 16 });
|
||||
gsap.set(mock, { opacity: 0, x: 48 });
|
||||
|
||||
const tl = gsap.timeline({
|
||||
defaults: { ease: 'power3.out' },
|
||||
onComplete: () => gsap.set([words, desc, cta, mock], { clearProps: 'all' }),
|
||||
});
|
||||
|
||||
words.forEach((w, i) =>
|
||||
tl.to(w, { opacity: 1, y: 0, skewX: 0, duration: 0.75 }, 0.05 + i * 0.1)
|
||||
);
|
||||
tl.to(desc, { opacity: 1, y: 0, duration: 0.65, ease: 'power2.out' }, 0.42);
|
||||
tl.to(cta, { opacity: 1, y: 0, duration: 0.55, ease: 'expo.out' }, 0.58);
|
||||
tl.to(mock, { opacity: 1, x: 0, duration: 0.9, ease: 'power2.out' }, 0.18);
|
||||
}, section);
|
||||
|
||||
return () => ctx.revert();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section
|
||||
bind:this={section}
|
||||
class="flex min-h-screen flex-col gap-10 px-5 pt-24 pb-14
|
||||
sm:px-10 sm:pt-28
|
||||
xl:h-screen xl:min-h-0 xl:flex-row xl:items-center xl:justify-between xl:gap-0 xl:px-5 xl:pt-16 xl:pb-0"
|
||||
>
|
||||
<div class="z-20 xl:transform-[perspective(1000px)_rotateY(6deg)_rotateX(3deg)]">
|
||||
<h1 class="flex flex-col font-display leading-[0.95] font-black uppercase">
|
||||
<span data-hero-word class="text-[clamp(2.5rem,10vw,6.25rem)]">Рыночный</span>
|
||||
<span data-hero-word class="text-[clamp(3rem,13vw,8rem)]">прогноз</span>
|
||||
<span data-hero-word class="text-[clamp(1.5rem,6.5vw,4rem)] leading-none text-primary">от ML-модели</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
data-hero-desc
|
||||
class="mt-6 max-w-full text-base leading-[1.3] text-desc sm:mt-8 sm:text-xl lg:mt-10 xl:max-w-180 lg:text-2xl lg:leading-[1.1]"
|
||||
>
|
||||
Исторические свечи, прогнозная зона, уровни входа и выхода — всё на одном интерактивном
|
||||
графике. Горизонт 19 свечей, таймфрейм 5M.
|
||||
</p>
|
||||
|
||||
<div data-hero-cta class="mt-8 flex items-center justify-start gap-4 lg:mt-10">
|
||||
<Button href="/dashboard">Открыть дашборд</Button>
|
||||
<Button href="/about" variant="secondary">Как работает</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-hero-mockup class="w-full mx-auto xl:mx-0 z-20">
|
||||
<DashboardMockup />
|
||||
</div>
|
||||
|
||||
<!-- Визуальные элементы -->
|
||||
<span
|
||||
class="absolute -top-60 left-0 z-0 h-80 w-full rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
<span class="absolute bottom-35 left-0 z-0 hidden h-px w-full bg-bg-h xl:block"></span>
|
||||
<span class="absolute top-55 left-0 z-0 hidden h-px w-full bg-bg-h xl:block"></span>
|
||||
</section>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { scrollStagger } from '$lib/gsap/actions';
|
||||
|
||||
type Stat = { num: string; suffix: string; label: string; sub: string };
|
||||
|
||||
const stats: Stat[] = [
|
||||
@@ -8,40 +10,41 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="relative border-b border-white/6 bg-bg-e">
|
||||
<div class="noise"></div>
|
||||
<section class="flex justify-center items-center px-5 py-14 sm:px-10 sm:py-20 lg:py-0">
|
||||
<div class="border-b border-white/6 bg-bg-e">
|
||||
<section class="flex items-center justify-center px-5 py-14 sm:px-10 sm:py-20 lg:py-0">
|
||||
<div
|
||||
class="grid grid-cols-1 overflow-hidden rounded-xl border border-white/6 w-full sm:w-max sm:grid-cols-3"
|
||||
use:scrollStagger={{ selector: '[data-stat]', stagger: 0.12, y: 28 }}
|
||||
class="grid w-full grid-cols-1 overflow-hidden rounded-xl border border-white/6 md:w-max md:grid-cols-3"
|
||||
>
|
||||
{#each stats as stat, i (stat.num)}
|
||||
<div
|
||||
class="relative overflow-hidden px-8 py-10 border-white/6 sm:px-14 sm:py-16
|
||||
data-stat
|
||||
class="relative overflow-hidden border-white/6 px-8 py-10 sm:px-14 sm:py-16
|
||||
{i > 0 ? 'border-t sm:border-t-0 sm:border-l' : ''}"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute w-full h-full left-0 -bottom-80 bg-primary/40 rounded-3xl blur-3xl"
|
||||
class="pointer-events-none absolute -bottom-80 left-0 h-full w-full rounded-3xl bg-primary/40 blur-3xl"
|
||||
></div>
|
||||
<div class="relative flex flex-col gap-4 text-center">
|
||||
<div class="flex items-baseline justify-center gap-1">
|
||||
<span
|
||||
class="font-display font-black leading-none tracking-tighter text-[clamp(3.5rem,9vw,8rem)]"
|
||||
class="font-display text-[clamp(3.5rem,9vw,8rem)] leading-none font-black tracking-tighter"
|
||||
>
|
||||
{stat.num}
|
||||
</span>
|
||||
{#if stat.suffix}
|
||||
<span
|
||||
class="font-display font-black leading-none tracking-tighter text-primary text-[clamp(2rem,5vw,4rem)]"
|
||||
class="font-display text-[clamp(2rem,5vw,4rem)] leading-none font-black tracking-tighter text-primary"
|
||||
>
|
||||
{stat.suffix}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-mono text-lg tracking-widest leading-none text-primary">
|
||||
<span class="font-mono text-lg leading-none tracking-widest text-primary">
|
||||
{stat.label}
|
||||
</span>
|
||||
<span class="text-xl text-desc leading-[1.2]">{stat.sub}</span>
|
||||
<span class="text-xl leading-[1.2] text-desc">{stat.sub}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="noise"></div>
|
||||
<Header />
|
||||
{@render children()}
|
||||
<Footer />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Hero from './_sections/Hero.svelte';
|
||||
import Ticker from './_sections/Ticker.svelte';
|
||||
import About from './_sections/About.svelte';
|
||||
import Features from './_sections/Features.svelte';
|
||||
import Stats from './_sections/Stats.svelte';
|
||||
import BlogPreview from './_sections/BlogPreview.svelte';
|
||||
import Cta from './_sections/Cta.svelte';
|
||||
|
||||
import Hero from './(sections)/Hero.svelte';
|
||||
import Ticker from './(sections)/Ticker.svelte';
|
||||
import About from './(sections)/About.svelte';
|
||||
import Features from './(sections)/Features.svelte';
|
||||
import Stats from './(sections)/Stats.svelte';
|
||||
import BlogPreview from './(sections)/BlogPreview.svelte';
|
||||
import Cta from './(sections)/Cta.svelte';
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
</script>
|
||||
|
||||
<div class="relative overflow-hidden">
|
||||
<section
|
||||
class="flex flex-col items-center justify-center overflow-hidden text-center z-20 px-5 min-h-[70vh] sm:px-10 lg:h-[80vh] lg:min-h-0"
|
||||
>
|
||||
<div class="relative flex flex-col items-center justify-center gap-6 lg:gap-8">
|
||||
<h2
|
||||
class="font-display font-black uppercase leading-[0.9] tracking-tighter text-[clamp(2.5rem,10vw,9rem)]"
|
||||
>
|
||||
ОТКРОЙТЕ<br /><span class="text-primary">ДАШБОРД</span>
|
||||
</h2>
|
||||
<Button href="/dashboard">Открыть дашборд</Button>
|
||||
<p class="font-mono text-sm leading-[1.2] text-zinc-600 sm:text-lg">
|
||||
Материалы носят исследовательский характер.<br />Не являются инвестиционными рекомендациями.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute -top-180 left-0 w-140 h-320 rotate-80 bg-primary/20 rounded-full blur-3xl z-10"
|
||||
></div>
|
||||
<div
|
||||
class="pointer-events-none absolute -bottom-180 right-0 w-140 h-320 rotate-80 bg-primary/20 rounded-full blur-3xl z-10"
|
||||
></div>
|
||||
<div class="noise"></div>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import DashboardMockup from '$lib/components/DashboardMockup.svelte';
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="flex flex-col gap-10 min-h-screen px-5 pt-24 pb-14
|
||||
sm:px-10 sm:pt-28
|
||||
lg:flex-row lg:items-center lg:justify-between lg:h-screen lg:min-h-0 lg:px-0 lg:pt-16 lg:pb-0 lg:gap-0"
|
||||
>
|
||||
<div class="z-20 lg:[transform:perspective(1000px)_rotateY(6deg)_rotateX(3deg)]">
|
||||
<h1 class="flex flex-col font-display leading-[0.95] font-black uppercase">
|
||||
<span class="text-[clamp(2.5rem,10vw,6.25rem)]"> Рыночный </span>
|
||||
<span class="text-[clamp(3rem,13vw,8rem)]"> прогноз </span>
|
||||
<span class="text-[clamp(1.5rem,6.5vw,4rem)] leading-none text-primary"> от ML-модели </span>
|
||||
</h1>
|
||||
|
||||
<p class="mt-6 max-w-full text-base leading-[1.3] text-desc sm:text-xl sm:mt-8 lg:mt-10 lg:max-w-180 lg:text-2xl lg:leading-[1.1]">
|
||||
Исторические свечи, прогнозная зона, уровни входа и выхода — всё на одном интерактивном
|
||||
графике. Горизонт 19 свечей, таймфрейм 5M.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex items-center justify-start gap-4 lg:mt-10">
|
||||
<Button href="/dashboard">Открыть дашборд</Button>
|
||||
<Button href="/about" variant="secondary">Как работает</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DashboardMockup />
|
||||
|
||||
<!-- Визуальные элементы -->
|
||||
<span
|
||||
class="absolute -top-60 left-0 z-0 h-80 w-full rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
<span class="absolute bottom-35 left-0 z-0 h-px w-full bg-bg-h hidden lg:block"></span>
|
||||
<span class="absolute top-55 left-0 z-0 h-px w-full bg-bg-h hidden lg:block"></span>
|
||||
<div class="noise"></div>
|
||||
</section>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { about, params} from './_data';
|
||||
import { scrollFadeUp, scrollRevealLeft, scrollStagger } from '$lib/gsap/actions';
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="relative border-b border-b-bg-h bg-bg-e">
|
||||
<section
|
||||
class="z-10 flex h-screen flex-col items-start justify-end py-16
|
||||
sm:py-20"
|
||||
>
|
||||
<h1
|
||||
use:scrollRevealLeft={{ start: 'top 95%' }}
|
||||
class="font-display text-[clamp(2.5rem,10vw,8rem)] leading-none font-black uppercase"
|
||||
>
|
||||
О проекте
|
||||
</h1>
|
||||
<p
|
||||
use:scrollFadeUp={{ y: 20, delay: 0.1, start: 'top 95%' }}
|
||||
class="mt-4 max-w-xs text-xl leading-[1.2] text-desc sm:max-w-140 sm:text-2xl"
|
||||
>
|
||||
Публичная витрина результатов ML-модели, обученной на исторических рыночных данных.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<span
|
||||
class="absolute -top-20 -right-20 z-0 h-80 w-full rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<section class="grid max-h-full! grid-cols-1 lg:grid-cols-[1fr_420px]">
|
||||
<div class="border-b border-b-bg-h lg:border-r lg:border-b-0 lg:border-r-bg-h">
|
||||
{#each about as item (item.id)}
|
||||
{#if item.type === 'text'}
|
||||
<div
|
||||
use:scrollFadeUp={{ y: 28 }}
|
||||
class="flex items-start gap-5 border-b border-b-bg-h px-5 py-12
|
||||
sm:gap-10 sm:px-10 sm:py-24"
|
||||
>
|
||||
<span
|
||||
class="shrink-0 font-display text-[clamp(3rem,8vw,6rem)] leading-[0.8] font-black text-primary
|
||||
opacity-20"
|
||||
>
|
||||
0{item.id}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-col gap-4 sm:gap-6">
|
||||
<p class="font-mono text-xs tracking-widest text-desc uppercase">{item.desc}</p>
|
||||
<h2 class="font-display text-xl leading-none font-black sm:text-[2rem]">
|
||||
{item.title}
|
||||
</h2>
|
||||
{#each item.text as paragraph, i (i)}
|
||||
<p class="text-base leading-[1.4] text-desc sm:text-lg sm:leading-[1.3]">
|
||||
{paragraph}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- padding на родителе — не margin/w-full на самом блоке -->
|
||||
<div class="border-b border-b-bg-h px-5 py-12 sm:px-10 sm:py-16">
|
||||
<div
|
||||
class="flex h-[50vh] w-full items-center justify-center rounded-2xl
|
||||
border border-white/6 bg-bg-e"
|
||||
>
|
||||
<p class="font-mono text-xs tracking-widest text-desc/40 uppercase">
|
||||
Дашборд — скриншот
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="px-5 py-12 sm:px-8 sm:py-24">
|
||||
<div class="overflow-hidden rounded-xl border border-white/6 bg-bg-e">
|
||||
<p class="px-5 py-3 pt-5 text-center font-mono text-sm tracking-widest text-desc uppercase">
|
||||
Параметры модели
|
||||
</p>
|
||||
<div class="divide-y divide-white/6">
|
||||
{#each params as p (p.label)}
|
||||
<div class="flex items-center justify-between px-5 py-3">
|
||||
<span class="text-sm text-desc">{p.label}</span>
|
||||
<span class="font-mono text-sm text-primary">{p.value}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4 rounded-xl border border-primary/20 bg-primary/5 px-5 py-6">
|
||||
<p
|
||||
class="mb-2 flex items-center gap-2 font-display text-xs font-bold tracking-widest text-primary uppercase"
|
||||
>
|
||||
⚠ Дисклеймер
|
||||
</p>
|
||||
<p class="text-sm leading-normal text-desc">
|
||||
Материалы являются результатом работы исследовательской ML-модели и используются в
|
||||
демонстрационных целях. Не являются индивидуальной инвестиционной рекомендацией.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button href="/dashboard" class="w-full">Открыть дашборд</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -0,0 +1,55 @@
|
||||
type AboutText = {
|
||||
id: number;
|
||||
type: 'text';
|
||||
desc: string;
|
||||
title: string;
|
||||
text: string[];
|
||||
};
|
||||
type AboutImage = {
|
||||
id: number;
|
||||
type: 'image';
|
||||
src: string;
|
||||
};
|
||||
type About = AboutText | AboutImage;
|
||||
|
||||
export const about: About[] = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'text',
|
||||
desc: 'Назначение',
|
||||
title: 'Исследовательская платформа',
|
||||
text: [
|
||||
'Flamy AI — публичная витрина результатов ML-модели, обученной на исторических рыночных данных. Показывает прогнозы по крипто-инструментам: направление, уровни входа, Take Profit и Stop Loss.',
|
||||
'Платформа создана для изучения поведения ML-модели в рыночных условиях — не для торговли реальными средствами.'
|
||||
]
|
||||
},
|
||||
{ id: 0, type: 'image', src: '' },
|
||||
{
|
||||
id: 2,
|
||||
type: 'text',
|
||||
desc: 'Как работает',
|
||||
title: 'Модель → Публикация → График',
|
||||
text: [
|
||||
'Каждый час модель обрабатывает последние свечи по каждому инструменту и публикует прогноз: направление (bullish / bearish / flat), уверенность в процентах, прогнозные OHLC-свечи на горизонт 19 периодов.',
|
||||
'Если режим публикации TRADE_PLAN, модель также рассчитывает уровни Entry, TP и SL.'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'text',
|
||||
desc: 'Разработка',
|
||||
title: 'Сделано в Flamy Studio',
|
||||
text: [
|
||||
'Платформа разработана командой Flamy Studio — студии веб-разработки, специализирующейся на современных продуктах.'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const params = [
|
||||
{ label: 'Горизонт', value: '19 свечей' },
|
||||
{ label: 'Таймфрейм', value: '5 минут' },
|
||||
{ label: 'История', value: '20 дней' },
|
||||
{ label: 'Обновление', value: 'Каждый час' },
|
||||
{ label: 'Стек', value: 'SvelteKit' },
|
||||
{ label: 'Графики', value: 'lwc' }
|
||||
];
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { posts } from '$lib/blog/posts';
|
||||
import { scrollRevealLeft, scrollFadeUp, scrollStagger } from '$lib/gsap/actions';
|
||||
|
||||
function fmtDate(d: string) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat('ru', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}).format(new Date(d));
|
||||
} catch {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="relative overflow-hidden border-b border-bg-h bg-bg-e">
|
||||
<section class="z-10 flex flex-col items-start justify-end px-5 py-16 sm:px-10 sm:py-20">
|
||||
<h1
|
||||
use:scrollRevealLeft={{ start: 'top 95%' }}
|
||||
class="font-display text-[clamp(2.5rem,10vw,8rem)] font-black uppercase leading-none"
|
||||
>
|
||||
Блог
|
||||
</h1>
|
||||
<p
|
||||
use:scrollFadeUp={{ y: 20, delay: 0.1, start: 'top 95%' }}
|
||||
class="mt-4 max-w-xs text-xl leading-[1.2] text-desc sm:max-w-140 sm:text-2xl"
|
||||
>
|
||||
Гайды по чтению графиков, управлению рисками и работе с ML-прогнозами.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<span
|
||||
class="absolute -right-20 -bottom-40 z-0 h-80 w-full -rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:scrollStagger={{ selector: 'a', stagger: 0.08, y: 24, start: 'top 88%' }}
|
||||
class="mx-auto max-w-400 px-5 sm:px-10 lg:px-0 mb-32"
|
||||
>
|
||||
{#if posts.length === 0}
|
||||
<div class="flex flex-col items-center gap-3 py-20 text-center">
|
||||
<p class="font-mono text-sm text-desc">Статьи пока не опубликованы.</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each posts as post, i (post.id)}
|
||||
<a
|
||||
href="/blog/{post.slug}"
|
||||
class="group flex items-start gap-4 border-b border-bg-h py-7 no-underline transition-colors duration-150 hover:bg-bg-e sm:gap-6 sm:py-9
|
||||
{i === 0 ? 'border-t border-bg-h' : ''}"
|
||||
>
|
||||
<span
|
||||
class="w-8 shrink-0 pt-1 text-right font-mono text-[0.5625rem] tracking-widest text-desc/50"
|
||||
>
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="relative hidden aspect-video w-40 shrink-0 overflow-hidden rounded-xl border border-bg-h bg-bg-e sm:block md:w-92"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-linear-to-br from-primary/10 via-transparent to-transparent"
|
||||
></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<h2
|
||||
class="font-display text-[clamp(1.125rem,2.5vw,1.625rem)] font-black leading-[1.15] tracking-tight text-title transition-colors duration-150 group-hover:text-primary"
|
||||
>
|
||||
{post.title}
|
||||
</h2>
|
||||
<p class="line-clamp-2 text-[0.9375rem] leading-[1.6] text-desc">
|
||||
{post.description}
|
||||
</p>
|
||||
{#if post.tags.length > 0}
|
||||
<div class="mt-1 flex flex-wrap gap-1.5">
|
||||
{#each post.tags.slice(0, 3) as tag, i (i)}
|
||||
<span
|
||||
class="rounded-sm border border-white/6 bg-white/3 px-1.75 py-0.5 font-mono text-xs uppercase tracking-widest text-desc/60"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<time
|
||||
datetime={post.date}
|
||||
class="mt-1 font-mono text-xs tracking-[0.06em] text-desc/50"
|
||||
>
|
||||
{fmtDate(post.date)}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="shrink-0 pt-1 text-desc/30 transition-[color,translate] duration-700 group-hover:translate-x-2 group-hover:text-primary mr-6"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M4 10 H16 M12 6 L16 10 L12 14"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,200 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { post } = $derived(data);
|
||||
|
||||
function fmtDate(d: string) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat('ru', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}).format(new Date(d));
|
||||
} catch {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="mx-auto max-w-3xl px-5 pb-24 pt-16 sm:px-10 sm:pb-32 sm:pt-20 lg:px-0 mt-10">
|
||||
<a
|
||||
href="/blog"
|
||||
class="back-link mb-6 inline-flex items-center gap-1.5 font-mono text-[0.625rem] uppercase tracking-[0.08em] transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M11 7 H3 M6 4 L3 7 L6 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Блог
|
||||
</a>
|
||||
|
||||
<header class="mb-12 border-b border-bg-h pb-10">
|
||||
{#if post.tags.length > 0}
|
||||
<div class="mb-5 flex flex-wrap gap-1.5">
|
||||
{#each post.tags as tag (tag)}
|
||||
<span
|
||||
class="rounded-sm border border-primary/25 bg-primary/10 px-1.75 py-0.5 font-mono text-xs uppercase tracking-widest text-primary"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h1
|
||||
class="mb-5 font-display text-[clamp(1.875rem,5vw,2.75rem)] font-black leading-[1.08] tracking-[-0.035em] text-title"
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<p class="mb-6 text-lg leading-[1.65] text-desc">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
<time datetime={post.date} class="font-mono text-[0.625rem] uppercase tracking-[0.08em] text-desc/50">
|
||||
{fmtDate(post.date)}
|
||||
</time>
|
||||
</header>
|
||||
|
||||
<article class="prose-article">
|
||||
{@html post.content}
|
||||
</article>
|
||||
|
||||
<div class="mt-16 flex items-center justify-between gap-3 border-t border-bg-h pt-7">
|
||||
<a
|
||||
href="/blog"
|
||||
class="back-link inline-flex items-center gap-1.5 font-mono text-[0.625rem] uppercase tracking-[0.08em] transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M11 7 H3 M6 4 L3 7 L6 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Все статьи
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-primary px-5 py-3 font-display text-sm font-normal transition-colors hover:bg-primary-h"
|
||||
>
|
||||
Открыть дашборд
|
||||
<img src="/images/icons/arrow-right.svg" alt="" class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.back-link {
|
||||
color: var(--color-desc);
|
||||
opacity: 0.5;
|
||||
text-decoration: none;
|
||||
}
|
||||
.back-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.prose-article :global(h2) {
|
||||
font-family: var(--font-display),sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-title);
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.2;
|
||||
margin-top: 2.25em;
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.prose-article :global(h3) {
|
||||
font-family: var(--font-display), sans-serif;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-title);
|
||||
letter-spacing: -0.02em;
|
||||
margin-top: 1.75em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.prose-article :global(p) {
|
||||
color: var(--color-desc);
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose-article :global(strong) {
|
||||
color: var(--color-title);
|
||||
font-weight: 600;
|
||||
}
|
||||
.prose-article :global(a) {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent);
|
||||
transition: opacity 130ms;
|
||||
}
|
||||
.prose-article :global(a:hover) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.prose-article :global(ul),
|
||||
.prose-article :global(ol) {
|
||||
color: var(--color-desc);
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
padding-left: 1.5em;
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.prose-article :global(li) {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.prose-article :global(li strong) {
|
||||
color: var(--color-title);
|
||||
}
|
||||
.prose-article :global(code) {
|
||||
font-family: var(--font-mono),sans-serif;
|
||||
font-size: 0.875em;
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.prose-article :global(pre) {
|
||||
background: var(--color-bg-e);
|
||||
border: 1px solid var(--color-bg-h);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
margin: 1.75em 0;
|
||||
}
|
||||
.prose-article :global(pre code) {
|
||||
background: none;
|
||||
color: var(--color-desc);
|
||||
padding: 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.prose-article :global(blockquote) {
|
||||
border-left: 3px solid var(--color-primary);
|
||||
padding: 4px 0 4px 20px;
|
||||
margin: 1.75em 0;
|
||||
color: var(--color-desc);
|
||||
font-style: italic;
|
||||
}
|
||||
.prose-article :global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-bg-h);
|
||||
margin: 2.5em 0;
|
||||
}
|
||||
.prose-article :global(img) {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-bg-h);
|
||||
margin: 1.75em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getPostBySlug } from '$lib/blog/posts';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = ({ params }) => {
|
||||
const post = getPostBySlug(params.slug);
|
||||
if (!post) throw error(404, 'Статья не найдена');
|
||||
return { post };
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { gsap } from 'gsap';
|
||||
import { generateChartData, SYMBOLS, TIMEFRAMES, type TimeframeId } from '$lib/stores/chartStore';
|
||||
import Chart from '$lib/components/Chart.svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
let activeSymbol = $state('BTCUSDT');
|
||||
let activeTimeframe = $state<TimeframeId>('5m');
|
||||
let chartHeight = $state(520);
|
||||
|
||||
const sym = $derived(SYMBOLS.find((s) => s.id === activeSymbol) ?? SYMBOLS[0]);
|
||||
const data = $derived(generateChartData(activeSymbol, activeTimeframe));
|
||||
|
||||
let titleEl = $state<HTMLElement>(null!);
|
||||
let panelEl = $state<HTMLElement>(null!);
|
||||
|
||||
onMount(() => {
|
||||
const update = () => {
|
||||
chartHeight = window.innerWidth < 640 ? 310 : window.innerWidth < 1024 ? 450 : 590;
|
||||
};
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
|
||||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.from(titleEl, { opacity: 0, y: 20, duration: 0.6, ease: 'power3.out', clearProps: 'all' });
|
||||
gsap.from(panelEl, { opacity: 0, y: 32, duration: 0.7, delay: 0.12, ease: 'power3.out', clearProps: 'all' });
|
||||
});
|
||||
return () => { ctx.revert(); window.removeEventListener('resize', update); };
|
||||
}
|
||||
|
||||
return () => window.removeEventListener('resize', update);
|
||||
});
|
||||
|
||||
function fmtPrice(n: number): string {
|
||||
if (n >= 10000) return n.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||||
if (n >= 100) return n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
if (n >= 1) return n.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 });
|
||||
return n.toLocaleString('en-US', { minimumFractionDigits: 6, maximumFractionDigits: 6 });
|
||||
}
|
||||
|
||||
function fmtPct(n: number): string {
|
||||
return (n >= 0 ? '+' : '') + n.toFixed(2) + '%';
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="min-h-screen bg-bg">
|
||||
<div class="mx-auto max-w-400 px-5 pt-22 pb-16 sm:px-10 sm:pt-26 lg:pt-24">
|
||||
<div bind:this={titleEl} class="mb-5 mx-auto w-fit">
|
||||
<h1 class="font-display text-center text-2xl font-black uppercase leading-none tracking-tight mb-2 sm:text-4xl ">
|
||||
Дашборд
|
||||
</h1>
|
||||
<p class="mt-1.5 font-mono text-[0.625rem] tracking-widest text-desc uppercase sm:text-xs">
|
||||
ML-прогноз · Горизонт 19 свечей · Таймфрейм 5M
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div bind:this={panelEl} class="overflow-hidden rounded-2xl border border-bg-h bg-bg-c shadow-[0_40px_80px_rgba(0,0,0,0.6)]">
|
||||
<div class="flex flex-col border-b border-bg-h sm:flex-row sm:items-stretch sm:justify-between">
|
||||
|
||||
<div class="flex overflow-hidden border-b border-bg-h sm:border-b-0 [&::-webkit-scrollbar]:hidden">
|
||||
{#each SYMBOLS as s (s.id)}
|
||||
<button
|
||||
class={cn(
|
||||
'shrink-0 border-b-2 px-3.5 py-3 font-display text-[0.6875rem] font-medium tracking-tight transition-colors duration-150 sm:px-4 sm:text-xs',
|
||||
activeSymbol === s.id
|
||||
? '-mb-px border-primary text-title'
|
||||
: 'border-transparent text-desc hover:text-title'
|
||||
)}
|
||||
onclick={() => (activeSymbol = s.id)}
|
||||
>
|
||||
{s.short}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-0.5 px-3 py-2 sm:border-l sm:border-l-bg-h">
|
||||
{#each TIMEFRAMES as tf (tf.id)}
|
||||
<button
|
||||
class={cn(
|
||||
'rounded-lg px-2.5 py-1.5 font-mono text-[0.6875rem] transition-colors duration-150',
|
||||
activeTimeframe === tf.id ? 'bg-bg-h text-title' : 'text-desc hover:text-title'
|
||||
)}
|
||||
onclick={() => (activeTimeframe = tf.id)}
|
||||
>
|
||||
{tf.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-3 border-b border-bg-h px-4 py-3 sm:px-5">
|
||||
|
||||
<div class="flex items-baseline gap-2.5">
|
||||
<span class="font-display text-[0.625rem] font-black tracking-tight text-desc uppercase hidden sm:inline">
|
||||
{sym.label}
|
||||
</span>
|
||||
<span class="font-mono text-lg font-medium text-title sm:text-2xl">
|
||||
{fmtPrice(data.currentPrice)}
|
||||
</span>
|
||||
<span class={cn(
|
||||
'font-mono text-xs font-medium sm:text-sm',
|
||||
data.change24hPct >= 0 ? 'text-emerald-400' : 'text-red-400'
|
||||
)}>
|
||||
{fmtPct(data.change24hPct)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class={cn(
|
||||
'flex items-center gap-1.5 rounded-lg px-2.5 py-1.5',
|
||||
data.signal.direction === 'long' ? 'bg-emerald-400/10' : 'bg-red-400/10'
|
||||
)}>
|
||||
<span class={cn(
|
||||
'font-display text-[0.6875rem] font-black uppercase tracking-tight',
|
||||
data.signal.direction === 'long' ? 'text-emerald-400' : 'text-red-400'
|
||||
)}>
|
||||
{data.signal.direction === 'long' ? '▲ LONG' : '▼ SHORT'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase hidden sm:inline">
|
||||
Уверенность
|
||||
</span>
|
||||
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-bg-h sm:w-20">
|
||||
<div
|
||||
class={cn(
|
||||
'h-full rounded-full transition-all duration-700',
|
||||
data.signal.confidence >= 70 ? 'bg-emerald-400' : data.signal.confidence >= 55 ? 'bg-amber-400' : 'bg-red-400'
|
||||
)}
|
||||
style="width: {data.signal.confidence}%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="font-mono text-xs text-title">{data.signal.confidence}%</span>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex gap-4 sm:gap-6">
|
||||
{#each [
|
||||
{ label: 'Entry', val: data.signal.entry, cls: 'text-primary' },
|
||||
{ label: 'TP', val: data.signal.tp, cls: 'text-emerald-400' },
|
||||
{ label: 'SL', val: data.signal.sl, cls: 'text-red-400' },
|
||||
] as lvl (lvl.label)}
|
||||
<div class="flex flex-col gap-0.5 text-right">
|
||||
<span class="font-mono text-[0.5rem] tracking-widest text-desc uppercase">{lvl.label}</span>
|
||||
<span class="font-mono text-xs {lvl.cls}">{fmtPrice(lvl.val)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chart data={data} decimals={sym.dec} height={chartHeight} />
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-bg-h px-4 py-3 sm:px-5">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex gap-0.5">
|
||||
<span class="h-3.5 w-1.5 rounded-[2px] bg-emerald-400/80"></span>
|
||||
<span class="h-3.5 w-1.5 rounded-[2px] bg-red-400/80"></span>
|
||||
</div>
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase">Свечи</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg width="22" height="4" aria-hidden="true">
|
||||
<line x1="0" y1="2" x2="22" y2="2" stroke="#fe4b07" stroke-width="2" stroke-dasharray="4 3" />
|
||||
</svg>
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase">ML-прогноз</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="block h-px w-4 bg-primary"></span>
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase">Entry</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="block h-px w-4" style="border-top: 2px dashed #26a69a;"></span>
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase">TP</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="block h-px w-4" style="border-top: 2px dashed #ef5350;"></span>
|
||||
<span class="font-mono text-[0.5625rem] tracking-widest text-desc uppercase">SL</span>
|
||||
</div>
|
||||
|
||||
<span class="ml-auto font-mono text-[0.5rem] tracking-widest text-desc/60 uppercase">
|
||||
не является инвестиционной рекомендацией
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="pointer-events-none fixed top-0 left-1/2 -translate-x-1/2 h-80 w-full max-w-3xl rounded-full bg-primary opacity-[0.07] blur-[160px]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</main>
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
section {
|
||||
@apply relative mx-auto max-w-400 lg:min-h-175 lg:max-h-300;
|
||||
@apply relative mx-auto max-w-400 lg:min-h-175 lg:max-h-300 px-10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@
|
||||
|
||||
.noise {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
z-index: 50;
|
||||
background-image: url('/images/noise.png');
|
||||
background-repeat: repeat;
|
||||
opacity: 0.14;
|
||||
|
||||
Reference in New Issue
Block a user