Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
| id | title | type | status | priority | epic | story_points | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| US-TRD-017 | Zoom y Pan en el Chart | User Story | Done | Alta | OQI-003 | 3 | 2025-12-05 | 2026-01-04 |
US-TRD-017: Zoom y Pan en el Chart
Metadata
| Campo | Valor |
|---|---|
| ID | US-TRD-017 |
| Épica | OQI-003 - Trading y Charts |
| Módulo | trading |
| Prioridad | P1 |
| Story Points | 3 |
| Sprint | Sprint 5 |
| Estado | Pendiente |
| Asignado a | Por asignar |
Historia de Usuario
Como trader, quiero hacer zoom y desplazarme (pan) en el chart usando mouse, trackpad o touch, para analizar detalles específicos del precio en diferentes escalas temporales y niveles de zoom.
Descripción Detallada
El usuario debe poder navegar por el chart de forma fluida, haciendo zoom in/out para ver más o menos velas, y desplazándose horizontalmente para ver datos históricos. La navegación debe ser intuitiva usando mouse wheel, pinch gestures, o botones.
Mockups/Wireframes
┌─────────────────────────────────────────────────────────────────┐
│ BTCUSDT $97,234.50 +2.34% ▲ │
├─────────────────────────────────────────────────────────────────┤
│ [1m] [5m] [15m] [1h] [4h] [1D] [1W] [Indicators ▼] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ZOOM CONTROLS: │
│ [+] [-] [Fit] [Auto] │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ◄── Pan Left Chart Area Pan Right ──►│ │
│ │ │ │
│ │ ████ │ │
│ │ ████ ████ Zoom Level: 100% │ │
│ │ ████ ████ ████ Candles visible: 168 │ │
│ │ ████ ████ │ │
│ │ ████ │ │
│ │ │ │
│ │ [ Scroll to zoom ] [ Drag to pan ] │ │
│ │ [ Pinch gesture ] [ Double-click reset ] │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ TIMELINE: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ░░░░░░░░░░████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
│ │ Nov 1 Dec 5 (Current view) Jan 1 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
GESTOS SOPORTADOS:
┌─────────────────────────────────────┐
│ Mouse: │
│ • Scroll wheel: Zoom in/out │
│ • Click + Drag: Pan left/right │
│ • Double-click: Reset zoom │
│ │
│ Trackpad: │
│ • Pinch: Zoom in/out │
│ • Two-finger drag: Pan │
│ │
│ Touch (Mobile): │
│ • Pinch: Zoom in/out │
│ • Swipe: Pan left/right │
│ • Double-tap: Reset zoom │
│ │
│ Keyboard: │
│ • +/-: Zoom in/out │
│ • Arrow keys: Pan left/right │
│ • Home/End: Go to start/end │
│ • 0: Reset zoom to 100% │
└─────────────────────────────────────┘
Criterios de Aceptación
Escenario 1: Zoom in con mouse wheel
DADO que el usuario está viendo el chart con 168 velas visibles
CUANDO hace scroll hacia adelante (wheel up)
ENTONCES el chart hace zoom in
Y muestra menos velas (ej: 84 velas)
Y las velas se ven más grandes y detalladas
Y el zoom se centra en la posición del cursor
Escenario 2: Zoom out con mouse wheel
DADO que el usuario está viendo el chart con 168 velas
CUANDO hace scroll hacia atrás (wheel down)
ENTONCES el chart hace zoom out
Y muestra más velas (ej: 336 velas)
Y las velas se ven más pequeñas
Y puede ver un rango temporal más amplio
Escenario 3: Pan con click y drag
DADO que el usuario está viendo el chart
CUANDO hace click y arrastra hacia la izquierda
ENTONCES el chart se desplaza hacia la derecha
Y muestra datos históricos más antiguos
Y el desplazamiento es fluido y sigue el cursor
CUANDO arrastra hacia la derecha
ENTONCES el chart se desplaza hacia la izquierda
Y muestra datos más recientes
Escenario 4: Zoom con pinch gesture (mobile/trackpad)
DADO que el usuario está en mobile o usando trackpad
CUANDO hace pinch out (separar dedos)
ENTONCES el chart hace zoom in
Y las velas se agrandan
CUANDO hace pinch in (juntar dedos)
ENTONCES el chart hace zoom out
Y se ven más velas
Escenario 5: Reset zoom con double-click
DADO que el usuario ha hecho zoom y pan
CUANDO hace double-click en el chart
ENTONCES el zoom se resetea a 100%
Y se muestra el rango por defecto (168 velas)
Y se centra en las velas más recientes
Escenario 6: Zoom con botones
DADO que el usuario hace click en botón [+]
ENTONCES el chart hace zoom in un 20%
DADO que hace click en botón [-]
ENTONCES el chart hace zoom out un 20%
DADO que hace click en botón [Fit]
ENTONCES el chart se ajusta para mostrar todas las velas disponibles
DADO que hace click en botón [Auto]
ENTONCES el chart vuelve al zoom automático (latest candles)
Escenario 7: Pan con teclado
DADO que el usuario presiona flecha izquierda
ENTONCES el chart se desplaza hacia la izquierda (muestra datos antiguos)
DADO que presiona flecha derecha
ENTONCES el chart se desplaza hacia la derecha (muestra datos recientes)
DADO que presiona Home
ENTONCES el chart va al inicio (primera vela disponible)
DADO que presiona End
ENTONCES el chart va al final (última vela - presente)
Escenario 8: Límites de zoom
DADO que el usuario hace zoom in al máximo
CUANDO intenta hacer más zoom
ENTONCES el chart no hace más zoom
Y muestra mínimo 20 velas (zoom máximo)
DADO que hace zoom out al máximo
ENTONCES muestra todas las velas disponibles
Y no permite zoom out adicional
Escenario 9: Timeline navigation
DADO que el usuario ve el timeline debajo del chart
CUANDO hace click en una posición del timeline
ENTONCES el chart salta a ese rango temporal
Y se centra en la fecha clickeada
Criterios Adicionales
- Animación suave de zoom y pan (60 FPS)
- Indicador visual del rango visible en timeline
- Mantener zoom level al cambiar timeframe
- Auto-scroll al último precio cuando hay nuevas velas
- Minimap para navegación rápida
Tareas Técnicas
Database:
- No requiere cambios en DB
Backend:
- BE-TRD-094: Optimizar endpoint candles para rangos variables
- BE-TRD-095: Implementar paginación eficiente de velas históricas
Frontend:
- FE-TRD-093: Configurar zoom en Lightweight Charts
- FE-TRD-094: Implementar pan con mouse drag
- FE-TRD-095: Implementar zoom con wheel
- FE-TRD-096: Implementar pinch gestures (mobile/trackpad)
- FE-TRD-097: Crear componente ZoomControls.tsx
- FE-TRD-098: Crear componente Timeline.tsx
- FE-TRD-099: Implementar keyboard shortcuts
- FE-TRD-100: Implementar límites de zoom
- FE-TRD-101: Implementar hook useChartNavigation
Tests:
- TEST-TRD-046: Test unitario zoom logic
- TEST-TRD-047: Test integración pan y zoom
- TEST-TRD-048: Test E2E navegación completa
Dependencias
Depende de:
- US-TRD-001: Ver chart - Estado: Pendiente
Bloquea:
- Ninguna
Notas Técnicas
Componentes UI:
ZoomControls: Botones de zoomTimeline: Barra de navegación temporalChartContainer: Wrapper que maneja eventos
Lightweight Charts Configuration:
const chart = createChart(container, {
timeScale: {
rightOffset: 12,
barSpacing: 3,
fixLeftEdge: false,
fixRightEdge: false,
lockVisibleTimeRangeOnResize: true,
rightBarStaysOnScroll: true,
borderVisible: true,
borderColor: '#fff000',
visible: true,
timeVisible: true,
secondsVisible: false,
shiftVisibleRangeOnNewBar: true,
},
handleScroll: {
mouseWheel: true,
pressedMouseMove: true,
horzTouchDrag: true,
vertTouchDrag: false,
},
handleScale: {
axisPressedMouseMove: true,
mouseWheel: true,
pinch: true,
},
});
Zoom Implementation:
// Zoom with mouse wheel
function handleWheel(event: WheelEvent) {
event.preventDefault();
const chart = chartRef.current;
if (!chart) return;
const delta = event.deltaY;
const zoomFactor = delta > 0 ? 0.9 : 1.1; // Zoom out / Zoom in
const timeScale = chart.timeScale();
const visibleRange = timeScale.getVisibleRange();
if (!visibleRange) return;
const from = visibleRange.from as number;
const to = visibleRange.to as number;
const center = (from + to) / 2;
const newRange = (to - from) * zoomFactor;
timeScale.setVisibleRange({
from: center - newRange / 2,
to: center + newRange / 2,
});
}
// Zoom with buttons
function zoomIn() {
const timeScale = chart.timeScale();
const range = timeScale.getVisibleRange();
if (!range) return;
const center = (range.from + range.to) / 2;
const newRange = (range.to - range.from) * 0.8; // 20% zoom in
timeScale.setVisibleRange({
from: center - newRange / 2,
to: center + newRange / 2,
});
}
function zoomOut() {
const timeScale = chart.timeScale();
const range = timeScale.getVisibleRange();
if (!range) return;
const center = (range.from + range.to) / 2;
const newRange = (range.to - range.from) * 1.2; // 20% zoom out
timeScale.setVisibleRange({
from: center - newRange / 2,
to: center + newRange / 2,
});
}
// Reset zoom
function resetZoom() {
chart.timeScale().fitContent();
}
// Auto zoom (show latest)
function autoZoom() {
chart.timeScale().scrollToRealTime();
}
Pan Implementation:
// Pan with mouse drag
let isDragging = false;
let startX = 0;
function handleMouseDown(event: MouseEvent) {
isDragging = true;
startX = event.clientX;
container.style.cursor = 'grabbing';
}
function handleMouseMove(event: MouseEvent) {
if (!isDragging) return;
const deltaX = event.clientX - startX;
startX = event.clientX;
const timeScale = chart.timeScale();
const range = timeScale.getVisibleRange();
if (!range) return;
const rangeWidth = range.to - range.from;
const containerWidth = container.clientWidth;
const pixelToTime = rangeWidth / containerWidth;
const shift = -deltaX * pixelToTime;
timeScale.setVisibleRange({
from: range.from + shift,
to: range.to + shift,
});
}
function handleMouseUp() {
isDragging = false;
container.style.cursor = 'default';
}
Touch/Pinch Implementation:
let lastDistance = 0;
function handleTouchMove(event: TouchEvent) {
if (event.touches.length === 2) {
// Pinch zoom
event.preventDefault();
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
if (lastDistance > 0) {
const zoomFactor = distance / lastDistance;
applyZoom(zoomFactor);
}
lastDistance = distance;
}
}
function handleTouchEnd() {
lastDistance = 0;
}
Keyboard Shortcuts:
function handleKeyDown(event: KeyboardEvent) {
const timeScale = chart.timeScale();
switch (event.key) {
case '+':
case '=':
zoomIn();
break;
case '-':
case '_':
zoomOut();
break;
case 'ArrowLeft':
pan(-50); // Pan left 50 pixels
break;
case 'ArrowRight':
pan(50); // Pan right 50 pixels
break;
case 'Home':
timeScale.scrollToPosition(0, false);
break;
case 'End':
timeScale.scrollToRealTime();
break;
case '0':
resetZoom();
break;
}
}
Zoom Limits:
const MIN_VISIBLE_CANDLES = 20;
const MAX_VISIBLE_CANDLES = 1000;
function applyZoom(zoomFactor: number) {
const timeScale = chart.timeScale();
const range = timeScale.getVisibleRange();
if (!range) return;
const currentCandles = calculateVisibleCandles(range);
const newCandles = currentCandles / zoomFactor;
// Apply limits
if (newCandles < MIN_VISIBLE_CANDLES || newCandles > MAX_VISIBLE_CANDLES) {
return; // Don't zoom beyond limits
}
// Apply zoom
const center = (range.from + range.to) / 2;
const newRange = (range.to - range.from) * zoomFactor;
timeScale.setVisibleRange({
from: center - newRange / 2,
to: center + newRange / 2,
});
}
Timeline Component:
function Timeline({ chart, candles }) {
const [visibleRange, setVisibleRange] = useState(null);
useEffect(() => {
const timeScale = chart.timeScale();
const updateRange = () => {
setVisibleRange(timeScale.getVisibleRange());
};
timeScale.subscribeVisibleTimeRangeChange(updateRange);
return () => timeScale.unsubscribeVisibleTimeRangeChange(updateRange);
}, [chart]);
const handleTimelineClick = (position: number) => {
// Jump to clicked position in timeline
const timeScale = chart.timeScale();
const totalRange = candles[candles.length - 1].time - candles[0].time;
const clickedTime = candles[0].time + (totalRange * position);
timeScale.scrollToPosition(clickedTime, true);
};
return (
<div className="timeline" onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const position = (e.clientX - rect.left) / rect.width;
handleTimelineClick(position);
}}>
{/* Render timeline visualization */}
</div>
);
}
Definition of Ready (DoR)
- Historia claramente escrita
- Criterios de aceptación definidos
- Story points estimados
- Dependencias identificadas
- Sin bloqueadores
- Diseño/mockup disponible
- API spec disponible
Definition of Done (DoD)
- Código implementado según criterios
- Tests unitarios escritos y pasando
- Tests de integración pasando
- Code review aprobado
- Documentación actualizada
- QA aprobado
- Desplegado en ambiente de pruebas
Historial de Cambios
| Fecha | Cambio | Autor |
|---|---|---|
| 2025-12-05 | Creación | Requirements-Analyst |
Creada por: Requirements-Analyst Fecha: 2025-12-05 Última actualización: 2025-12-05