Prepare production deployment

This commit is contained in:
2026-07-20 04:14:24 +05:00
parent 55583e2101
commit 2e3550a4dc
46 changed files with 1497 additions and 882 deletions
+86 -37
View File
@@ -1,11 +1,25 @@
export type Post = {
export type PostContentBlock =
| {
type: 'paragraph';
text: string;
}
| {
type: 'heading';
text: string;
}
| {
type: 'list';
items: string[];
};
export type Post = {
id: number;
slug: string;
date: string;
title: string;
description: string;
tags: string[];
content: string;
content: PostContentBlock[];
};
export const posts: Post[] = [
@@ -17,50 +31,85 @@ export const posts: Post[] = [
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>
`
content: [
{
type: 'paragraph',
text: 'Управление рисками — фундамент любой торговой стратегии. Без него даже точный ML-прогноз не поможет сохранить капитал.'
},
{ type: 'heading', text: 'Stop Loss: где ваша позиция неправа' },
{
type: 'paragraph',
text: 'Stop Loss — уровень, при достижении которого позиция закрывается автоматически. Это не признание ошибки, а часть стратегии. На графиках Flamy AI уровень SL рассчитывается моделью на основе исторической волатильности инструмента.'
},
{ type: 'heading', text: 'Take Profit: когда забирать прибыль' },
{
type: 'paragraph',
text: 'Take Profit — целевой уровень закрытия позиции с прибылью. TP рассчитывается пропорционально прогнозному движению с учётом Risk:Reward ratio.'
},
{ type: 'heading', text: 'Risk:Reward Ratio' },
{
type: 'paragraph',
text: 'Оптимальное соотношение риска к доходности — не менее 1:2. Это означает, что потенциальная прибыль должна минимум вдвое превышать риск.'
},
{
type: 'list',
items: [
'SL: не более 1% от капитала на сделку',
'TP: 2% и выше от точки входа',
'Одна сделка: не более 2% депозита'
]
},
{ type: 'heading', text: 'Почему это важно при работе с ML-прогнозами' },
{
type: 'paragraph',
text: 'ML-модель не даёт гарантий. Уверенность 80% означает, что 20% прогнозов будут неточными. Управление рисками — страховка на эти 20%.'
}
]
},
{
id: 2,
slug: 'kak-chitat-grafiki-flamy-ai',
date: '2026-05-17',
title: 'Как читать графики Flamy AI',
description:
'Краткое объяснение свечей, прогнозной зоны, TP и SL на графиках 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>
`
content: [
{
type: 'paragraph',
text: 'Графики Flamy AI содержат три слоя: исторические свечи, прогнозную зону и уровни торгового плана. Разберём каждый из них.'
},
{ type: 'heading', text: 'Исторические свечи' },
{
type: 'paragraph',
text: 'Левая часть графика — реальные OHLC-свечи. Зелёные — бычьи, красные — медвежьи. Это основа для анализа модели.'
},
{ type: 'heading', text: 'Прогнозная зона' },
{
type: 'paragraph',
text: 'Правая часть — прогнозные свечи на горизонт 19 периодов вперёд. Это не точные значения, а вероятностный коридор движения цены на ближайшие 95 минут.'
},
{ type: 'heading', text: 'Уровни Entry, TP и SL' },
{
type: 'paragraph',
text: 'Если модель публикует режим TRADE_PLAN, на графике появляются три горизонтальные линии.'
},
{
type: 'list',
items: [
'Entry — рекомендуемый уровень входа в позицию',
'TP — цель Take Profit',
'SL — Stop Loss для ограничения убытков'
]
},
{ type: 'heading', text: 'Таймфрейм и горизонт' },
{
type: 'paragraph',
text: 'Все графики работают на таймфрейме 5 минут. Горизонт прогноза — 19 свечей = 95 минут вперёд. Прогнозы обновляются каждый час автоматически.'
}
]
}
];
export function getPostBySlug(slug: string): Post | undefined {
return posts.find((p) => p.slug === slug);
}
}
+32 -27
View File
@@ -9,7 +9,7 @@
type IChartApi,
type ISeriesApi,
type MouseEventParams,
type UTCTimestamp,
type UTCTimestamp
} from 'lightweight-charts';
import type { ChartData, CandleBar } from '$lib/stores/chartStore';
@@ -50,7 +50,7 @@
high: 0,
low: 0,
close: 0,
isUp: true,
isUp: true
});
function fmt(n: number): string {
@@ -75,7 +75,7 @@
lineWidth: 1,
lineStyle: LineStyle.Solid,
axisLabelVisible: true,
title: 'Entry',
title: 'Entry'
}),
candleSeries.createPriceLine({
price: d.signal.tp,
@@ -83,7 +83,7 @@
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'TP',
title: 'TP'
}),
candleSeries.createPriceLine({
price: d.signal.sl,
@@ -91,8 +91,8 @@
lineWidth: 1,
lineStyle: LineStyle.Dashed,
axisLabelVisible: true,
title: 'SL',
}),
title: 'SL'
})
];
chart.timeScale().fitContent();
@@ -107,11 +107,11 @@
textColor: '#8a887f',
fontFamily: "'JetBrains Mono', monospace",
fontSize: 11,
attributionLogo: false,
attributionLogo: false
},
grid: {
vertLines: { color: '#1a191e' },
horzLines: { color: '#1a191e' },
horzLines: { color: '#1a191e' }
},
crosshair: {
mode: CrosshairMode.Normal,
@@ -119,25 +119,25 @@
color: '#3d3b42',
labelBackgroundColor: '#1a191e',
style: LineStyle.Dashed,
width: 1,
width: 1
},
horzLine: {
color: '#3d3b42',
labelBackgroundColor: '#1a191e',
style: LineStyle.Dashed,
width: 1,
},
width: 1
}
},
rightPriceScale: {
borderColor: '#1a191e',
scaleMargins: { top: 0.06, bottom: 0.04 },
scaleMargins: { top: 0.06, bottom: 0.04 }
},
timeScale: {
borderColor: '#1a191e',
timeVisible: true,
secondsVisible: false,
rightOffset: 24,
},
rightOffset: 24
}
});
candleSeries = chart.addSeries(CandlestickSeries, {
@@ -146,7 +146,7 @@
borderUpColor: '#26a69a',
borderDownColor: '#ef5350',
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
wickDownColor: '#ef5350'
});
predSeries = chart.addSeries(LineSeries, {
@@ -158,7 +158,7 @@
crosshairMarkerBorderColor: '#fe4b07',
crosshairMarkerBackgroundColor: '#09080a',
priceLineVisible: false,
lastValueVisible: true,
lastValueVisible: true
});
applyData(data);
@@ -169,17 +169,22 @@
return;
}
const c = param.seriesData.get(candleSeries) as CandleBar;
if (!c) { tip = { ...tip, visible: false }; return; }
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 pred =
predSeries && param.seriesData.has(predSeries)
? (param.seriesData.get(predSeries) as { value?: number })?.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' });
const timeStr =
d.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }) +
' ' +
d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
tip = {
visible: true,
@@ -191,14 +196,14 @@
low: c.low,
close: c.close,
isUp: c.close >= c.open,
pred,
pred
};
});
const ro = new ResizeObserver(() => {
chart?.applyOptions({
width: container.clientWidth,
height: container.clientHeight,
height: container.clientHeight
});
});
ro.observe(container);
@@ -229,7 +234,7 @@
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]}
{#each [['O', tip.open], ['H', tip.high], ['L', tip.low], ['C', tip.close]] as [lbl, val] (lbl)}
<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>
@@ -237,7 +242,7 @@
{/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-[0.5625rem] tracking-widest text-primary uppercase">ML</span>
<span class="text-primary">{fmt(tip.pred)}</span>
</div>
{/if}
@@ -62,7 +62,7 @@
}
</script>
<div class="w-full max-w-170 z-20 mx-auto xl:mx-0" style="perspective: 1000px;">
<div class="z-20 mx-auto w-full max-w-170 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;"
@@ -55,7 +55,7 @@
};
</script>
<div class="hidden h-full w-full max-w-140 items-center justify-end lg:flex z-20">
<div class="z-20 hidden h-full w-full max-w-140 items-center justify-end lg:flex">
<div
class="w-full overflow-hidden rounded-2xl border border-white/8 bg-zinc-950 shadow-[0_0_0_1px_rgba(255,255,255,0.03),0_40px_80px_rgba(0,0,0,0.5),0_0_80px_rgba(255,69,0,0.05)]"
>
@@ -81,9 +81,7 @@
class="grid grid-cols-[1fr_56px_76px] gap-2.5 border-b border-white/6 bg-white/1 px-4 py-3.5"
>
<span class="font-mono text-[0.55rem] tracking-widest text-zinc-600">ИНСТРУМЕНТ</span>
<span class="text-right font-mono text-[0.55rem] tracking-widest text-zinc-600"
>КОНФИД.</span
>
<span class="text-right font-mono text-[0.55rem] tracking-widest text-zinc-600">КОНФИД.</span>
<span class="pl-2 font-mono text-[0.55rem] tracking-widest text-zinc-600">ПРОГНОЗ</span>
</div>
+3 -3
View File
@@ -12,7 +12,6 @@
<footer class="mt-auto border-t border-white/6 bg-bg-e">
<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 lg:col-span-1">
<Logo />
@@ -23,7 +22,9 @@
</div>
<div class="flex flex-col gap-1">
<span class="font-mono text-base tracking-widest text-zinc-600 uppercase mb-1">Навигация</span>
<span class="mb-1 font-mono text-base tracking-widest text-zinc-600 uppercase"
>Навигация</span
>
{#each navLinks as link (link.id)}
<a
href={link.href}
@@ -59,6 +60,5 @@
</a>
</span>
</div>
</div>
</footer>
+3 -3
View File
@@ -21,12 +21,12 @@
opacity: 0,
duration: 0.5,
ease: 'power2.out',
clearProps: 'transform,opacity',
clearProps: 'transform,opacity'
});
});
</script>
<header bind:this={header} class="fixed right-0 left-0 border-b border-b-bg-h bg-bg-e z-9999">
<header bind:this={header} class="fixed right-0 left-0 z-9999 border-b border-b-bg-h bg-bg-e">
<div class="mx-auto flex max-w-400 items-center justify-between px-5 py-5 sm:px-10 lg:px-5">
<Logo />
@@ -60,7 +60,7 @@
<span
class={cn(
'block h-0.5 w-6 bg-title transition-all duration-200',
mobileOpen && 'opacity-0 scale-x-0'
mobileOpen && 'scale-x-0 opacity-0'
)}
></span>
<span
+2 -2
View File
@@ -19,9 +19,9 @@
{target}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
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',
'flex w-fit items-center justify-center gap-2 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' && 'border bg-bg hover:text-desc',
className
)}
>
+3 -3
View File
@@ -1,9 +1,9 @@
<script lang="ts">
</script>
<a href="/" class="flex items-center gap-2.5 no-underline max-w-fit">
<img src="images/icons/logo.svg" alt="Логотип" class="w-8 h-8" />
<a href="/" class="flex max-w-fit items-center gap-2.5 no-underline">
<img src="images/icons/logo.svg" alt="Логотип" class="h-8 w-8" />
<span class="font-display text-xl font-black tracking-tighter">
FLAMY<span class="text-primary">TRADE</span>
</span>
</a>
</a>
+16
View File
@@ -0,0 +1,16 @@
import { env } from '$env/dynamic/public';
export const site = {
name: env.PUBLIC_SITE_NAME || 'Flamy Trade',
url: (env.PUBLIC_SITE_URL || 'https://trade.flamy.studio').replace(/\/$/, ''),
description: env.PUBLIC_SITE_DESCRIPTION || 'Публичная витрина ML-прогнозов для крипторынка.'
};
export function pageTitle(title?: string): string {
return title ? `${title} | ${site.name}` : site.name;
}
export function canonical(path = '/'): string {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${site.url}${normalizedPath}`;
}
+5 -5
View File
@@ -67,7 +67,7 @@ export function scrollFadeUp(el: HTMLElement, params: FadeUpParams = {}) {
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
scrollTrigger: { trigger: el, start, once: true }
});
});
@@ -93,7 +93,7 @@ export function scrollRevealLeft(el: HTMLElement, params: RevealLeftParams = {})
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
scrollTrigger: { trigger: el, start, once: true }
});
});
@@ -115,7 +115,7 @@ export function scrollStagger(el: HTMLElement, params: StaggerParams = {}) {
duration = 0.65,
stagger = 0.1,
start = 'top 80%',
selector = ':scope > *',
selector = ':scope > *'
} = params;
const targets = el.querySelectorAll<HTMLElement>(selector);
@@ -133,7 +133,7 @@ export function scrollStagger(el: HTMLElement, params: StaggerParams = {}) {
ease: 'power3.out',
stagger: { each: stagger, ease: 'power1.inOut' },
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
scrollTrigger: { trigger: el, start, once: true }
});
});
@@ -160,7 +160,7 @@ export function scrollScaleIn(el: HTMLElement, params: { delay?: number; start?:
delay,
ease: 'power3.out',
clearProps: 'transform,opacity',
scrollTrigger: { trigger: el, start, once: true },
scrollTrigger: { trigger: el, start, once: true }
});
});
+18 -11
View File
@@ -39,7 +39,7 @@ export const SYMBOLS = [
{ 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 },
{ id: 'LINKUSDT', label: 'LINK/USDT', short: 'LINK', base: 18.5, dec: 3 }
] as const;
export const TIMEFRAMES = [
@@ -48,7 +48,7 @@ export const TIMEFRAMES = [
{ 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 },
{ id: '1d' as TimeframeId, label: '1д', sec: 86400 }
];
// Simple LCG PRNG — deterministic per seed so switching back gives the same chart
@@ -91,7 +91,13 @@ export function generateChartData(symbolId: string, tfId: TimeframeId): ChartDat
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) });
candles.push({
time: startTime + i * tf.sec,
open: round(open),
high: round(high),
low: round(low),
close: round(close)
});
price = close;
}
@@ -107,15 +113,12 @@ export function generateChartData(symbolId: string, tfId: TimeframeId): ChartDat
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 },
];
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 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)) });
}
@@ -127,8 +130,12 @@ export function generateChartData(symbolId: string, tfId: TimeframeId): ChartDat
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);
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,
@@ -136,6 +143,6 @@ export function generateChartData(symbolId: string, tfId: TimeframeId): ChartDat
signal: { direction, confidence, entry, tp, sl },
currentPrice,
change24h,
change24hPct,
change24hPct
};
}