Переглянути джерело

FEATURE DONT FINISHE: implement the new trading screen

gdias 1 місяць тому
батько
коміт
94f0aea5a5

+ 41 - 12
src/lib/components/trading/GraficoCandlestick.svelte

@@ -4,17 +4,25 @@
 
   export let dadosMock = [];
 
-  const xScale = scaleBand().paddingInner(0.2);
+  const xScale = scaleBand().paddingInner(0.5);
 
   $: dataSeries = (dadosMock || [])
-    .map((d) => ({ date: d.date || d.time, valor: Number(d.valor) }))
+    .map((d) => ({
+      date: d.date || d.time,
+      valor: Number(d.valor),
+      high: d.high != null ? Number(d.high) : undefined,
+      low: d.low != null ? Number(d.low) : undefined
+    })) 
     .filter((d) => d.date && Number.isFinite(d.valor));
 
   $: ohlc = dataSeries.map((d, i) => {
     const open = i > 0 ? dataSeries[i - 1].valor : d.valor;
     const close = d.valor;
-    const high = Math.max(open, close);
-    const low = Math.min(open, close);
+    const providedHigh = Number.isFinite(d.high) ? d.high : undefined;
+    const providedLow = Number.isFinite(d.low) ? d.low : undefined;
+    // Se vierem no mock, usa; senão faz fallback mínimo seguro
+    const high = providedHigh !== undefined ? Math.max(providedHigh, open, close) : Math.max(open, close);
+    const low = providedLow !== undefined ? Math.min(providedLow, open, close) : Math.min(open, close);
     return { date: d.date, open, high, low, close };
   });
 
@@ -32,29 +40,50 @@
   function fmtNum(n) {
     return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(n ?? 0));
   }
+  function fmtTickY(v) {
+    const raw = typeof v === 'object' && v !== null && 'value' in v ? v.value : v;
+    const n = Number(raw);
+    if (!Number.isFinite(n)) return '';
+    return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);
+  }
+  // domínio Y: 0 até (max atual + 5%)
+  $: yMaxRaw = (ohlc && ohlc.length)
+    ? Math.max(...ohlc.map((d) => Math.max(Number(d.high ?? d.open ?? d.close ?? 0), Number(d.open ?? 0), Number(d.close ?? 0))))
+    : 0;
+  $: yDomain = [0, (yMaxRaw > 0 ? yMaxRaw * 1.05 : 1)];
+
   const colorKey = (d) => (Number(d.close) < Number(d.open) ? 'desc' : 'asc');
 </script>
 
-<div class="h-[300px] p-4 border rounded bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 cursor-crosshair select-none">
+<div class="h-[550px] p-4 border rounded bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 cursor-crosshair select-none">
   <Chart
     data={ohlc}
     x="date"
     xScale={xScale}
     y={["high", "low"]}
+    yDomain={yDomain}
     yNice
     c={colorKey}
     cScale={scaleOrdinal()}
     cDomain={["desc", "asc"]}
     cRange={["#e41a1c", "#16a34a"]}
-    padding={{ left: 16, bottom: 36 }}
+    padding={{ left: 20, bottom: 26 }}
     tooltip={tooltip}
   >
-    <Svg>
-      <Axis placement="left" grid rule ticks={5} />
-      <Axis placement="bottom" rule ticks={Math.min(6, ohlc.length)} format={fmtDate} />
-      <Highlight rule stroke="#94a3b8" strokeWidth={1} strokeDasharray="3 3" />
-      <Points links r={0} c={colorKey} strokeWidth={1.5} strokeLinecap="round" strokeOpacity={0.7} />
-      <Bars y={(d) => [Number(d.open), Number(d.close)]} radius={4} c={colorKey} stroke="rgba(255,255,255,0.12)" fillOpacity={0.8} />
+    <Svg clip={false}>
+      <Axis
+  placement="left"
+  grid
+  rule
+  ticks={6}
+  format={fmtTickY}
+  tickPadding={10}
+  labelPadding={12}
+  class="text-xs fill-black dark:fill-white stroke-gray-400/50"
+  style="font-family: sans-serif;"
+/><Axis placement="bottom" rule ticks={Math.min(8, ohlc.length)} format={fmtDate} class="text-black dark:text-white fill-black dark:fill-white text-xs" />
+      <Points links r={0} c={colorKey} strokeWidth={1} strokeLinecap="round" strokeOpacity={0.8} />
+      <Bars y={(d) => [Number(d.open), Number(d.close)]} radius={0} c={colorKey} stroke="transparent" fillOpacity={1} />
     </Svg>
 
     <Tooltip.Root let:data>

