-- ============================================================================ -- SCHEMA: trading -- TABLE: signals -- DESCRIPTION: Senales de trading generadas por ML/bots -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- Enum para tipo de senal DO $$ BEGIN CREATE TYPE trading.signal_type AS ENUM ( 'entry', -- Senal de entrada 'exit', -- Senal de salida 'scale_in', -- Agregar a posicion 'scale_out', -- Reducir posicion 'move_sl', -- Mover stop loss 'move_tp', -- Mover take profit 'alert' -- Alerta informativa ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para estado de senal DO $$ BEGIN CREATE TYPE trading.signal_status AS ENUM ( 'pending', -- Pendiente de validacion 'active', -- Activa y disponible 'executed', -- Ejecutada 'expired', -- Expirada sin ejecutar 'cancelled', -- Cancelada 'invalidated' -- Invalidada por condiciones ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para fuente de senal DO $$ BEGIN CREATE TYPE trading.signal_source AS ENUM ( 'ml_model', -- Modelo ML 'technical', -- Analisis tecnico 'fundamental', -- Analisis fundamental 'sentiment', -- Analisis de sentimiento 'manual', -- Analista manual 'bot', -- Bot de trading 'copy_trade' -- Copy trading ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de Senales de Trading CREATE TABLE IF NOT EXISTS trading.signals ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, bot_id UUID REFERENCES trading.bots(id), strategy_id UUID REFERENCES trading.strategies(id), -- Clasificacion type trading.signal_type NOT NULL DEFAULT 'entry', source trading.signal_source NOT NULL DEFAULT 'ml_model', status trading.signal_status NOT NULL DEFAULT 'pending', -- Instrumento symbol_id UUID REFERENCES trading.symbols(id), symbol VARCHAR(20) NOT NULL, timeframe trading.timeframe DEFAULT 'H1', -- Direccion y precios direction trading.trade_direction NOT NULL, entry_price DECIMAL(15, 8) NOT NULL, stop_loss DECIMAL(15, 8), take_profit DECIMAL(15, 8), take_profit_2 DECIMAL(15, 8), -- TP secundario take_profit_3 DECIMAL(15, 8), -- TP terciario -- Risk/Reward risk_pips DECIMAL(10, 4), reward_pips DECIMAL(10, 4), risk_reward_ratio DECIMAL(10, 4), -- Confianza y scoring confidence DECIMAL(5, 2) NOT NULL DEFAULT 50 -- 0-100 CHECK (confidence BETWEEN 0 AND 100), strength VARCHAR(20), -- 'weak', 'moderate', 'strong', 'very_strong' quality_score DECIMAL(5, 2), -- Modelo ML (si aplica) model_id VARCHAR(100), model_version VARCHAR(20), model_prediction JSONB, -- Prediccion completa del modelo -- Analisis tecnico technical_analysis JSONB DEFAULT '{}'::JSONB, -- Indicadores al momento de la senal pattern_detected VARCHAR(100), -- Patron identificado key_levels JSONB, -- Niveles clave S/R -- Contexto de mercado market_context JSONB DEFAULT '{}'::JSONB, -- Condiciones de mercado trend_direction VARCHAR(20), volatility_level VARCHAR(20), -- Validez valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(), valid_until TIMESTAMPTZ, -- NULL = sin expiracion invalidation_price DECIMAL(15, 8), -- Precio que invalida la senal -- Ejecucion executed_at TIMESTAMPTZ, executed_price DECIMAL(15, 8), execution_slippage DECIMAL(10, 4), position_id UUID, -- Posicion resultante -- Resultado result VARCHAR(20), -- 'win', 'loss', 'breakeven', 'partial' result_pips DECIMAL(10, 4), result_amount DECIMAL(15, 4), -- Notificaciones notification_sent BOOLEAN NOT NULL DEFAULT FALSE, notification_sent_at TIMESTAMPTZ, notification_channels JSONB DEFAULT '[]'::JSONB, -- Usuarios que vieron/ejecutaron view_count INTEGER NOT NULL DEFAULT 0, execution_count INTEGER NOT NULL DEFAULT 0, -- Metadata tags VARCHAR(50)[], notes TEXT, metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE trading.signals IS 'Senales de trading generadas por modelos ML, bots o analistas'; COMMENT ON COLUMN trading.signals.confidence IS 'Nivel de confianza de la senal (0-100%)'; COMMENT ON COLUMN trading.signals.invalidation_price IS 'Precio que invalida la senal antes de ser ejecutada'; -- Indices CREATE INDEX IF NOT EXISTS idx_signals_tenant ON trading.signals(tenant_id); CREATE INDEX IF NOT EXISTS idx_signals_bot ON trading.signals(bot_id); CREATE INDEX IF NOT EXISTS idx_signals_symbol ON trading.signals(symbol, generated_at DESC); CREATE INDEX IF NOT EXISTS idx_signals_status ON trading.signals(status); CREATE INDEX IF NOT EXISTS idx_signals_active ON trading.signals(tenant_id, status, generated_at DESC) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_signals_type_direction ON trading.signals(type, direction); CREATE INDEX IF NOT EXISTS idx_signals_confidence ON trading.signals(confidence DESC) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_signals_valid_until ON trading.signals(valid_until) WHERE valid_until IS NOT NULL AND status = 'active'; CREATE INDEX IF NOT EXISTS idx_signals_generated ON trading.signals(generated_at DESC); -- GIN index para technical_analysis CREATE INDEX IF NOT EXISTS idx_signals_ta_gin ON trading.signals USING GIN (technical_analysis); -- Trigger para updated_at DROP TRIGGER IF EXISTS signal_updated_at ON trading.signals; CREATE TRIGGER signal_updated_at BEFORE UPDATE ON trading.signals FOR EACH ROW EXECUTE FUNCTION trading.update_trading_timestamp(); -- Trigger para calcular risk/reward CREATE OR REPLACE FUNCTION trading.calculate_signal_rr() RETURNS TRIGGER AS $$ BEGIN IF NEW.entry_price IS NOT NULL AND NEW.stop_loss IS NOT NULL THEN IF NEW.direction = 'long' THEN NEW.risk_pips := (NEW.entry_price - NEW.stop_loss) * 10000; -- Para forex IF NEW.take_profit IS NOT NULL THEN NEW.reward_pips := (NEW.take_profit - NEW.entry_price) * 10000; END IF; ELSE NEW.risk_pips := (NEW.stop_loss - NEW.entry_price) * 10000; IF NEW.take_profit IS NOT NULL THEN NEW.reward_pips := (NEW.entry_price - NEW.take_profit) * 10000; END IF; END IF; IF NEW.risk_pips > 0 AND NEW.reward_pips IS NOT NULL THEN NEW.risk_reward_ratio := NEW.reward_pips / NEW.risk_pips; END IF; END IF; -- Determinar strength basado en confidence NEW.strength := CASE WHEN NEW.confidence >= 85 THEN 'very_strong' WHEN NEW.confidence >= 70 THEN 'strong' WHEN NEW.confidence >= 55 THEN 'moderate' ELSE 'weak' END; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS signal_calc_rr ON trading.signals; CREATE TRIGGER signal_calc_rr BEFORE INSERT OR UPDATE OF entry_price, stop_loss, take_profit, confidence ON trading.signals FOR EACH ROW EXECUTE FUNCTION trading.calculate_signal_rr(); -- Trigger para expirar senales CREATE OR REPLACE FUNCTION trading.expire_old_signals() RETURNS INTEGER AS $$ DECLARE v_count INTEGER; BEGIN UPDATE trading.signals SET status = 'expired' WHERE status = 'active' AND valid_until IS NOT NULL AND valid_until < NOW(); GET DIAGNOSTICS v_count = ROW_COUNT; RETURN v_count; END; $$ LANGUAGE plpgsql; -- Funcion para actualizar estadisticas del bot cuando se procesa senal CREATE OR REPLACE FUNCTION trading.update_bot_on_signal() RETURNS TRIGGER AS $$ BEGIN IF NEW.bot_id IS NOT NULL THEN UPDATE trading.bots SET last_signal_at = NOW() WHERE id = NEW.bot_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS signal_bot_update ON trading.signals; CREATE TRIGGER signal_bot_update AFTER INSERT ON trading.signals FOR EACH ROW EXECUTE FUNCTION trading.update_bot_on_signal(); -- Vista de senales activas CREATE OR REPLACE VIEW trading.v_active_signals AS SELECT s.id, s.tenant_id, s.bot_id, b.name AS bot_name, s.symbol, s.timeframe, s.type, s.direction, s.entry_price, s.stop_loss, s.take_profit, s.risk_reward_ratio, s.confidence, s.strength, s.pattern_detected, s.valid_until, s.generated_at FROM trading.signals s LEFT JOIN trading.bots b ON s.bot_id = b.id WHERE s.status = 'active' AND (s.valid_until IS NULL OR s.valid_until > NOW()) ORDER BY s.confidence DESC, s.generated_at DESC; -- Vista de rendimiento de senales por simbolo CREATE OR REPLACE VIEW trading.v_signal_performance AS SELECT symbol, COUNT(*) AS total_signals, COUNT(*) FILTER (WHERE result = 'win') AS wins, COUNT(*) FILTER (WHERE result = 'loss') AS losses, ROUND(AVG(confidence)::NUMERIC, 2) AS avg_confidence, ROUND(AVG(risk_reward_ratio)::NUMERIC, 2) AS avg_rr, ROUND((COUNT(*) FILTER (WHERE result = 'win')::DECIMAL / NULLIF(COUNT(*) FILTER (WHERE result IN ('win', 'loss')), 0) * 100)::NUMERIC, 2) AS win_rate FROM trading.signals WHERE status IN ('executed', 'expired') AND result IS NOT NULL GROUP BY symbol ORDER BY win_rate DESC NULLS LAST; -- RLS Policy para multi-tenancy ALTER TABLE trading.signals ENABLE ROW LEVEL SECURITY; CREATE POLICY signals_tenant_isolation ON trading.signals FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT, UPDATE ON trading.signals TO trading_app; GRANT SELECT ON trading.signals TO trading_readonly; GRANT SELECT ON trading.v_active_signals TO trading_app; GRANT SELECT ON trading.v_signal_performance TO trading_app; GRANT EXECUTE ON FUNCTION trading.expire_old_signals TO trading_app;