trading-platform-database-v2/ddl/schemas/ml/001_predictions_marketplace.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

366 lines
12 KiB
PL/PgSQL

-- ============================================================================
-- 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;