+ 2 - 2
src/lib/components/trading/ModalBase.svelte

@@ -10,12 +10,12 @@
 {#if visible}
 <div class="fixed inset-0 z-50 flex items-center justify-center">
   <div class="absolute inset-0 bg-black/50" on:click={overlayClick}></div>
-  <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-lg mx-4">
+  <div class="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-lg mx-4 max-h-[80vh] flex flex-col">
     <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
       <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{title}</h3>
       <button class="p-1 rounded text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" on:click={close}>✕</button>
     </div>
-    <div class="p-4">
+    <div class="p-4 flex-1 overflow-auto">
       <slot />
     </div>
   </div>

+ 74 - 9
src/lib/components/trading/TabelaOrdens.svelte

@@ -1,24 +1,59 @@
 <script>
-  import { ordensCompra, ordensVenda, ultimaVenda } from '$lib/mock/ordens.js';
+  export let ordensCompra = [];
+  export let ordensVenda = [];
+  export let ultimaVenda = { valor: 0, quantidade: 0 };
   function formatBRL(n) { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(n || 0)); }
   function formatQty(n) { return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(Number(n || 0)); }
+
+  $: listaCompra = (ordensCompra || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0) }));
+  $: listaCompraView = (listaCompra || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
+  $: listaVenda = (ordensVenda || []).map((d) => ({ valor: Number(d?.valor ?? 0), quantidade: Number(d?.quantidade ?? 0) }));
+  $: listaVendaView = (listaVenda || []).map((o, __idx) => ({ ...o, __idx })).slice().reverse();
+  // `ultimaVenda` agora vem por prop do pai
+
+  let flashCompra = new Set();
+  let flashVenda = new Set();
+  let prevCompraLen = listaCompra?.length || 0;
+  let prevVendaLen = listaVenda?.length || 0;
+
+  $: {
+    const curr = listaCompra?.length || 0;
+    if (curr > prevCompraLen) {
+      for (let i = prevCompraLen; i < curr; i++) {
+        flashCompra.add(i);
+        setTimeout(() => { flashCompra.delete(i); }, 1500);
+      }
+    }
+    prevCompraLen = curr;
+  }
+
+  $: {
+    const curr = listaVenda?.length || 0;
+    if (curr > prevVendaLen) {
+      for (let i = prevVendaLen; i < curr; i++) {
+        flashVenda.add(i);
+        setTimeout(() => { flashVenda.delete(i); }, 1500);
+      }
+    }
+    prevVendaLen = curr;
+  }
 </script>
-<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
+<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm h-full">
   <div class="px-4 py-2 bg-yellow-100 dark:bg-yellow-900/40 border-b border-yellow-300 dark:border-yellow-700 text-sm text-yellow-900 dark:text-yellow-200">
     <div class="flex items-center justify-between">
       <span>Última Venda</span>
       <span class="font-medium">{formatBRL(ultimaVenda?.valor)} · {formatQty(ultimaVenda?.quantidade)}</span>
     </div>
   </div>
-  <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700">
+  <div class="h-[70vh] overflow-auto grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700 scroll-ordens">
     <div class="p-4">
-      <div class="flex items-center justify-between mb-2">
+      <div class="flex flex-col items-center justify-between mb-2">
         <h3 class="text-sm font-semibold text-green-600">Ordem de Compra</h3>
         <div class="text-xs text-gray-500">VALOR · QUANTIDADE</div>
       </div>
       <div class="space-y-1">
-        {#each ordensCompra as o}
-        <div class="flex items-center justify-between text-sm">
+        {#each listaCompraView as o}
+        <div class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 ${flashCompra.has(o.__idx) ? 'bg-green-100 dark:bg-green-900/30' : ''}`}>
           <span class="text-green-600">{formatBRL(o.valor)}</span>
           <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
         </div>
@@ -26,13 +61,13 @@
       </div>
     </div>
     <div class="p-4">
-      <div class="flex items-center justify-between mb-2">
+      <div class="flex flex-col items-center justify-between mb-2">
         <h3 class="text-sm font-semibold text-red-600">Ordem de Venda</h3>
         <div class="text-xs text-gray-500">VALOR · QUANTIDADE</div>
       </div>
       <div class="space-y-1">
-        {#each ordensVenda as o}
-        <div class="flex items-center justify-between text-sm">
+        {#each listaVendaView as o}
+        <div class={`flex items-center justify-between text-sm rounded px-1 py-0.5 transition-colors duration-700 ${flashVenda.has(o.__idx) ? 'bg-red-100 dark:bg-red-900/30' : ''}`}>
           <span class="text-red-600">{formatBRL(o.valor)}</span>
           <span class="text-gray-700 dark:text-gray-200">{formatQty(o.quantidade)}</span>
         </div>
@@ -41,3 +76,33 @@
     </div>
   </div>
 </div>
+
+<style>
+  :global(.scroll-ordens) {
+    scrollbar-width: thin;                 /* Firefox */
+    scrollbar-color: #cbd5e1 #f3f4f6;      /* thumb track */
+  }
+  :global(.dark .scroll-ordens) {
+    scrollbar-color: #374151 #1f2937;      /* thumb track */
+  }
+
+  :global(.scroll-ordens::-webkit-scrollbar) {
+    width: 10px;
+    height: 10px;
+  }
+  :global(.scroll-ordens::-webkit-scrollbar-track) {
+    background: #f3f4f6;                   /* gray-100 */
+  }
+  :global(.scroll-ordens::-webkit-scrollbar-thumb) {
+    background-color: #cbd5e1;             /* slate-300 */
+    border-radius: 9999px;
+    border: 2px solid #f3f4f6;             /* match track */
+  }
+  :global(.dark .scroll-ordens::-webkit-scrollbar-track) {
+    background: #1f2937;                   /* gray-800 */
+  }
+  :global(.dark .scroll-ordens::-webkit-scrollbar-thumb) {
+    background-color: #374151;             /* gray-700 */
+    border-color: #1f2937;                 /* match dark track */
+  }
+</style>

+ 58 - 21
src/lib/mock/ordens.js

@@ -1,28 +1,65 @@
 export const ordensCompra = [
-  { valor: 9.98, quantidade: 10000 },
-  { valor: 9.99, quantidade: 20000 },
-  { valor: 10.0, quantidade: 10000 },
-  { valor: 9.98, quantidade: 10000 },
-  { valor: 9.99, quantidade: 20000 },
-  { valor: 10.0, quantidade: 10000 },
-  { valor: 9.98, quantidade: 10000 },
-  { valor: 9.99, quantidade: 20000 },
-  { valor: 10.0, quantidade: 10000 },
-  { valor: 9.98, quantidade: 10000 },
-  { valor: 9.99, quantidade: 20000 },
-  { valor: 10.0, quantidade: 10000 }
+  { valor: 109.98, quantidade: 10000 },
+  { valor: 109.99, quantidade: 20000 },
+  { valor: 110.0, quantidade: 10000 },
+
 ];
 
 export const ordensVenda = [
-  { valor: 10.01, quantidade: 10000 },
-  { valor: 10.02, quantidade: 20000 },
-  { valor: 10.03, quantidade: 10000 },
-  { valor: 10.04, quantidade: 20000 },
-  { valor: 10.05, quantidade: 10000 },
-  { valor: 10.04, quantidade: 20000 },
-  { valor: 10.05, quantidade: 10000 },
-  { valor: 10.04, quantidade: 20000 },
-  { valor: 10.05, quantidade: 10000 }
+  { valor: 110.01, quantidade: 10000 },
+  { valor: 110.02, quantidade: 20000 },
+  { valor: 110.03, quantidade: 10000 },
+  
 ];
 
 export const ultimaVenda = { valor: 10.0, quantidade: 8000 };
+
+export function startOrdensSimulator({ get, set, intervalMs = 1500, maxItems = 60, minItems = 8, qtyGenerator }) {
+  let timer = null;
+
+  const randQty = () => Math.floor(1000 + Math.random() * (10000 - 1000 + 1));
+  const pad = (n) => String(n).padStart(2, '0');
+  const fmtDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+
+  function nextItem(prevArr) {
+    const last = prevArr[prevArr.length - 1];
+    let lastDate = last?.date ? new Date(last.date) : new Date();
+    if (Number.isNaN(lastDate.getTime())) lastDate = new Date();
+    const nextDate = new Date(lastDate.getTime() + 24 * 60 * 60 * 1000);
+
+    const rangeMin = 100;
+    const rangeMax = 200;
+    const lastValor = Number(last?.valor);
+    const base = Number.isFinite(lastValor) && lastValor >= rangeMin && lastValor <= rangeMax
+      ? lastValor
+      : (rangeMin + Math.random() * (rangeMax - rangeMin));
+
+    const delta = (Math.random() - 0.5) * 5; // +/- ~2.5 ao redor do base
+    const close = Math.min(rangeMax, Math.max(rangeMin, Number((base + delta).toFixed(2))));
+    const open = base;
+    const wick = Math.random() * 1.5 + 0.2; // 0.2 .. 1.7
+    const high = Math.min(rangeMax, +(Math.max(open, close) + wick).toFixed(2));
+    const low = Math.max(rangeMin, +(Math.min(open, close) - wick).toFixed(2));
+
+    return { date: fmtDate(nextDate), valor: close, high, low, quantidade: (typeof qtyGenerator === 'function' ? qtyGenerator({ last, close }) : randQty()) };
+  }
+
+  timer = setInterval(() => {
+    const curr = Array.isArray(get?.()) ? get() : [];
+    const shouldAdd = curr.length < minItems || (Math.random() < 0.6 && curr.length < maxItems);
+
+    let next = curr.slice();
+    if (shouldAdd) {
+      next.push(nextItem(curr));
+    } else if (next.length > minItems) {
+      const idx = Math.floor(Math.random() * next.length);
+      next.splice(idx, 1);
+    }
+
+    set?.(next);
+  }, intervalMs);
+
+  return () => {
+    if (timer) clearInterval(timer);
+  };
+}

+ 65 - 13
src/routes/trading/+page.svelte

@@ -7,6 +7,7 @@
   import { writable } from 'svelte/store';
   import { onMount } from 'svelte';
   import { browser } from '$app/environment';
+  import { ordensCompra as ordensCompraMock, ordensVenda as ordensVendaMock, startOrdensSimulator } from '$lib/mock/ordens.js';
 
   const breadcrumb = [{ label: 'Início' }, { label: 'Trading', active: true }];
 
@@ -23,6 +24,27 @@
         if (saved) orders.set(JSON.parse(saved));
       } catch {}
     }
+
+    const stopSimCompra = startOrdensSimulator({
+      get: () => ordensCompra,
+      set: (next) => { ordensCompra = next; },
+      intervalMs: 1500,
+      maxItems: 60,
+      minItems: 8
+    });
+
+    const stopSimVenda = startOrdensSimulator({
+      get: () => ordensVenda,
+      set: (next) => { ordensVenda = next; },
+      intervalMs: 1500,
+      maxItems: 60,
+      minItems: 8
+    });
+
+    return () => {
+      stopSimCompra?.();
+      stopSimVenda?.();
+    };
   });
 
   function addOrder(order) {
@@ -120,19 +142,49 @@ $: displayPendingSells = pendingSells.map((o) => ({
   $: selectedCommodityObj = tokens.find((t) => t.id === selectedCommodity);
   $: selectedCommodityLabel = selectedCommodityObj?.label || selectedCommodityObj?.name;
 
+  let ordensCompra = ordensCompraMock.slice();
+  let ordensVenda = ordensVendaMock.slice();
+  $: ultimaVenda = (ordensVenda && ordensVenda.length)
+    ? { valor: Number(ordensVenda[ordensVenda.length - 1]?.valor ?? 0), quantidade: Number(ordensVenda[ordensVenda.length - 1]?.quantidade ?? 0) }
+    : { valor: 0, quantidade: 0 };
+
   const dadosMock = [
-    { date: '2025-10-01', valor: 9.98 },
-    { date: '2025-10-02', valor: 10.01 },
-    { date: '2025-10-03', valor: 9.97 },
-    { date: '2025-10-04', valor: 10.02 },
-    { date: '2025-10-05', valor: 10.00 },
-    { date: '2025-10-06', valor: 9.99 },
-    { date: '2025-10-07', valor: 10.03 },
-    { date: '2025-10-08', valor: 9.96 },
-    { date: '2025-10-09', valor: 10.05 },
-    { date: '2025-10-10', valor: 10.01 },
-    { date: '2025-10-11', valor: 10.04 },
-    { date: '2025-10-12', valor: 9.98 }
+  { date: '2025-10-01', valor: 9.98, high: 10.05, low: 9.90 },
+  { date: '2025-10-02', valor: 10.12, high: 10.25, low: 9.95 },
+  { date: '2025-10-03', valor: 9.85, high: 9.92, low: 9.60 },
+  { date: '2025-10-04', valor: 10.30, high: 10.55, low: 10.10 },
+  { date: '2025-10-05', valor: 10.05, high: 10.20, low: 9.85 },
+  { date: '2025-10-06', valor: 9.70, high: 9.85, low: 9.45 },
+  { date: '2025-10-07', valor: 10.45, high: 10.75, low: 10.25 },
+  { date: '2025-10-08', valor: 10.60, high: 10.80, low: 10.40 },
+  { date: '2025-10-09', valor: 9.55, high: 9.70, low: 9.20 },   // forte queda
+  { date: '2025-10-10', valor: 9.30, high: 9.50, low: 9.00 },  // fundo do poço
+  { date: '2025-10-11', valor: 9.85, high: 10.00, low: 9.60 }, // leve recuperação
+  { date: '2025-10-12', valor: 10.50, high: 10.80, low: 10.30 }, // disparada
+  { date: '2025-10-13', valor: 10.75, high: 11.10, low: 10.60 },
+  { date: '2025-10-14', valor: 10.95, high: 11.20, low: 10.80 }, // pico máximo
+  { date: '2025-10-15', valor: 10.40, high: 10.60, low: 10.10 },
+  { date: '2025-10-16', valor: 10.10, high: 10.25, low: 9.90 },
+  { date: '2025-10-17', valor: 9.85, high: 9.95, low: 9.60 },
+  { date: '2025-10-18', valor: 9.20, high: 9.35, low: 8.95 },   // nova queda brusca
+  { date: '2025-10-19', valor: 8.85, high: 9.00, low: 8.55 },
+  { date: '2025-10-20', valor: 9.50, high: 9.70, low: 9.20 },
+  { date: '2025-10-21', valor: 9.95, high: 10.10, low: 9.80 },
+  { date: '2025-10-22', valor: 10.25, high: 10.50, low: 10.05 },
+  { date: '2025-10-23', valor: 10.90, high: 11.15, low: 10.75 }, // novo pico
+  { date: '2025-10-24', valor: 11.40, high: 11.75, low: 11.10 },
+  { date: '2025-10-25', valor: 13.75, high: 13.95, low: 13.50 },
+  { date: '2025-10-26', valor: 13.60, high: 13.80, low: 13.40 },
+  { date: '2025-10-27', valor: 9.30, high: 9.45, low: 9.00 },  // queda acentuada
+  { date: '2025-10-28', valor: 9.10, high: 9.25, low: 8.80 },
+  { date: '2025-10-29', valor: 9.60, high: 9.80, low: 9.40 },
+  { date: '2025-10-30', valor: 10.25, high: 10.50, low: 10.05 },
+  { date: '2025-10-31', valor: 10.80, high: 11.00, low: 10.65 }, // novo rally
+  { date: '2025-11-01', valor: 11.20, high: 11.45, low: 11.00 },
+  { date: '2025-11-02', valor: 10.95, high: 11.10, low: 10.70 },
+  { date: '2025-11-03', valor: 10.50, high: 10.65, low: 10.30 },
+  { date: '2025-11-04', valor: 9.80, high: 9.95, low: 9.55 },   // nova correção
+  { date: '2025-11-05', valor: 10.05, high: 10.30, low: 9.85 }
   ];
 </script>
 
@@ -165,7 +217,7 @@ $: displayPendingSells = pendingSells.map((o) => ({
   <div class="p-4 space-y-4">
     <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
       <div class="md:col-span-1">
-        <TabelaOrdens />
+        <TabelaOrdens {ordensCompra} {ordensVenda} {ultimaVenda} />
       </div>
       <div class="md:col-span-2">
         <GraficoCandlestick {dadosMock} />