0), broker text not null default 'MetaTrader 5 (MT5) - Direct', stop_loss_pct numeric(5,2) not null default 10.00, take_profit_pct numeric(5,2) not null default 30.00, status text not null default 'RUNNING' check (status in ('RUNNING', 'PAUSED', 'TERMINATED')), unrealized_pnl numeric(14,4) not null default 0.0000, realized_pnl numeric(14,4) not null default 0.0000, account_type text not null default 'live' check (account_type in ('demo', 'live')), created_at timestamptz not null default now(), terminated_at timestamptz ); create index if not exists idx_copied_positions_user_status on public.copied_positions(user_id, status, created_at desc); alter table public.copied_positions enable row level security; grant select, insert, update, delete on public.copied_positions to authenticated; drop policy if exists "Users and admins can manage copied positions" on public.copied_positions; create policy "Users and admins can manage copied positions" on public.copied_positions for all to authenticated using ((select auth.uid()) = user_id or public.is_admin()) with check ((select auth.uid()) = user_id or public.is_admin()); -- 4. RPC: EXECUTE COPY TRADE STAKE (ATOMIC WALLET DEDUCTION) create or replace function public.execute_copy_trade_stake( p_master_id bigint, p_stake_amount numeric, p_broker text default 'MetaTrader 5 (MT5) - Direct', p_stop_loss numeric default 10.00, p_take_profit numeric default 30.00, p_account_type text default 'live' ) returns public.copied_positions language plpgsql security definer as $$ declare v_user_id uuid; v_master public.copy_master_traders%rowtype; v_current_balance numeric(14,2); v_new_position public.copied_positions; v_initial_pnl numeric(14,4); begin v_user_id := auth.uid(); if v_user_id is null then raise exception 'Not authenticated'; end if; if p_stake_amount <= 0 then raise exception 'Stake amount must be greater than zero'; end if; -- Fetch master trader details select * into v_master from public.copy_master_traders where id = p_master_id; if not found then raise exception 'Master trader not found'; end if; -- Verify and deduct wallet balance for live accounts if p_account_type = 'live' then select real_balance into v_current_balance from public.profiles where id = v_user_id for update; if v_current_balance is null or v_current_balance < p_stake_amount then raise exception 'Insufficient live balance ($%) to allocate stake ($%)', coalesce(v_current_balance, 0), p_stake_amount; end if; -- Atomically deduct stake amount from main real wallet balance update public.profiles set real_balance = real_balance - p_stake_amount, updated_at = now() where id = v_user_id; -- Record in account_activities insert into public.account_activities (user_id, activity_type, title, description, amount, status) values ( v_user_id, 'copy_trade_stake', 'Copy Trade Stake Allocated', 'Allocated $' || p_stake_amount || ' stake to copy master ' || v_master.name, -p_stake_amount, 'completed' ); end if; -- Calculate initial varying digits PnL based on master seed and yield multiplier v_initial_pnl := round((p_stake_amount * v_master.pnl_variance_seed * v_master.yield_multiplier)::numeric, 4); -- Create copied position row insert into public.copied_positions ( user_id, master_id, master_name, allocated_stake, broker, stop_loss_pct, take_profit_pct, status, unrealized_pnl, account_type ) values ( v_user_id, p_master_id, v_master.name, p_stake_amount, p_broker, p_stop_loss, p_take_profit, 'RUNNING', v_initial_pnl, p_account_type ) returning * into v_new_position; return v_new_position; end; $$; grant execute on function public.execute_copy_trade_stake to authenticated; -- 5. RPC: TERMINATE COPY TRADE STAKE (ATOMIC WALLET PAYOUT & REFUND) create or replace function public.terminate_copy_trade_stake( p_position_id bigint ) returns numeric language plpgsql security definer as $$ declare v_user_id uuid; v_pos public.copied_positions; v_payout numeric(14,2); v_new_balance numeric(14,2); begin v_user_id := auth.uid(); if v_user_id is null then raise exception 'Not authenticated'; end if; select * into v_pos from public.copied_positions where id = p_position_id for update; if not found then raise exception 'Copied position not found'; end if; if v_pos.user_id != v_user_id and not public.is_admin() then raise exception 'Unauthorized to terminate this position'; end if; if v_pos.status = 'TERMINATED' then raise exception 'Position already terminated'; end if; -- Payout = Allocated Stake + Realized PnL v_payout := greatest(0, round((v_pos.allocated_stake + v_pos.unrealized_pnl)::numeric, 2)); -- Update copied position status update public.copied_positions set status = 'TERMINATED', realized_pnl = unrealized_pnl, terminated_at = now() where id = p_position_id; -- Refund payout to user's real balance if v_pos.account_type = 'live' then update public.profiles set real_balance = real_balance + v_payout, updated_at = now() where id = v_pos.user_id returning real_balance into v_new_balance; insert into public.account_activities (user_id, activity_type, title, description, amount, status) values ( v_pos.user_id, 'copy_trade_payout', 'Copy Trade Terminated Payout', 'Returned $' || v_payout || ' to main wallet from master ' || v_pos.master_name, v_payout, 'completed' ); end if; return coalesce(v_new_balance, 0.00); end; $$; grant execute on function public.terminate_copy_trade_stake to authenticated; -- 6. ADD REALTIME PUBLICATIONS do $$ begin if not exists ( select 1 from pg_publication_tables where pubname = 'supabase_realtime' and schemaname = 'public' and tablename = 'copy_master_traders' ) then alter publication supabase_realtime add table public.copy_master_traders; end if; if not exists ( select 1 from pg_publication_tables where pubname = 'supabase_realtime' and schemaname = 'public' and tablename = 'copied_positions' ) then alter publication supabase_realtime add table public.copied_positions; end if; if not exists ( select 1 from pg_publication_tables where pubname = 'supabase_realtime' and schemaname = 'public' and tablename = 'copy_trader_applications' ) then alter publication supabase_realtime add table public.copy_trader_applications; end if; end; $$; -- 7. SEED INITIAL 70+ MASTER TRADERS DATA INTO DATABASE insert into public.copy_master_traders (id, name, avatar, asset, win_rate, roi, drawdown, risk, followers, is_verified_owner, brokerage, yield_multiplier, pnl_variance_seed) values (36, 'Cole Mutisya', 'https://images.unsplash.com/photo-1544829099-b9a0c07fad1a?w=150&auto=format&fit=crop&q=80', 'FOREX', 81.00, 320.50, 6.50, 'Low', 3820, true, 'Cole Mutisya Direct', 1.2500, 0.0482), (1, 'π°πͺ Mwangi FX Pro', 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&auto=format&fit=crop&q=80', 'FOREX', 78.50, 245.20, 7.80, 'Low', 2140, false, 'MetaTrader 5 Direct', 1.1200, 0.0391), (2, 'π°πͺ Kamau Crypto Alpha', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&auto=format&fit=crop&q=80', 'CRYPTO', 82.10, 310.40, 9.20, 'Medium', 1890, false, 'Binance Futures API', 1.3400, 0.0523), (3, 'π°πͺ Otieno Trades', 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&auto=format&fit=crop&q=80', 'STOCKS', 74.30, 182.90, 5.40, 'Low', 1250, false, 'Interactive Brokers', 0.9500, 0.0287), (4, 'π°πͺ Wanjiku Capital', 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=150&auto=format&fit=crop&q=80', 'FOREX', 80.40, 275.60, 6.90, 'Low', 2980, false, 'MetaTrader 5 Direct', 1.1800, 0.0415), (5, 'π³π¬ Okafor Crypto', 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=150&auto=format&fit=crop&q=80', 'CRYPTO', 79.80, 290.10, 11.20, 'High', 1650, false, 'Bybit Unified API', 1.4200, 0.0589), (6, 'π³π¬ Adeboye FX', 'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=150&auto=format&fit=crop&q=80', 'FOREX', 76.20, 210.80, 8.10, 'Medium', 1420, false, 'MetaTrader 5 Direct', 1.0500, 0.0342), (7, 'πΊπΈ Alex Rivera', 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&auto=format&fit=crop&q=80', 'STOCKS', 83.50, 340.20, 6.10, 'Low', 3120, false, 'Interactive Brokers', 1.2800, 0.0463), (8, 'π©πͺ Elena Rostova', 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&auto=format&fit=crop&q=80', 'FOREX', 81.20, 280.90, 7.20, 'Low', 2740, false, 'MetaTrader 5 Direct', 1.1500, 0.0398), (9, 'πΈπ¬ Marcus Chen', 'https://images.unsplash.com/photo-1501196354995-cbb51c65aaea?w=150&auto=format&fit=crop&q=80', 'CRYPTO', 85.00, 395.40, 10.50, 'High', 3580, false, 'Binance Futures API', 1.5000, 0.0612), (10, 'π¬π§ Sarah Jenkins', 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=150&auto=format&fit=crop&q=80', 'FOREX', 77.90, 230.50, 6.80, 'Low', 2110, false, 'MetaTrader 5 Direct', 1.0800, 0.0365) on conflict (id) do update set name = excluded.name, avatar = excluded.avatar, win_rate = excluded.win_rate, roi = excluded.roi, yield_multiplier = excluded.yield_multiplier, pnl_variance_seed = excluded.pnl_variance_seed; - Founded by Pioneers" />