324 lines
10 KiB
PL/PgSQL
324 lines
10 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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;
|