- data_sources schema: - api_providers: Proveedores de datos de mercado - ticker_mapping: Mapeo de simbolos entre proveedores - data_sync_status: Estado de sincronizacion de datos - ml schema additions: - range_predictions: Predicciones de rango (particionada) - entry_signals: Señales de entrada con metodologias ICT/SMC/AMD - market_analysis: Analisis de mercado (sesgo, estructura, volatilidad) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
409 lines
14 KiB
PL/PgSQL
409 lines
14 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: ml
|
|
-- TABLE: entry_signals
|
|
-- DESCRIPTION: Senales de entrada generadas por modelos ML
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 4 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para metodologia de senal
|
|
DO $$ BEGIN
|
|
CREATE TYPE ml.signal_methodology AS ENUM (
|
|
'amd', -- Accumulation, Manipulation, Distribution
|
|
'ict_smc', -- ICT Smart Money Concepts
|
|
'order_blocks', -- Order Blocks
|
|
'fair_value_gap', -- Fair Value Gaps
|
|
'liquidity_sweep', -- Liquidity Sweeps
|
|
'break_of_structure', -- Break of Structure
|
|
'supply_demand', -- Supply & Demand zones
|
|
'technical', -- Indicadores tecnicos clasicos
|
|
'pattern', -- Patrones de precio
|
|
'ensemble' -- Combinacion de metodologias
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para fase AMD
|
|
DO $$ BEGIN
|
|
CREATE TYPE ml.amd_phase AS ENUM (
|
|
'accumulation', -- Fase de acumulacion
|
|
'manipulation', -- Fase de manipulacion
|
|
'distribution', -- Fase de distribucion
|
|
'expansion', -- Fase de expansion
|
|
'retracement', -- Fase de retroceso
|
|
'consolidation', -- Consolidacion
|
|
'unknown' -- Fase no determinada
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para estado de senal de entrada
|
|
DO $$ BEGIN
|
|
CREATE TYPE ml.entry_signal_status AS ENUM (
|
|
'generated', -- Generada por modelo
|
|
'pending', -- Pendiente de activacion
|
|
'active', -- Activa (precio llego a zona)
|
|
'triggered', -- Disparada (orden ejecutada)
|
|
'expired', -- Expirada sin activar
|
|
'invalidated', -- Invalidada por condiciones
|
|
'cancelled' -- Cancelada
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de Senales de Entrada ML
|
|
CREATE TABLE IF NOT EXISTS ml.entry_signals (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
user_id UUID, -- NULL si es senal publica
|
|
|
|
-- Referencias
|
|
range_prediction_id UUID, -- Prediccion de rango asociada
|
|
ticker_id UUID REFERENCES market_data.tickers(id),
|
|
|
|
-- Simbolo y timeframe
|
|
symbol VARCHAR(20) NOT NULL,
|
|
asset_class ml.asset_class NOT NULL DEFAULT 'FOREX',
|
|
timeframe VARCHAR(10) NOT NULL DEFAULT 'H1',
|
|
session VARCHAR(20), -- 'asia', 'london', 'ny'
|
|
|
|
-- Metodologia
|
|
methodology ml.signal_methodology NOT NULL,
|
|
amd_phase ml.amd_phase,
|
|
|
|
-- Direccion
|
|
direction VARCHAR(10) NOT NULL CHECK (direction IN ('LONG', 'SHORT')),
|
|
|
|
-- Estado
|
|
status ml.entry_signal_status NOT NULL DEFAULT 'generated',
|
|
|
|
-- Zona de entrada
|
|
entry_zone_high DECIMAL(15, 8) NOT NULL, -- Limite superior de entrada
|
|
entry_zone_low DECIMAL(15, 8) NOT NULL, -- Limite inferior de entrada
|
|
optimal_entry DECIMAL(15, 8), -- Precio optimo de entrada
|
|
|
|
-- Stop Loss
|
|
stop_loss DECIMAL(15, 8) NOT NULL,
|
|
stop_loss_pips DECIMAL(10, 2),
|
|
invalidation_price DECIMAL(15, 8), -- Precio que invalida la senal
|
|
|
|
-- Take Profits
|
|
take_profit_1 DECIMAL(15, 8),
|
|
take_profit_2 DECIMAL(15, 8),
|
|
take_profit_3 DECIMAL(15, 8),
|
|
tp1_pips DECIMAL(10, 2),
|
|
tp2_pips DECIMAL(10, 2),
|
|
tp3_pips DECIMAL(10, 2),
|
|
|
|
-- Risk/Reward
|
|
risk_reward_1 DECIMAL(5, 2), -- RR a TP1
|
|
risk_reward_2 DECIMAL(5, 2), -- RR a TP2
|
|
risk_reward_3 DECIMAL(5, 2), -- RR a TP3
|
|
|
|
-- Confianza
|
|
confidence_score DECIMAL(5, 4) NOT NULL DEFAULT 0.5
|
|
CHECK (confidence_score BETWEEN 0 AND 1),
|
|
strength VARCHAR(20), -- 'weak', 'moderate', 'strong', 'very_strong'
|
|
quality_grade VARCHAR(2), -- 'A+', 'A', 'B', 'C', 'D'
|
|
|
|
-- Modelo
|
|
model_id VARCHAR(100),
|
|
model_version VARCHAR(50) NOT NULL,
|
|
|
|
-- Contexto tecnico
|
|
market_structure JSONB DEFAULT '{}'::JSONB, -- BOS, CHoCH, etc.
|
|
key_levels JSONB DEFAULT '[]'::JSONB, -- Niveles clave cercanos
|
|
order_blocks JSONB DEFAULT '[]'::JSONB, -- Order blocks detectados
|
|
fair_value_gaps JSONB DEFAULT '[]'::JSONB, -- FVGs detectados
|
|
liquidity_pools JSONB DEFAULT '[]'::JSONB, -- Pools de liquidez
|
|
|
|
-- Indicadores al momento
|
|
rsi_14 DECIMAL(5, 2),
|
|
macd_histogram DECIMAL(15, 8),
|
|
atr_14 DECIMAL(15, 8),
|
|
volume_ratio DECIMAL(5, 2),
|
|
|
|
-- Contexto de mercado
|
|
market_bias VARCHAR(20), -- 'bullish', 'bearish', 'neutral'
|
|
volatility_regime VARCHAR(20), -- 'low', 'medium', 'high', 'extreme'
|
|
trend_direction VARCHAR(20), -- 'up', 'down', 'ranging'
|
|
session_bias VARCHAR(20), -- Sesgo de la sesion
|
|
|
|
-- Validez
|
|
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
valid_until TIMESTAMPTZ NOT NULL,
|
|
|
|
-- Ejecucion
|
|
activated_at TIMESTAMPTZ, -- Cuando precio llego a zona
|
|
triggered_at TIMESTAMPTZ, -- Cuando se ejecuto
|
|
triggered_price DECIMAL(15, 8),
|
|
slippage DECIMAL(10, 4),
|
|
|
|
-- Resultado
|
|
result VARCHAR(20), -- 'win', 'loss', 'breakeven', 'partial'
|
|
exit_price DECIMAL(15, 8),
|
|
exit_reason VARCHAR(50),
|
|
pnl_pips DECIMAL(10, 2),
|
|
pnl_rr DECIMAL(5, 2), -- Resultado en multiplos de RR
|
|
|
|
-- Trazabilidad
|
|
position_id UUID, -- Posicion resultante
|
|
|
|
-- Features del modelo
|
|
features JSONB DEFAULT '{}'::JSONB,
|
|
explanation TEXT, -- Explicacion de la senal
|
|
|
|
-- Notificacion
|
|
notified_at TIMESTAMPTZ,
|
|
notification_channels VARCHAR(20)[],
|
|
|
|
-- 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(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT entry_signals_zone_valid CHECK (entry_zone_high >= entry_zone_low)
|
|
);
|
|
|
|
COMMENT ON TABLE ml.entry_signals IS
|
|
'Senales de entrada generadas por modelos ML con metodologias ICT/SMC/AMD';
|
|
|
|
COMMENT ON COLUMN ml.entry_signals.amd_phase IS
|
|
'Fase AMD detectada: Accumulation, Manipulation, Distribution';
|
|
|
|
COMMENT ON COLUMN ml.entry_signals.quality_grade IS
|
|
'Calificacion de calidad de la senal: A+, A, B, C, D';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_tenant
|
|
ON ml.entry_signals(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_user
|
|
ON ml.entry_signals(user_id)
|
|
WHERE user_id IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_symbol
|
|
ON ml.entry_signals(symbol, generated_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_status
|
|
ON ml.entry_signals(status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_active
|
|
ON ml.entry_signals(status, valid_until)
|
|
WHERE status IN ('pending', 'active');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_methodology
|
|
ON ml.entry_signals(methodology, direction);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_amd
|
|
ON ml.entry_signals(amd_phase, generated_at DESC)
|
|
WHERE amd_phase IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_confidence
|
|
ON ml.entry_signals(confidence_score DESC)
|
|
WHERE status IN ('pending', 'active');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_generated
|
|
ON ml.entry_signals(generated_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_result
|
|
ON ml.entry_signals(result, triggered_at DESC)
|
|
WHERE result IS NOT NULL;
|
|
|
|
-- GIN indices para JSONB
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_keys_gin
|
|
ON ml.entry_signals USING GIN (key_levels);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_signals_ob_gin
|
|
ON ml.entry_signals USING GIN (order_blocks);
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS entry_signal_updated_at ON ml.entry_signals;
|
|
CREATE TRIGGER entry_signal_updated_at
|
|
BEFORE UPDATE ON ml.entry_signals
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION ml.update_ml_timestamp();
|
|
|
|
-- Trigger para calcular strength basado en confidence
|
|
CREATE OR REPLACE FUNCTION ml.calculate_signal_strength()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.strength := CASE
|
|
WHEN NEW.confidence_score >= 0.85 THEN 'very_strong'
|
|
WHEN NEW.confidence_score >= 0.70 THEN 'strong'
|
|
WHEN NEW.confidence_score >= 0.55 THEN 'moderate'
|
|
ELSE 'weak'
|
|
END;
|
|
|
|
-- Calcular pips si hay precios
|
|
IF NEW.optimal_entry IS NOT NULL AND NEW.stop_loss IS NOT NULL THEN
|
|
NEW.stop_loss_pips := ABS(NEW.optimal_entry - NEW.stop_loss) * 10000;
|
|
|
|
IF NEW.take_profit_1 IS NOT NULL THEN
|
|
NEW.tp1_pips := ABS(NEW.take_profit_1 - NEW.optimal_entry) * 10000;
|
|
NEW.risk_reward_1 := NEW.tp1_pips / NULLIF(NEW.stop_loss_pips, 0);
|
|
END IF;
|
|
|
|
IF NEW.take_profit_2 IS NOT NULL THEN
|
|
NEW.tp2_pips := ABS(NEW.take_profit_2 - NEW.optimal_entry) * 10000;
|
|
NEW.risk_reward_2 := NEW.tp2_pips / NULLIF(NEW.stop_loss_pips, 0);
|
|
END IF;
|
|
|
|
IF NEW.take_profit_3 IS NOT NULL THEN
|
|
NEW.tp3_pips := ABS(NEW.take_profit_3 - NEW.optimal_entry) * 10000;
|
|
NEW.risk_reward_3 := NEW.tp3_pips / NULLIF(NEW.stop_loss_pips, 0);
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS entry_signal_calc_strength ON ml.entry_signals;
|
|
CREATE TRIGGER entry_signal_calc_strength
|
|
BEFORE INSERT OR UPDATE OF confidence_score, optimal_entry, stop_loss, take_profit_1, take_profit_2, take_profit_3
|
|
ON ml.entry_signals
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION ml.calculate_signal_strength();
|
|
|
|
-- Funcion para activar senal (precio llego a zona)
|
|
CREATE OR REPLACE FUNCTION ml.activate_entry_signal(
|
|
p_signal_id UUID,
|
|
p_current_price DECIMAL
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_signal ml.entry_signals;
|
|
BEGIN
|
|
SELECT * INTO v_signal FROM ml.entry_signals WHERE id = p_signal_id;
|
|
|
|
IF NOT FOUND OR v_signal.status != 'pending' THEN
|
|
RETURN FALSE;
|
|
END IF;
|
|
|
|
-- Verificar si precio esta en zona
|
|
IF p_current_price BETWEEN v_signal.entry_zone_low AND v_signal.entry_zone_high THEN
|
|
UPDATE ml.entry_signals
|
|
SET status = 'active',
|
|
activated_at = NOW()
|
|
WHERE id = p_signal_id;
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
RETURN FALSE;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para registrar resultado de senal
|
|
CREATE OR REPLACE FUNCTION ml.record_signal_result(
|
|
p_signal_id UUID,
|
|
p_result VARCHAR,
|
|
p_exit_price DECIMAL,
|
|
p_exit_reason VARCHAR DEFAULT 'manual',
|
|
p_position_id UUID DEFAULT NULL
|
|
)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_signal ml.entry_signals;
|
|
BEGIN
|
|
SELECT * INTO v_signal FROM ml.entry_signals WHERE id = p_signal_id;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
UPDATE ml.entry_signals
|
|
SET result = p_result,
|
|
exit_price = p_exit_price,
|
|
exit_reason = p_exit_reason,
|
|
position_id = p_position_id,
|
|
pnl_pips = CASE v_signal.direction
|
|
WHEN 'LONG' THEN (p_exit_price - v_signal.triggered_price) * 10000
|
|
ELSE (v_signal.triggered_price - p_exit_price) * 10000
|
|
END,
|
|
pnl_rr = CASE
|
|
WHEN v_signal.stop_loss_pips > 0 THEN
|
|
CASE v_signal.direction
|
|
WHEN 'LONG' THEN (p_exit_price - v_signal.triggered_price) * 10000 / v_signal.stop_loss_pips
|
|
ELSE (v_signal.triggered_price - p_exit_price) * 10000 / v_signal.stop_loss_pips
|
|
END
|
|
ELSE NULL
|
|
END
|
|
WHERE id = p_signal_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de senales activas
|
|
CREATE OR REPLACE VIEW ml.v_active_entry_signals AS
|
|
SELECT
|
|
id,
|
|
tenant_id,
|
|
symbol,
|
|
direction,
|
|
methodology,
|
|
amd_phase,
|
|
status,
|
|
entry_zone_high,
|
|
entry_zone_low,
|
|
optimal_entry,
|
|
stop_loss,
|
|
take_profit_1,
|
|
risk_reward_1,
|
|
confidence_score,
|
|
strength,
|
|
quality_grade,
|
|
market_bias,
|
|
volatility_regime,
|
|
valid_until,
|
|
generated_at
|
|
FROM ml.entry_signals
|
|
WHERE status IN ('pending', 'active')
|
|
AND valid_until > NOW()
|
|
ORDER BY confidence_score DESC, generated_at DESC;
|
|
|
|
-- Vista de rendimiento por metodologia
|
|
CREATE OR REPLACE VIEW ml.v_signal_methodology_performance AS
|
|
SELECT
|
|
methodology,
|
|
amd_phase,
|
|
direction,
|
|
COUNT(*) AS total_signals,
|
|
COUNT(*) FILTER (WHERE result = 'win') AS wins,
|
|
COUNT(*) FILTER (WHERE result = 'loss') AS losses,
|
|
COUNT(*) FILTER (WHERE result = 'breakeven') AS breakeven,
|
|
ROUND((COUNT(*) FILTER (WHERE result = 'win')::DECIMAL
|
|
/ NULLIF(COUNT(*) FILTER (WHERE result IS NOT NULL), 0) * 100), 2) AS win_rate,
|
|
ROUND(AVG(confidence_score)::NUMERIC, 4) AS avg_confidence,
|
|
ROUND(AVG(pnl_rr) FILTER (WHERE pnl_rr IS NOT NULL)::NUMERIC, 2) AS avg_rr,
|
|
ROUND(SUM(pnl_pips) FILTER (WHERE pnl_pips IS NOT NULL)::NUMERIC, 2) AS total_pips
|
|
FROM ml.entry_signals
|
|
WHERE result IS NOT NULL
|
|
GROUP BY methodology, amd_phase, direction
|
|
ORDER BY win_rate DESC NULLS LAST;
|
|
|
|
-- RLS Policies
|
|
ALTER TABLE ml.entry_signals ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY entry_signals_tenant ON ml.entry_signals
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON ml.entry_signals TO trading_app;
|
|
GRANT SELECT ON ml.entry_signals TO trading_readonly;
|
|
GRANT SELECT ON ml.v_active_entry_signals TO trading_app;
|
|
GRANT SELECT ON ml.v_signal_methodology_performance TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION ml.activate_entry_signal TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION ml.record_signal_result TO trading_app;
|