-- ============================================================================ -- SCHEMA: ml -- TABLES: prediction_purchases, prediction_outcomes, prediction_packages -- DESCRIPTION: Extensiones para marketplace de predicciones ML -- VERSION: 1.0.0 -- CREATED: 2026-01-10 -- DEPENDS: ml schema (existing), financial.wallets, products.products -- ============================================================================ -- Asume que ml schema ya existe con predictions table -- Enum para estado de outcome DO $$ BEGIN CREATE TYPE ml.outcome_status AS ENUM ( 'pending', -- Esperando resultado 'win', -- Prediccion acertada 'loss', -- Prediccion fallida 'partial', -- Parcialmente correcta 'cancelled', -- Cancelada (mercado cerrado, etc) 'expired' -- Expirada sin resultado ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para tipo de prediccion comprada DO $$ BEGIN CREATE TYPE ml.purchase_source AS ENUM ( 'INDIVIDUAL', -- Compra individual 'SUBSCRIPTION', -- Incluida en suscripcion 'VIP_ACCESS', -- Acceso VIP 'PACKAGE', -- Parte de un paquete 'PROMO' -- Promocional/gratuita ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de paquetes de predicciones (productos vendibles) CREATE TABLE IF NOT EXISTS ml.prediction_packages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificacion name VARCHAR(200) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, description TEXT, -- Configuracion del paquete model_id VARCHAR(100) NOT NULL, model_name VARCHAR(200), prediction_count INT NOT NULL CHECK (prediction_count > 0), -- Simbolos incluidos (NULL = todos disponibles) included_symbols VARCHAR[] DEFAULT NULL, -- Timeframes incluidos included_timeframes VARCHAR[] DEFAULT '{H1,H4,D1}', -- Precios price DECIMAL(10, 2) NOT NULL CHECK (price >= 0), compare_price DECIMAL(10, 2), -- Validez validity_days INT DEFAULT 30, -- Stripe stripe_product_id VARCHAR(255), stripe_price_id VARCHAR(255), -- Display badge VARCHAR(50), is_popular BOOLEAN DEFAULT FALSE, sort_order INT DEFAULT 0, -- Estado is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE ml.prediction_packages IS 'Paquetes de predicciones vendibles (ej: 10 predicciones AMD por $29)'; -- Tabla de compras de predicciones CREATE TABLE IF NOT EXISTS ml.prediction_purchases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, -- Origen de la compra source ml.purchase_source NOT NULL, -- Referencias segun origen package_id UUID REFERENCES ml.prediction_packages(id), product_purchase_id UUID, -- Ref a products.purchases vip_subscription_id UUID, -- Ref a vip.subscriptions -- Modelo y configuracion model_id VARCHAR(100) NOT NULL, model_name VARCHAR(200), -- Creditos de predicciones predictions_total INT NOT NULL CHECK (predictions_total > 0), predictions_used INT NOT NULL DEFAULT 0 CHECK (predictions_used >= 0), predictions_remaining INT GENERATED ALWAYS AS (predictions_total - predictions_used) STORED, -- Pago amount_paid DECIMAL(10, 2) NOT NULL DEFAULT 0, wallet_transaction_id UUID, -- Validez valid_from TIMESTAMPTZ DEFAULT NOW(), valid_until TIMESTAMPTZ, -- Estado is_active BOOLEAN DEFAULT TRUE, -- Metadata metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT valid_predictions CHECK (predictions_used <= predictions_total) ); COMMENT ON TABLE ml.prediction_purchases IS 'Registro de compras/accesos a predicciones por usuario'; -- Tabla de predicciones generadas y sus resultados CREATE TABLE IF NOT EXISTS ml.prediction_outcomes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, -- Referencia a la compra purchase_id UUID NOT NULL REFERENCES ml.prediction_purchases(id), -- Referencia a la prediccion original (si existe en ml.predictions) prediction_id UUID, -- Datos de la prediccion model_id VARCHAR(100) NOT NULL, symbol VARCHAR(20) NOT NULL, timeframe VARCHAR(10) NOT NULL, -- Prediccion predicted_direction VARCHAR(10) NOT NULL, -- BUY, SELL, NEUTRAL predicted_price DECIMAL(20, 8), confidence DECIMAL(5, 4) CHECK (confidence >= 0 AND confidence <= 1), -- Niveles predichos entry_price DECIMAL(20, 8), take_profit DECIMAL(20, 8), stop_loss DECIMAL(20, 8), -- Resultado outcome_status ml.outcome_status NOT NULL DEFAULT 'pending', actual_direction VARCHAR(10), actual_price DECIMAL(20, 8), -- Metricas de resultado pips_result DECIMAL(10, 2), percentage_result DECIMAL(10, 4), hit_tp BOOLEAN, hit_sl BOOLEAN, -- Tiempos predicted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), target_time TIMESTAMPTZ, resolved_at TIMESTAMPTZ, -- Metadata adicional del modelo model_metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON TABLE ml.prediction_outcomes IS 'Historial de predicciones generadas con sus resultados para validacion'; -- Indices para prediction_packages CREATE INDEX IF NOT EXISTS idx_pred_pkg_model ON ml.prediction_packages(model_id); CREATE INDEX IF NOT EXISTS idx_pred_pkg_active ON ml.prediction_packages(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_pred_pkg_slug ON ml.prediction_packages(slug); -- Indices para prediction_purchases CREATE INDEX IF NOT EXISTS idx_pred_purch_tenant ON ml.prediction_purchases(tenant_id); CREATE INDEX IF NOT EXISTS idx_pred_purch_user ON ml.prediction_purchases(user_id); CREATE INDEX IF NOT EXISTS idx_pred_purch_model ON ml.prediction_purchases(model_id); CREATE INDEX IF NOT EXISTS idx_pred_purch_active ON ml.prediction_purchases(is_active, valid_until) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_pred_purch_source ON ml.prediction_purchases(source); -- Indices para prediction_outcomes CREATE INDEX IF NOT EXISTS idx_pred_out_tenant ON ml.prediction_outcomes(tenant_id); CREATE INDEX IF NOT EXISTS idx_pred_out_user ON ml.prediction_outcomes(user_id); CREATE INDEX IF NOT EXISTS idx_pred_out_purchase ON ml.prediction_outcomes(purchase_id); CREATE INDEX IF NOT EXISTS idx_pred_out_status ON ml.prediction_outcomes(outcome_status); CREATE INDEX IF NOT EXISTS idx_pred_out_symbol ON ml.prediction_outcomes(symbol); CREATE INDEX IF NOT EXISTS idx_pred_out_predicted ON ml.prediction_outcomes(predicted_at DESC); CREATE INDEX IF NOT EXISTS idx_pred_out_model ON ml.prediction_outcomes(model_id); -- Trigger updated_at para packages CREATE OR REPLACE FUNCTION ml.update_pkg_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS pred_pkg_updated ON ml.prediction_packages; CREATE TRIGGER pred_pkg_updated BEFORE UPDATE ON ml.prediction_packages FOR EACH ROW EXECUTE FUNCTION ml.update_pkg_timestamp(); -- Funcion para consumir una prediccion CREATE OR REPLACE FUNCTION ml.consume_prediction( p_purchase_id UUID, p_symbol VARCHAR(20), p_timeframe VARCHAR(10), p_direction VARCHAR(10), p_confidence DECIMAL(5, 4), p_entry DECIMAL(20, 8) DEFAULT NULL, p_tp DECIMAL(20, 8) DEFAULT NULL, p_sl DECIMAL(20, 8) DEFAULT NULL, p_target_time TIMESTAMPTZ DEFAULT NULL, p_metadata JSONB DEFAULT '{}' ) RETURNS UUID AS $$ DECLARE v_purchase ml.prediction_purchases%ROWTYPE; v_outcome_id UUID; BEGIN -- Lock and get purchase SELECT * INTO v_purchase FROM ml.prediction_purchases WHERE id = p_purchase_id FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'Purchase not found: %', p_purchase_id; END IF; IF NOT v_purchase.is_active THEN RAISE EXCEPTION 'Purchase is not active'; END IF; IF v_purchase.valid_until IS NOT NULL AND v_purchase.valid_until < NOW() THEN RAISE EXCEPTION 'Purchase has expired'; END IF; IF v_purchase.predictions_used >= v_purchase.predictions_total THEN RAISE EXCEPTION 'No predictions remaining in this purchase'; END IF; -- Create outcome record INSERT INTO ml.prediction_outcomes ( tenant_id, user_id, purchase_id, model_id, symbol, timeframe, predicted_direction, confidence, entry_price, take_profit, stop_loss, target_time, model_metadata ) VALUES ( v_purchase.tenant_id, v_purchase.user_id, p_purchase_id, v_purchase.model_id, p_symbol, p_timeframe, p_direction, p_confidence, p_entry, p_tp, p_sl, p_target_time, p_metadata ) RETURNING id INTO v_outcome_id; -- Update purchase counter UPDATE ml.prediction_purchases SET predictions_used = predictions_used + 1 WHERE id = p_purchase_id; RETURN v_outcome_id; END; $$ LANGUAGE plpgsql; -- Funcion para resolver outcome CREATE OR REPLACE FUNCTION ml.resolve_prediction_outcome( p_outcome_id UUID, p_status ml.outcome_status, p_actual_direction VARCHAR(10) DEFAULT NULL, p_actual_price DECIMAL(20, 8) DEFAULT NULL, p_pips DECIMAL(10, 2) DEFAULT NULL, p_percentage DECIMAL(10, 4) DEFAULT NULL, p_hit_tp BOOLEAN DEFAULT NULL, p_hit_sl BOOLEAN DEFAULT NULL ) RETURNS VOID AS $$ BEGIN UPDATE ml.prediction_outcomes SET outcome_status = p_status, actual_direction = p_actual_direction, actual_price = p_actual_price, pips_result = p_pips, percentage_result = p_percentage, hit_tp = p_hit_tp, hit_sl = p_hit_sl, resolved_at = NOW() WHERE id = p_outcome_id; END; $$ LANGUAGE plpgsql; -- Funcion para obtener estadisticas de predicciones por usuario CREATE OR REPLACE FUNCTION ml.get_user_prediction_stats( p_user_id UUID, p_model_id VARCHAR(100) DEFAULT NULL ) RETURNS TABLE ( total_predictions BIGINT, wins BIGINT, losses BIGINT, pending BIGINT, win_rate DECIMAL(5, 2), avg_pips DECIMAL(10, 2), avg_confidence DECIMAL(5, 4) ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*)::BIGINT as total_predictions, COUNT(*) FILTER (WHERE outcome_status = 'win')::BIGINT as wins, COUNT(*) FILTER (WHERE outcome_status = 'loss')::BIGINT as losses, COUNT(*) FILTER (WHERE outcome_status = 'pending')::BIGINT as pending, CASE WHEN COUNT(*) FILTER (WHERE outcome_status IN ('win', 'loss')) > 0 THEN ROUND( COUNT(*) FILTER (WHERE outcome_status = 'win')::DECIMAL * 100 / COUNT(*) FILTER (WHERE outcome_status IN ('win', 'loss')), 2 ) ELSE 0 END as win_rate, ROUND(AVG(pips_result) FILTER (WHERE pips_result IS NOT NULL), 2) as avg_pips, ROUND(AVG(confidence), 4) as avg_confidence FROM ml.prediction_outcomes WHERE user_id = p_user_id AND (p_model_id IS NULL OR model_id = p_model_id); END; $$ LANGUAGE plpgsql; -- RLS ALTER TABLE ml.prediction_packages ENABLE ROW LEVEL SECURITY; ALTER TABLE ml.prediction_purchases ENABLE ROW LEVEL SECURITY; ALTER TABLE ml.prediction_outcomes ENABLE ROW LEVEL SECURITY; -- Packages visibles para todos CREATE POLICY pred_pkg_read_all ON ml.prediction_packages FOR SELECT USING (TRUE); -- Purchases por tenant CREATE POLICY pred_purch_tenant ON ml.prediction_purchases FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Outcomes por tenant CREATE POLICY pred_out_tenant ON ml.prediction_outcomes FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT ON ml.prediction_packages TO trading_app; GRANT SELECT, INSERT, UPDATE ON ml.prediction_purchases TO trading_app; GRANT SELECT, INSERT, UPDATE ON ml.prediction_outcomes TO trading_app; GRANT SELECT ON ml.prediction_packages TO trading_readonly; GRANT SELECT ON ml.prediction_purchases TO trading_readonly; GRANT SELECT ON ml.prediction_outcomes TO trading_readonly;