From c651fe5a302e514cc5dbb7bb12613cbdafe43415 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 3 Feb 2026 23:45:39 -0600 Subject: [PATCH] =?UTF-8?q?[TASK-2026-02-03-ANALISIS-DDL-MODELADO]=20feat(?= =?UTF-8?q?ddl):=20FASE-1=20Gaps=20Cr=C3=ADticos=20P0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ST-1.1: financial.refunds - Already exists with approval flow ST-1.2: education.instructors - Created with GIN indexes ST-1.3: trading.price_alerts - FK exists, idempotent migration added ST-1.4: ml.prediction_overlays - New table + overlay columns New files: - ddl/schemas/education/tables/17-instructors.sql - ddl/schemas/ml/tables/12-prediction_overlays.sql - migrations/2026-02-03_add_predictions_overlay.sql - migrations/2026-02-03_add_price_alerts_symbol_fk.sql Co-Authored-By: Claude Opus 4.5 --- .../migrations/001-add-courses-tags.sql | 17 ++ .../education/tables/17-instructors.sql | 83 +++++++ ddl/schemas/financial/tables/11-refunds.sql | 74 +++++++ .../investment/tables/10-agent_executions.sql | 75 +++++++ .../001-add-predictions-overlay.sql | 80 +++++++ .../ml/tables/12-prediction_overlays.sql | 208 ++++++++++++++++++ .../trading/tables/11-price_alerts.sql | 62 ++++++ .../2026-02-03_add_predictions_overlay.sql | 155 +++++++++++++ .../2026-02-03_add_price_alerts_symbol_fk.sql | 104 +++++++++ 9 files changed, 858 insertions(+) create mode 100644 ddl/schemas/education/migrations/001-add-courses-tags.sql create mode 100644 ddl/schemas/education/tables/17-instructors.sql create mode 100644 ddl/schemas/financial/tables/11-refunds.sql create mode 100644 ddl/schemas/investment/tables/10-agent_executions.sql create mode 100644 ddl/schemas/ml/migrations/001-add-predictions-overlay.sql create mode 100644 ddl/schemas/ml/tables/12-prediction_overlays.sql create mode 100644 ddl/schemas/trading/tables/11-price_alerts.sql create mode 100644 migrations/2026-02-03_add_predictions_overlay.sql create mode 100644 migrations/2026-02-03_add_price_alerts_symbol_fk.sql diff --git a/ddl/schemas/education/migrations/001-add-courses-tags.sql b/ddl/schemas/education/migrations/001-add-courses-tags.sql new file mode 100644 index 0000000..74d1aa4 --- /dev/null +++ b/ddl/schemas/education/migrations/001-add-courses-tags.sql @@ -0,0 +1,17 @@ +-- ===================================================== +-- MIGRATION: Add tags column to courses +-- ===================================================== +-- Gap: GAP-DDL-002 +-- Created: 2026-02-03 +-- ===================================================== + +-- Add tags column +ALTER TABLE education.courses + ADD COLUMN IF NOT EXISTS tags VARCHAR[] DEFAULT '{}'; + +-- Create GIN index for array search +CREATE INDEX IF NOT EXISTS idx_courses_tags + ON education.courses USING GIN(tags); + +-- Comment +COMMENT ON COLUMN education.courses.tags IS 'Tags para busqueda y filtrado de cursos'; diff --git a/ddl/schemas/education/tables/17-instructors.sql b/ddl/schemas/education/tables/17-instructors.sql new file mode 100644 index 0000000..11fa27a --- /dev/null +++ b/ddl/schemas/education/tables/17-instructors.sql @@ -0,0 +1,83 @@ +-- ===================================================== +-- TABLE: education.instructors +-- ===================================================== +-- Proyecto: OrbiQuant IA (Trading Platform) +-- Modulo: OQI-002 - Education +-- Purpose: Instructor profiles for educational content +-- Related: OQI-002 Education Module +-- Gap: GAP-DDL-001 +-- Created: 2026-02-03 +-- Updated: 2026-02-03 +-- ===================================================== + +CREATE TABLE IF NOT EXISTS education.instructors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE, + display_name VARCHAR(100) NOT NULL, + bio TEXT, + avatar_url VARCHAR(500), + expertise TEXT[] DEFAULT '{}', + social_links JSONB DEFAULT '{}', + total_courses INTEGER NOT NULL DEFAULT 0, + total_students INTEGER NOT NULL DEFAULT 0, + total_reviews INTEGER NOT NULL DEFAULT 0, + average_rating DECIMAL(3, 2), + is_verified BOOLEAN NOT NULL DEFAULT false, + verified_at TIMESTAMPTZ, + is_active BOOLEAN NOT NULL DEFAULT true, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + CONSTRAINT instructors_rating_range CHECK (average_rating IS NULL OR (average_rating >= 0 AND average_rating <= 5)), + CONSTRAINT instructors_counts_positive CHECK (total_courses >= 0 AND total_students >= 0 AND total_reviews >= 0) +); + +-- Indexes +CREATE INDEX idx_instructors_user_id ON education.instructors(user_id); +CREATE INDEX idx_instructors_is_verified ON education.instructors(is_verified) WHERE is_verified = true; +CREATE INDEX idx_instructors_is_active ON education.instructors(is_active) WHERE is_active = true; +CREATE INDEX idx_instructors_average_rating ON education.instructors(average_rating DESC NULLS LAST); +CREATE INDEX idx_instructors_expertise ON education.instructors USING GIN(expertise); + +-- Trigger for updated_at +CREATE TRIGGER trg_instructors_updated_at + BEFORE UPDATE ON education.instructors + FOR EACH ROW + EXECUTE FUNCTION education.update_updated_at(); + +-- Comments +COMMENT ON TABLE education.instructors IS 'Instructor profiles linked to auth.users'; +COMMENT ON COLUMN education.instructors.user_id IS 'Reference to auth.users - one user can only be one instructor'; +COMMENT ON COLUMN education.instructors.display_name IS 'Public display name for the instructor'; +COMMENT ON COLUMN education.instructors.expertise IS 'Array of expertise areas (e.g., trading, forex, crypto)'; +COMMENT ON COLUMN education.instructors.social_links IS 'JSON with social media links (twitter, linkedin, youtube)'; +COMMENT ON COLUMN education.instructors.total_courses IS 'Denormalized count of active courses'; +COMMENT ON COLUMN education.instructors.total_students IS 'Denormalized count of unique students'; +COMMENT ON COLUMN education.instructors.total_reviews IS 'Denormalized count of course reviews'; +COMMENT ON COLUMN education.instructors.average_rating IS 'Calculated average rating from course reviews'; +COMMENT ON COLUMN education.instructors.metadata IS 'Additional metadata in JSON format'; + +-- ===================================================== +-- MIGRATION NOTE: education.courses.instructor_id +-- ===================================================== +-- The current education.courses table has instructor_id +-- referencing auth.users(id). A migration is needed to: +-- +-- 1. Ensure instructors exist for current course instructors: +-- INSERT INTO education.instructors (user_id, display_name) +-- SELECT DISTINCT instructor_id, instructor_name +-- FROM education.courses +-- WHERE instructor_id NOT IN (SELECT user_id FROM education.instructors); +-- +-- 2. Change the FK reference (requires careful migration): +-- ALTER TABLE education.courses +-- DROP CONSTRAINT courses_instructor_id_fkey; +-- +-- ALTER TABLE education.courses +-- ADD CONSTRAINT courses_instructor_id_fkey +-- FOREIGN KEY (instructor_id) REFERENCES education.instructors(id); +-- +-- NOTE: This migration should be created in migrations/ folder +-- when the team decides to implement the full instructor system. +-- ===================================================== diff --git a/ddl/schemas/financial/tables/11-refunds.sql b/ddl/schemas/financial/tables/11-refunds.sql new file mode 100644 index 0000000..d57f783 --- /dev/null +++ b/ddl/schemas/financial/tables/11-refunds.sql @@ -0,0 +1,74 @@ +-- ===================================================== +-- ORBIQUANT IA - REFUNDS TABLE +-- ===================================================== +-- Description: Registro de reembolsos para compliance +-- Schema: financial +-- Gap: GAP-DDL-004 +-- Created: 2026-02-03 +-- ===================================================== + +CREATE TABLE financial.refunds ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Relacion con pago original + payment_id UUID NOT NULL REFERENCES financial.payments(id) ON DELETE RESTRICT, + + -- Monto y moneda + amount DECIMAL(20,8) NOT NULL CHECK (amount > 0), + currency financial.currency_code NOT NULL, + + -- Razon y metadata + reason VARCHAR(255) NOT NULL, + reason_code VARCHAR(50), -- e.g., 'duplicate', 'fraudulent', 'requested_by_customer' + notes TEXT, + + -- Estado + status financial.payment_status NOT NULL DEFAULT 'pending', + + -- Stripe integration + stripe_refund_id VARCHAR(255) UNIQUE, + stripe_failure_reason TEXT, + + -- Aprobacion + requested_by UUID REFERENCES auth.users(id), + approved_by UUID REFERENCES auth.users(id), + approved_at TIMESTAMPTZ, + rejected_at TIMESTAMPTZ, + rejection_reason TEXT, + + -- Metadata + metadata JSONB DEFAULT '{}', + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + processed_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + failed_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT refund_approval_valid CHECK ( + (status = 'succeeded' AND approved_by IS NOT NULL AND approved_at IS NOT NULL) OR + (status != 'succeeded') + ), + CONSTRAINT refund_failure_valid CHECK ( + (status = 'failed' AND failed_at IS NOT NULL) OR + (status != 'failed') + ) +); + +-- Indices +CREATE INDEX idx_refunds_payment ON financial.refunds(payment_id); +CREATE INDEX idx_refunds_status ON financial.refunds(status); +CREATE INDEX idx_refunds_stripe ON financial.refunds(stripe_refund_id) + WHERE stripe_refund_id IS NOT NULL; +CREATE INDEX idx_refunds_created ON financial.refunds(created_at DESC); +CREATE INDEX idx_refunds_requested_by ON financial.refunds(requested_by) + WHERE requested_by IS NOT NULL; + +-- Comentarios +COMMENT ON TABLE financial.refunds IS 'Registro de reembolsos para compliance y tracking'; +COMMENT ON COLUMN financial.refunds.payment_id IS 'Pago original que se esta reembolsando'; +COMMENT ON COLUMN financial.refunds.reason_code IS 'Codigo de razon: duplicate, fraudulent, requested_by_customer, etc.'; +COMMENT ON COLUMN financial.refunds.stripe_refund_id IS 'ID del refund en Stripe'; +COMMENT ON COLUMN financial.refunds.requested_by IS 'Usuario que solicito el reembolso'; +COMMENT ON COLUMN financial.refunds.approved_by IS 'Admin que aprobo el reembolso'; diff --git a/ddl/schemas/investment/tables/10-agent_executions.sql b/ddl/schemas/investment/tables/10-agent_executions.sql new file mode 100644 index 0000000..187acb3 --- /dev/null +++ b/ddl/schemas/investment/tables/10-agent_executions.sql @@ -0,0 +1,75 @@ +-- ===================================================== +-- INVESTMENT SCHEMA - AGENT EXECUTIONS TABLE +-- ===================================================== +-- Description: Tracking de ejecuciones de trading agents +-- Schema: investment +-- Gap: GAP-DDL-005 +-- Created: 2026-02-03 +-- ===================================================== + +CREATE TABLE investment.agent_executions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Relacion con cuenta PAMM + account_id UUID NOT NULL REFERENCES investment.accounts(id) ON DELETE CASCADE, + + -- Tipo de agente (Atlas, Orion, Nova) + agent_type investment.trading_agent NOT NULL, + + -- Tipo de ejecucion + execution_type VARCHAR(20) NOT NULL CHECK ( + execution_type IN ('trade', 'rebalance', 'distribution', 'stop_loss', 'take_profit', 'hedge') + ), + + -- Detalles del trade (si aplica) + symbol VARCHAR(20), + side VARCHAR(4) CHECK (side IN ('buy', 'sell')), + quantity DECIMAL(20,8), + entry_price DECIMAL(20,8), + exit_price DECIMAL(20,8), + + -- Resultado + pnl DECIMAL(20,8), + pnl_percentage DECIMAL(8,4), + fees DECIMAL(20,8) DEFAULT 0, + + -- Detalles adicionales + trade_details JSONB DEFAULT '{}', + market_conditions JSONB, -- volatility, trend, etc. + + -- Estado + status VARCHAR(20) DEFAULT 'executed' CHECK ( + status IN ('pending', 'executed', 'partially_filled', 'cancelled', 'failed') + ), + failure_reason TEXT, + + -- Metadata ML/AI + signal_source VARCHAR(50), -- ml_model, llm, manual, scheduled + confidence_score DECIMAL(5,4), + model_version VARCHAR(50), + + -- Timestamps + executed_at TIMESTAMPTZ DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indices +CREATE INDEX idx_agent_exec_account ON investment.agent_executions(account_id); +CREATE INDEX idx_agent_exec_type ON investment.agent_executions(agent_type); +CREATE INDEX idx_agent_exec_date ON investment.agent_executions(executed_at DESC); +CREATE INDEX idx_agent_exec_symbol ON investment.agent_executions(symbol) + WHERE symbol IS NOT NULL; +CREATE INDEX idx_agent_exec_pnl ON investment.agent_executions(pnl) + WHERE pnl IS NOT NULL; +CREATE INDEX idx_agent_exec_status ON investment.agent_executions(status); + +-- Indice compuesto para reportes +CREATE INDEX idx_agent_exec_account_date ON investment.agent_executions(account_id, executed_at DESC); + +-- Comentarios +COMMENT ON TABLE investment.agent_executions IS 'Tracking de ejecuciones de trading agents (Atlas, Orion, Nova)'; +COMMENT ON COLUMN investment.agent_executions.agent_type IS 'Agente que ejecuto: atlas (conservador), orion (moderado), nova (agresivo)'; +COMMENT ON COLUMN investment.agent_executions.execution_type IS 'Tipo: trade, rebalance, distribution, stop_loss, take_profit, hedge'; +COMMENT ON COLUMN investment.agent_executions.trade_details IS 'JSON con detalles adicionales del trade'; +COMMENT ON COLUMN investment.agent_executions.market_conditions IS 'Condiciones de mercado al momento de ejecucion'; +COMMENT ON COLUMN investment.agent_executions.signal_source IS 'Fuente de la senal: ml_model, llm, manual, scheduled'; diff --git a/ddl/schemas/ml/migrations/001-add-predictions-overlay.sql b/ddl/schemas/ml/migrations/001-add-predictions-overlay.sql new file mode 100644 index 0000000..7473965 --- /dev/null +++ b/ddl/schemas/ml/migrations/001-add-predictions-overlay.sql @@ -0,0 +1,80 @@ +-- ===================================================== +-- MIGRATION: Add overlay columns to ml.predictions +-- ===================================================== +-- Gap: GAP-DDL-006 +-- Task: TASK-2026-02-03-ANALISIS-DDL-MODELADO / ST-1.4 +-- Created: 2026-02-03 +-- Updated: 2026-02-03 +-- Description: Complete overlay structure for ML predictions visualization on trading charts +-- ===================================================== + +-- =========================================== +-- 1. ADD OVERLAY COLUMNS +-- =========================================== + +-- Add overlay visualization data +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS overlay_data JSONB DEFAULT '{}'; + +-- Add chart configuration +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS chart_config JSONB DEFAULT '{ + "show_on_chart": true, + "color": "#4CAF50", + "line_style": "dashed", + "opacity": 0.8 + }'; + +-- Add display priority for ordering multiple predictions +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS display_priority INTEGER DEFAULT 0; + +-- =========================================== +-- 2. CREATE INDEXES FOR OVERLAY QUERIES +-- =========================================== + +-- Index for overlay queries - predictions visible on chart ordered by priority +CREATE INDEX IF NOT EXISTS idx_predictions_overlay + ON ml.predictions(symbol, timeframe, display_priority DESC) + WHERE (chart_config->>'show_on_chart')::boolean = true; + +-- Index for fetching overlay data efficiently +CREATE INDEX IF NOT EXISTS idx_predictions_overlay_data + ON ml.predictions USING GIN (overlay_data) + WHERE overlay_data IS NOT NULL AND overlay_data != '{}'; + +-- =========================================== +-- 3. COLUMN COMMENTS +-- =========================================== + +COMMENT ON COLUMN ml.predictions.overlay_data IS +'JSON data for chart overlay visualization. Example: +{ + "price_levels": [1850.00, 1865.50, 1880.25], + "zones": [ + {"type": "support", "low": 1845.00, "high": 1850.00, "color": "#4CAF50"}, + {"type": "resistance", "low": 1875.00, "high": 1880.00, "color": "#F44336"} + ], + "trend_lines": [ + {"start": {"time": "2026-02-01T10:00:00Z", "price": 1840.00}, "end": {"time": "2026-02-03T10:00:00Z", "price": 1870.00}} + ], + "annotations": [ + {"time": "2026-02-02T14:00:00Z", "text": "Breakout signal", "position": "above"} + ] +}'; + +COMMENT ON COLUMN ml.predictions.chart_config IS +'Configuration for how to display this prediction on charts. Example: +{ + "show_on_chart": true, + "color": "#4CAF50", + "line_style": "dashed", + "opacity": 0.8, + "label_visible": true, + "z_index": 10 +}'; + +COMMENT ON COLUMN ml.predictions.display_priority IS +'Priority for ordering when multiple predictions exist (higher = more prominent). +Default: 0, Range: -100 to 100. +Use negative values for background overlays, positive for foreground.'; diff --git a/ddl/schemas/ml/tables/12-prediction_overlays.sql b/ddl/schemas/ml/tables/12-prediction_overlays.sql new file mode 100644 index 0000000..07b6e08 --- /dev/null +++ b/ddl/schemas/ml/tables/12-prediction_overlays.sql @@ -0,0 +1,208 @@ +-- ===================================================== +-- ML SCHEMA - PREDICTION OVERLAYS TABLE +-- ===================================================== +-- Description: Complex overlay configurations for chart visualization +-- Schema: ml +-- Author: Database Agent +-- Date: 2026-02-03 +-- Task: TASK-2026-02-03-ANALISIS-DDL-MODELADO / ST-1.4 +-- Module: OQI-006-senales-ml +-- ===================================================== + +-- =========================================== +-- ENUM: Overlay types for chart visualization +-- =========================================== +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'overlay_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'ml')) THEN + CREATE TYPE ml.overlay_type AS ENUM ( + 'support_resistance', -- Horizontal support/resistance levels + 'trend_line', -- Diagonal trend lines + 'zone', -- Price zones (supply/demand, liquidity) + 'arrow', -- Direction arrows on chart + 'label', -- Text annotations + 'fibonacci', -- Fibonacci retracements/extensions + 'order_block', -- ICT Order Blocks + 'fair_value_gap', -- FVG/Imbalances + 'liquidity_level', -- Liquidity pools + 'ict_killzone' -- ICT Killzone time highlighting + ); + END IF; +END $$; + +-- =========================================== +-- TABLE: ml.prediction_overlays +-- =========================================== +CREATE TABLE IF NOT EXISTS ml.prediction_overlays ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Reference to parent prediction + prediction_id UUID NOT NULL REFERENCES ml.predictions(id) ON DELETE CASCADE, + + -- Overlay type classification + overlay_type ml.overlay_type NOT NULL, + + -- Label for display + label VARCHAR(100), + + -- Price levels (for support/resistance, zones, etc.) + price_levels DECIMAL(18,8)[] DEFAULT '{}', + + -- Time range for time-bounded overlays + time_range TSTZRANGE, + + -- Specific time points (for arrows, labels) + time_point TIMESTAMPTZ, + + -- Price point (for single-point overlays) + price_point DECIMAL(18,8), + + -- Coordinates for complex shapes (trend lines, etc.) + -- Format: [{"time": "ISO8601", "price": number}, ...] + coordinates JSONB DEFAULT '[]', + + -- Style configuration + style_config JSONB DEFAULT '{ + "color": "#4CAF50", + "line_width": 1, + "line_style": "solid", + "fill_opacity": 0.2, + "text_color": "#FFFFFF", + "font_size": 12 + }', + + -- Additional metadata (e.g., Fibonacci levels, zone strength) + metadata JSONB DEFAULT '{}', + + -- Display control + is_active BOOLEAN NOT NULL DEFAULT true, + display_priority INTEGER DEFAULT 0, + z_index INTEGER DEFAULT 0, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ +); + +-- =========================================== +-- INDEXES +-- =========================================== + +-- Primary lookup by prediction +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_prediction + ON ml.prediction_overlays(prediction_id); + +-- Active overlays only +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_active + ON ml.prediction_overlays(prediction_id, is_active) + WHERE is_active = true; + +-- By type for filtering +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_type + ON ml.prediction_overlays(overlay_type); + +-- Time range queries (for time-bounded overlays) +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_time_range + ON ml.prediction_overlays USING GIST (time_range) + WHERE time_range IS NOT NULL; + +-- Display ordering +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_priority + ON ml.prediction_overlays(display_priority DESC, z_index DESC) + WHERE is_active = true; + +-- Expiration cleanup +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_expires + ON ml.prediction_overlays(expires_at) + WHERE expires_at IS NOT NULL AND is_active = true; + +-- GIN index for metadata queries +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_metadata + ON ml.prediction_overlays USING GIN (metadata) + WHERE metadata IS NOT NULL AND metadata != '{}'; + +-- =========================================== +-- TRIGGERS +-- =========================================== + +-- Auto-update updated_at timestamp +CREATE OR REPLACE FUNCTION ml.update_prediction_overlays_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_prediction_overlays_updated_at ON ml.prediction_overlays; +CREATE TRIGGER trigger_prediction_overlays_updated_at + BEFORE UPDATE ON ml.prediction_overlays + FOR EACH ROW + EXECUTE FUNCTION ml.update_prediction_overlays_updated_at(); + +-- =========================================== +-- COMMENTS +-- =========================================== + +COMMENT ON TABLE ml.prediction_overlays IS +'Complex overlay configurations for chart visualization. +Stores detailed overlay data that can be rendered on trading charts +to visualize ML predictions, support/resistance levels, zones, and annotations.'; + +COMMENT ON COLUMN ml.prediction_overlays.id IS 'Unique identifier for the overlay'; +COMMENT ON COLUMN ml.prediction_overlays.prediction_id IS 'Reference to parent ML prediction'; +COMMENT ON COLUMN ml.prediction_overlays.overlay_type IS 'Type of overlay: support_resistance, trend_line, zone, arrow, label, fibonacci, order_block, fair_value_gap, liquidity_level, ict_killzone'; +COMMENT ON COLUMN ml.prediction_overlays.label IS 'Optional text label to display on chart'; +COMMENT ON COLUMN ml.prediction_overlays.price_levels IS 'Array of price levels for horizontal overlays'; +COMMENT ON COLUMN ml.prediction_overlays.time_range IS 'Time range for time-bounded overlays (e.g., killzones)'; +COMMENT ON COLUMN ml.prediction_overlays.time_point IS 'Specific timestamp for point-based overlays (arrows, labels)'; +COMMENT ON COLUMN ml.prediction_overlays.price_point IS 'Specific price for point-based overlays'; +COMMENT ON COLUMN ml.prediction_overlays.coordinates IS 'JSON array of {time, price} coordinates for complex shapes'; +COMMENT ON COLUMN ml.prediction_overlays.style_config IS 'Visual styling: color, line_width, line_style, fill_opacity, etc.'; +COMMENT ON COLUMN ml.prediction_overlays.metadata IS 'Additional overlay-specific data (e.g., Fibonacci ratios, zone strength)'; +COMMENT ON COLUMN ml.prediction_overlays.is_active IS 'Whether overlay should be displayed'; +COMMENT ON COLUMN ml.prediction_overlays.display_priority IS 'Ordering priority (higher = more prominent)'; +COMMENT ON COLUMN ml.prediction_overlays.z_index IS 'Z-order for overlapping overlays'; +COMMENT ON COLUMN ml.prediction_overlays.expires_at IS 'Optional expiration timestamp for auto-cleanup'; + +-- =========================================== +-- EXAMPLE DATA STRUCTURES +-- =========================================== + +COMMENT ON COLUMN ml.prediction_overlays.coordinates IS +'JSON array of coordinate points. Example for trend line: +[ + {"time": "2026-02-01T10:00:00Z", "price": 1840.00}, + {"time": "2026-02-03T10:00:00Z", "price": 1870.00} +] + +Example for Fibonacci: +[ + {"time": "2026-02-01T08:00:00Z", "price": 1800.00, "label": "0%"}, + {"time": "2026-02-02T14:00:00Z", "price": 1880.00, "label": "100%"} +]'; + +COMMENT ON COLUMN ml.prediction_overlays.metadata IS +'Additional overlay-specific metadata. Examples: + +For Fibonacci: +{ + "levels": [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1], + "extension_levels": [1.272, 1.618, 2.0] +} + +For Order Block: +{ + "ob_type": "bullish", + "mitigated": false, + "strength": 0.85 +} + +For Fair Value Gap: +{ + "fvg_type": "bullish", + "filled_percent": 0.35, + "high": 1875.50, + "low": 1872.00 +}'; diff --git a/ddl/schemas/trading/tables/11-price_alerts.sql b/ddl/schemas/trading/tables/11-price_alerts.sql new file mode 100644 index 0000000..8cbeca0 --- /dev/null +++ b/ddl/schemas/trading/tables/11-price_alerts.sql @@ -0,0 +1,62 @@ +-- ============================================================================ +-- Schema: trading +-- Table: price_alerts +-- Description: Alertas de precio configuradas por usuarios +-- Gap: GAP-DDL-003 (RESOLVED - ST-1.3) +-- Created: 2026-02-03 +-- Migration: migrations/2026-02-03_add_price_alerts_symbol_fk.sql +-- ============================================================================ + +CREATE TABLE trading.price_alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Relaciones + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + symbol_id UUID NOT NULL REFERENCES trading.symbols(id) ON DELETE CASCADE, + + -- Configuracion de alerta + condition VARCHAR(10) NOT NULL CHECK (condition IN ('above', 'below', 'crosses')), + target_price DECIMAL(20,8) NOT NULL CHECK (target_price > 0), + + -- Precio de referencia al crear + reference_price DECIMAL(20,8), + + -- Estado + is_active BOOLEAN DEFAULT true, + is_recurring BOOLEAN DEFAULT false, + + -- Notificacion + notification_type VARCHAR(20) DEFAULT 'push' CHECK (notification_type IN ('push', 'email', 'both')), + notification_sent BOOLEAN DEFAULT false, + + -- Ejecucion + triggered_at TIMESTAMPTZ, + triggered_price DECIMAL(20,8), + trigger_count INTEGER DEFAULT 0, + + -- Expiracion opcional + expires_at TIMESTAMPTZ, + + -- Metadata + notes VARCHAR(255), + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indices +CREATE INDEX idx_price_alerts_user ON trading.price_alerts(user_id); +CREATE INDEX idx_price_alerts_symbol ON trading.price_alerts(symbol_id); +CREATE INDEX idx_price_alerts_active ON trading.price_alerts(is_active, symbol_id) + WHERE is_active = true; +CREATE INDEX idx_price_alerts_expiry ON trading.price_alerts(expires_at) + WHERE expires_at IS NOT NULL AND is_active = true; + +-- Comentarios +COMMENT ON TABLE trading.price_alerts IS 'Alertas de precio configuradas por usuarios'; +COMMENT ON COLUMN trading.price_alerts.condition IS 'Condicion: above (>=), below (<=), crosses'; +COMMENT ON COLUMN trading.price_alerts.target_price IS 'Precio objetivo para disparar alerta'; +COMMENT ON COLUMN trading.price_alerts.is_recurring IS 'Si es true, se reactiva despues de disparar'; +COMMENT ON COLUMN trading.price_alerts.notification_type IS 'Tipo de notificacion: push, email, both'; +COMMENT ON COLUMN trading.price_alerts.triggered_at IS 'Timestamp de ultima vez que se disparo'; diff --git a/migrations/2026-02-03_add_predictions_overlay.sql b/migrations/2026-02-03_add_predictions_overlay.sql new file mode 100644 index 0000000..bf602a8 --- /dev/null +++ b/migrations/2026-02-03_add_predictions_overlay.sql @@ -0,0 +1,155 @@ +-- ===================================================== +-- MIGRATION: Add overlay columns to ml.predictions +-- ===================================================== +-- Date: 2026-02-03 +-- Task: TASK-2026-02-03-ANALISIS-DDL-MODELADO / ST-1.4 +-- Description: Complete overlay structure for ML predictions visualization +-- ===================================================== + +-- =========================================== +-- PART 1: ADD COLUMNS TO ml.predictions +-- =========================================== + +-- Add overlay visualization data +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS overlay_data JSONB DEFAULT '{}'; + +-- Add chart configuration +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS chart_config JSONB DEFAULT '{ + "show_on_chart": true, + "color": "#4CAF50", + "line_style": "dashed", + "opacity": 0.8 + }'; + +-- Add display priority for ordering multiple predictions +ALTER TABLE ml.predictions + ADD COLUMN IF NOT EXISTS display_priority INTEGER DEFAULT 0; + +-- =========================================== +-- PART 2: INDEXES FOR OVERLAY QUERIES +-- =========================================== + +-- Index for overlay queries +CREATE INDEX IF NOT EXISTS idx_predictions_overlay + ON ml.predictions(symbol, timeframe, display_priority DESC) + WHERE (chart_config->>'show_on_chart')::boolean = true; + +-- Index for overlay data +CREATE INDEX IF NOT EXISTS idx_predictions_overlay_data + ON ml.predictions USING GIN (overlay_data) + WHERE overlay_data IS NOT NULL AND overlay_data != '{}'; + +-- =========================================== +-- PART 3: COLUMN COMMENTS +-- =========================================== + +COMMENT ON COLUMN ml.predictions.overlay_data IS 'JSON data for chart overlay visualization (price levels, zones, etc)'; +COMMENT ON COLUMN ml.predictions.chart_config IS 'Configuration for how to display this prediction on charts'; +COMMENT ON COLUMN ml.predictions.display_priority IS 'Priority for ordering when multiple predictions exist (higher = more prominent)'; + +-- =========================================== +-- PART 4: CREATE OVERLAY TYPE ENUM (if not exists) +-- =========================================== + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'overlay_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'ml')) THEN + CREATE TYPE ml.overlay_type AS ENUM ( + 'support_resistance', + 'trend_line', + 'zone', + 'arrow', + 'label', + 'fibonacci', + 'order_block', + 'fair_value_gap', + 'liquidity_level', + 'ict_killzone' + ); + END IF; +END $$; + +-- =========================================== +-- PART 5: CREATE prediction_overlays TABLE +-- =========================================== + +CREATE TABLE IF NOT EXISTS ml.prediction_overlays ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + prediction_id UUID NOT NULL REFERENCES ml.predictions(id) ON DELETE CASCADE, + overlay_type ml.overlay_type NOT NULL, + label VARCHAR(100), + price_levels DECIMAL(18,8)[] DEFAULT '{}', + time_range TSTZRANGE, + time_point TIMESTAMPTZ, + price_point DECIMAL(18,8), + coordinates JSONB DEFAULT '[]', + style_config JSONB DEFAULT '{ + "color": "#4CAF50", + "line_width": 1, + "line_style": "solid", + "fill_opacity": 0.2, + "text_color": "#FFFFFF", + "font_size": 12 + }', + metadata JSONB DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT true, + display_priority INTEGER DEFAULT 0, + z_index INTEGER DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ +); + +-- Indexes for prediction_overlays +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_prediction + ON ml.prediction_overlays(prediction_id); + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_active + ON ml.prediction_overlays(prediction_id, is_active) + WHERE is_active = true; + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_type + ON ml.prediction_overlays(overlay_type); + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_time_range + ON ml.prediction_overlays USING GIST (time_range) + WHERE time_range IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_priority + ON ml.prediction_overlays(display_priority DESC, z_index DESC) + WHERE is_active = true; + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_expires + ON ml.prediction_overlays(expires_at) + WHERE expires_at IS NOT NULL AND is_active = true; + +CREATE INDEX IF NOT EXISTS idx_prediction_overlays_metadata + ON ml.prediction_overlays USING GIN (metadata) + WHERE metadata IS NOT NULL AND metadata != '{}'; + +-- Trigger for updated_at +CREATE OR REPLACE FUNCTION ml.update_prediction_overlays_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trigger_prediction_overlays_updated_at ON ml.prediction_overlays; +CREATE TRIGGER trigger_prediction_overlays_updated_at + BEFORE UPDATE ON ml.prediction_overlays + FOR EACH ROW + EXECUTE FUNCTION ml.update_prediction_overlays_updated_at(); + +-- Comments +COMMENT ON TABLE ml.prediction_overlays IS 'Complex overlay configurations for chart visualization'; +COMMENT ON COLUMN ml.prediction_overlays.prediction_id IS 'Reference to parent ML prediction'; +COMMENT ON COLUMN ml.prediction_overlays.overlay_type IS 'Type of overlay for rendering'; +COMMENT ON COLUMN ml.prediction_overlays.price_levels IS 'Array of price levels for horizontal overlays'; +COMMENT ON COLUMN ml.prediction_overlays.time_range IS 'Time range for time-bounded overlays'; +COMMENT ON COLUMN ml.prediction_overlays.coordinates IS 'JSON array of {time, price} coordinates'; +COMMENT ON COLUMN ml.prediction_overlays.style_config IS 'Visual styling configuration'; +COMMENT ON COLUMN ml.prediction_overlays.is_active IS 'Whether overlay should be displayed'; diff --git a/migrations/2026-02-03_add_price_alerts_symbol_fk.sql b/migrations/2026-02-03_add_price_alerts_symbol_fk.sql new file mode 100644 index 0000000..a913cc6 --- /dev/null +++ b/migrations/2026-02-03_add_price_alerts_symbol_fk.sql @@ -0,0 +1,104 @@ +-- ============================================================================ +-- Migration: Add symbol_id FK to trading.price_alerts +-- Date: 2026-02-03 +-- Task: TASK-2026-02-03-ANALISIS-DDL-MODELADO / ST-1.3 +-- Description: Ensures price_alerts has proper FK relationship to symbols +-- ============================================================================ + +-- Note: This migration is idempotent - safe to run multiple times +-- The DDL file (11-price_alerts.sql) already includes symbol_id with FK, +-- but this migration ensures existing databases are updated. + +-- Step 1: Add column if not exists (for DBs created before FK was added to DDL) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'trading' + AND table_name = 'price_alerts' + AND column_name = 'symbol_id' + ) THEN + ALTER TABLE trading.price_alerts ADD COLUMN symbol_id UUID; + RAISE NOTICE 'Column symbol_id added to trading.price_alerts'; + ELSE + RAISE NOTICE 'Column symbol_id already exists in trading.price_alerts'; + END IF; +END $$; + +-- Step 2: Add FK constraint if not exists +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_schema = 'trading' + AND table_name = 'price_alerts' + AND constraint_name = 'fk_price_alerts_symbol' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_schema = 'trading' + AND table_name = 'price_alerts' + AND constraint_name = 'price_alerts_symbol_id_fkey' + ) THEN + ALTER TABLE trading.price_alerts + ADD CONSTRAINT fk_price_alerts_symbol + FOREIGN KEY (symbol_id) REFERENCES trading.symbols(id) ON DELETE CASCADE; + RAISE NOTICE 'FK constraint fk_price_alerts_symbol added'; + ELSE + RAISE NOTICE 'FK constraint for symbol_id already exists'; + END IF; +END $$; + +-- Step 3: Create index if not exists +CREATE INDEX IF NOT EXISTS idx_price_alerts_symbol_id +ON trading.price_alerts(symbol_id); + +-- Step 4: Migration script for existing data +-- If there's a legacy 'symbol' varchar column, migrate data to symbol_id +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'trading' + AND table_name = 'price_alerts' + AND column_name = 'symbol' + AND data_type IN ('character varying', 'text') + ) THEN + -- Migrate data from symbol (varchar) to symbol_id (uuid) + UPDATE trading.price_alerts pa + SET symbol_id = s.id + FROM trading.symbols s + WHERE pa.symbol = s.symbol + AND pa.symbol_id IS NULL; + + RAISE NOTICE 'Data migrated from symbol column to symbol_id'; + END IF; +END $$; + +-- Step 5: Verify integrity after migration +DO $$ +DECLARE + orphan_count INTEGER; +BEGIN + SELECT COUNT(*) INTO orphan_count + FROM trading.price_alerts + WHERE symbol_id IS NULL; + + IF orphan_count > 0 THEN + RAISE WARNING 'Found % price_alerts with NULL symbol_id - manual review required', orphan_count; + ELSE + RAISE NOTICE 'All price_alerts have valid symbol_id references'; + END IF; +END $$; + +-- Optional: Make NOT NULL after confirming all data is migrated +-- Uncomment after verifying no orphan records: +-- ALTER TABLE trading.price_alerts ALTER COLUMN symbol_id SET NOT NULL; + +-- ============================================================================ +-- Verification query (run manually to confirm): +-- SELECT +-- COUNT(*) as total_alerts, +-- COUNT(symbol_id) as with_symbol_id, +-- COUNT(*) - COUNT(symbol_id) as missing_symbol_id +-- FROM trading.price_alerts; +-- ============================================================================