-- ============================================================ -- 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;