learning_ai_invt_trdg/backend/schema/011_capital_ledger.sql

110 lines
3.8 KiB
PL/PgSQL

-- ============================================================
-- Migration 011: Capital Ledger Hardening
-- Date: 2026-02-16
-- Purpose: Introduce deterministic per-profile capital ledger and helper RPCs.
-- ============================================================
CREATE TABLE IF NOT EXISTS capital_ledgers (
profile_id uuid PRIMARY KEY REFERENCES trade_profiles(id) ON DELETE CASCADE,
allocated_capital numeric NOT NULL DEFAULT 0,
reserved_for_orders numeric NOT NULL DEFAULT 0,
reserved_for_positions numeric NOT NULL DEFAULT 0,
realized_pnl numeric NOT NULL DEFAULT 0,
updated_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE capital_ledgers IS 'Deterministic ledger for profile capital isolation.';
COMMENT ON COLUMN capital_ledgers.realized_pnl IS 'Cumulative realized profit/loss for the profile.';
CREATE INDEX IF NOT EXISTS idx_capital_ledgers_profile ON capital_ledgers(profile_id);
ALTER TABLE capital_ledgers ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users can manage own ledger" ON capital_ledgers;
CREATE POLICY "Users can manage own ledger" ON capital_ledgers
FOR ALL
USING (auth.uid() = (SELECT user_id FROM trade_profiles WHERE id = capital_ledgers.profile_id))
WITH CHECK (auth.uid() = (SELECT user_id FROM trade_profiles WHERE id = capital_ledgers.profile_id));
CREATE OR REPLACE FUNCTION fn_reserve_for_order(p_profile uuid, p_amount numeric)
RETURNS capital_ledgers AS $$
DECLARE
ledger capital_ledgers;
BEGIN
UPDATE capital_ledgers
SET reserved_for_orders = reserved_for_orders + p_amount,
updated_at = now()
WHERE profile_id = p_profile
AND (allocated_capital - reserved_for_orders - reserved_for_positions) >= p_amount
RETURNING * INTO ledger;
IF NOT FOUND THEN
RAISE EXCEPTION 'Insufficient capital for profile %', p_profile;
END IF;
RETURN ledger;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION fn_release_order_reservation(p_profile uuid, p_amount numeric)
RETURNS capital_ledgers AS $$
DECLARE
ledger capital_ledgers;
BEGIN
UPDATE capital_ledgers
SET reserved_for_orders = GREATEST(reserved_for_orders - p_amount, 0),
updated_at = now()
WHERE profile_id = p_profile
RETURNING * INTO ledger;
RETURN ledger;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION fn_adjust_position_reservation(p_profile uuid, p_delta numeric)
RETURNS capital_ledgers AS $$
DECLARE
ledger capital_ledgers;
BEGIN
UPDATE capital_ledgers
SET reserved_for_positions = GREATEST(reserved_for_positions + p_delta, 0),
updated_at = now()
WHERE profile_id = p_profile
RETURNING * INTO ledger;
RETURN ledger;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION fn_record_realized_pnl(p_profile uuid, p_delta numeric)
RETURNS capital_ledgers AS $$
DECLARE
ledger capital_ledgers;
BEGIN
UPDATE capital_ledgers
SET realized_pnl = realized_pnl + COALESCE(p_delta, 0),
updated_at = now()
WHERE profile_id = p_profile
RETURNING * INTO ledger;
RETURN ledger;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION fn_rebuild_ledger(p_profile uuid, p_reserved_orders numeric, p_reserved_positions numeric)
RETURNS capital_ledgers AS $$
DECLARE
ledger capital_ledgers;
BEGIN
INSERT INTO capital_ledgers (profile_id, allocated_capital, reserved_for_orders, reserved_for_positions, updated_at)
VALUES (p_profile, (SELECT allocated_capital FROM trade_profiles WHERE id = p_profile), p_reserved_orders, p_reserved_positions, now())
ON CONFLICT (profile_id) DO UPDATE
SET reserved_for_orders = p_reserved_orders,
reserved_for_positions = p_reserved_positions,
updated_at = now()
RETURNING * INTO ledger;
RETURN ledger;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;