366 lines
12 KiB
PL/PgSQL
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;
|