217 lines
6.2 KiB
PL/PgSQL
217 lines
6.2 KiB
PL/PgSQL
-- ============================================================
|
|
-- Migration 012: Atomic ENTRY Lifecycle Persistence
|
|
-- Date: 2026-02-16
|
|
-- Purpose:
|
|
-- 1) Add explicit trade_lifecycle and positions tables for traceability.
|
|
-- 2) Harden orders/trade_history uniqueness to drive idempotent RPC inserts.
|
|
-- 3) Expose fn_persist_entry_lifecycle for exchange-first persistence.
|
|
-- ============================================================
|
|
|
|
BEGIN;
|
|
|
|
-- --------------------------------------------------------------------------------
|
|
-- Ensure lifecycle table captures profile + trade scope
|
|
-- --------------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS trade_lifecycle (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
profile_id uuid NOT NULL REFERENCES trade_profiles(id) ON DELETE CASCADE,
|
|
trade_id text NOT NULL,
|
|
entry_order_id text NOT NULL,
|
|
current_stage text NOT NULL DEFAULT 'ENTRY',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT trade_lifecycle_profile_trade_unique UNIQUE (profile_id, trade_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_trade_lifecycle_entry_order
|
|
ON trade_lifecycle(entry_order_id);
|
|
|
|
-- --------------------------------------------------------------------------------
|
|
-- Positions table for lifecycle-driven snapshots
|
|
-- --------------------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS positions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
profile_id uuid NOT NULL,
|
|
trade_id text NOT NULL,
|
|
entry_order_id text NOT NULL,
|
|
symbol text NOT NULL,
|
|
side text NOT NULL,
|
|
quantity numeric NOT NULL DEFAULT 0,
|
|
avg_price numeric NOT NULL DEFAULT 0,
|
|
status text NOT NULL DEFAULT 'OPEN',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT positions_profile_trade_unique UNIQUE (profile_id, trade_id),
|
|
CONSTRAINT positions_lifecycle_fk FOREIGN KEY (profile_id, trade_id)
|
|
REFERENCES trade_lifecycle(profile_id, trade_id) ON DELETE CASCADE,
|
|
CONSTRAINT chk_positions_side CHECK (side IN ('BUY','SELL'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_positions_profile_trade
|
|
ON positions (profile_id, trade_id);
|
|
CREATE INDEX IF NOT EXISTS idx_positions_symbol ON positions (symbol);
|
|
|
|
-- --------------------------------------------------------------------------------
|
|
-- Harden orders + trade_history uniqueness for idempotent child inserts
|
|
-- --------------------------------------------------------------------------------
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_orders_order_id ON orders (order_id) WHERE order_id IS NOT NULL;
|
|
|
|
ALTER TABLE IF EXISTS trade_history
|
|
ADD COLUMN IF NOT EXISTS lifecycle_marker text;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_trade_history_lifecycle_marker
|
|
ON trade_history (lifecycle_marker)
|
|
WHERE lifecycle_marker IS NOT NULL;
|
|
|
|
COMMIT;
|
|
|
|
-- --------------------------------------------------------------------------------
|
|
-- Entry persistence RPC
|
|
-- --------------------------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION fn_persist_entry_lifecycle(
|
|
p_profile_id uuid,
|
|
p_trade_id text,
|
|
p_order_id text,
|
|
p_symbol text,
|
|
p_side text,
|
|
p_type text,
|
|
p_quantity numeric,
|
|
p_price numeric,
|
|
p_status text,
|
|
p_timestamp bigint,
|
|
p_stop_loss numeric DEFAULT NULL,
|
|
p_take_profit numeric DEFAULT NULL,
|
|
p_source text DEFAULT 'BOT',
|
|
p_reason text DEFAULT 'Entry lifecycle persisted'
|
|
) RETURNS jsonb AS $$
|
|
DECLARE
|
|
lifecycle_row trade_lifecycle%ROWTYPE;
|
|
entry_marker text := format('entry:%s', p_trade_id);
|
|
BEGIN
|
|
BEGIN
|
|
INSERT INTO trade_lifecycle (
|
|
profile_id,
|
|
trade_id,
|
|
entry_order_id,
|
|
current_stage,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (
|
|
p_profile_id,
|
|
p_trade_id,
|
|
p_order_id,
|
|
'ENTRY',
|
|
now(),
|
|
now()
|
|
)
|
|
RETURNING * INTO lifecycle_row;
|
|
EXCEPTION WHEN unique_violation THEN
|
|
UPDATE trade_lifecycle
|
|
SET updated_at = now()
|
|
WHERE profile_id = p_profile_id
|
|
AND trade_id = p_trade_id;
|
|
END;
|
|
|
|
INSERT INTO orders (
|
|
order_id,
|
|
profile_id,
|
|
trade_id,
|
|
symbol,
|
|
type,
|
|
side,
|
|
qty,
|
|
quantity,
|
|
price,
|
|
status,
|
|
timestamp,
|
|
stop_loss,
|
|
take_profit,
|
|
action,
|
|
source
|
|
) VALUES (
|
|
p_order_id,
|
|
p_profile_id,
|
|
p_trade_id,
|
|
p_symbol,
|
|
p_type,
|
|
upper(p_side),
|
|
COALESCE(p_quantity, 0),
|
|
COALESCE(p_quantity, 0),
|
|
COALESCE(p_price, 0),
|
|
p_status,
|
|
p_timestamp,
|
|
p_stop_loss,
|
|
p_take_profit,
|
|
'ENTRY',
|
|
p_source
|
|
) ON CONFLICT (order_id) DO NOTHING;
|
|
|
|
INSERT INTO positions (
|
|
profile_id,
|
|
trade_id,
|
|
entry_order_id,
|
|
symbol,
|
|
side,
|
|
quantity,
|
|
avg_price,
|
|
status,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (
|
|
p_profile_id,
|
|
p_trade_id,
|
|
p_order_id,
|
|
p_symbol,
|
|
upper(p_side),
|
|
0,
|
|
COALESCE(p_price, 0),
|
|
'OPEN',
|
|
now(),
|
|
now()
|
|
) ON CONFLICT (profile_id, trade_id) DO NOTHING;
|
|
|
|
INSERT INTO trade_history (
|
|
trade_id,
|
|
profile_id,
|
|
symbol,
|
|
side,
|
|
size,
|
|
entry_price,
|
|
exit_price,
|
|
pnl,
|
|
pnl_percent,
|
|
reason,
|
|
timestamp,
|
|
stop_loss,
|
|
take_profit,
|
|
source,
|
|
lifecycle_marker
|
|
) VALUES (
|
|
p_trade_id,
|
|
p_profile_id,
|
|
p_symbol,
|
|
upper(p_side),
|
|
COALESCE(p_quantity, 0),
|
|
COALESCE(p_price, 0),
|
|
NULL,
|
|
0,
|
|
0,
|
|
p_reason,
|
|
p_timestamp,
|
|
p_stop_loss,
|
|
p_take_profit,
|
|
p_source,
|
|
entry_marker
|
|
) ON CONFLICT (lifecycle_marker) DO NOTHING;
|
|
|
|
IF lifecycle_row IS NULL THEN
|
|
SELECT * INTO lifecycle_row
|
|
FROM trade_lifecycle
|
|
WHERE profile_id = p_profile_id
|
|
AND trade_id = p_trade_id;
|
|
END IF;
|
|
|
|
RETURN row_to_json(lifecycle_row);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|