Page 2 of 2

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:54 am
by PTScalper
Dealing with the "Double Counting" Problem

Because your wrapper optimistically assumes the trade filled at the exact quantity requested, a naive WebSocket integration will double-count positions (once upon order submission, once upon receiving the WebSocket fill).

To fix this without sacrificing latency, modify the GlobalRiskManager to track Pending and Confirmed quantities separately:

Submission Phase: When SubmitOrderAsync fires, it adds to the PendingQuantity.

Reconciliation Phase: When the WebSocket receives the ExecutionReport, the GlobalRiskManager subtracts from PendingQuantity and adds the exact amount to ConfirmedQuantity.

Risk Check Adjustment: The EvaluateCrossAccountHedgeAsync method then calculates total exposure as (PendingQuantity + ConfirmedQuantity) before allowing the next trade.

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:54 am
by PTScalper
To solve the double-counting problem, we need to introduce a PositionState class that uses System.Threading.Interlocked to atomically update values without locking the entire dictionary.

The strategy shifts from a simple integer to a dual-state model:

Pending: Registered instantly inside the risk check lock.

Confirmed: Updated asynchronously by the WebSocket, releasing the pending amount.

Here is the updated implementation.

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:55 am
by PTScalper
1. The Atomic Position State

This class tracks the net exposure securely across multiple threads.

Code: Select all

using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public class PositionState
{
    private int _pendingQuantity;
    private int _confirmedQuantity;

    public int PendingQuantity => _pendingQuantity;
    public int ConfirmedQuantity => _confirmedQuantity;
    
    // Total exposure is what the risk gate evaluates
    public int NetExposure => _pendingQuantity + _confirmedQuantity;

    public void AddPending(int amount)
    {
        Interlocked.Add(ref _pendingQuantity, amount);
    }

    public void ConfirmFill(int pendingToRelease, int confirmedAmount)
    {
        // Remove from pending, add to confirmed atomically
        Interlocked.Add(ref _pendingQuantity, -pendingToRelease);
        Interlocked.Add(ref _confirmedQuantity, confirmedAmount);
    }

    public void RevertPending(int amount)
    {
        Interlocked.Add(ref _pendingQuantity, -amount);
    }
}

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:55 am
by PTScalper
2. The Updated Risk Manager

The GlobalRiskManager now registers the pending quantity inside the SemaphoreSlim lock. This is critical: if you check the risk and then release the lock before adding the pending quantity, a concurrent thread could slip an opposing order through the gap.

Code: Select all

public class GlobalRiskManager
{
    // Dictionary mapping Instrument -> (AccountId -> PositionState)
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, PositionState>> _positions = new();
    private readonly SemaphoreSlim _gateLock = new SemaphoreSlim(1, 1);

    private PositionState GetOrAddPositionState(string instrument, string accountId)
    {
        var accountPositions = _positions.GetOrAdd(instrument, _ => new ConcurrentDictionary<string, PositionState>());
        return accountPositions.GetOrAdd(accountId, _ => new PositionState());
    }

    public async Task EvaluateAndRegisterOrderAsync(TradeOrder order)
    {
        await _gateLock.WaitAsync();
        try
        {
            var instrumentPositions = _positions.GetOrAdd(order.Instrument, _ => new ConcurrentDictionary<string, PositionState>());
            bool isAttemptingLong = order.Side == OrderSide.Buy;
            
            foreach (var kvp in instrumentPositions)
            {
                var otherAccountId = kvp.Key;
                var state = kvp.Value;
                var netPos = state.NetExposure; // Uses Pending + Confirmed
                
                if (netPos == 0 || otherAccountId == order.AccountId) continue;

                bool isOtherAccountLong = netPos > 0;
                
                if (isAttemptingLong != isOtherAccountLong)
                {
                    throw new RiskGateException(
                        $"Cross-account hedge rejected: Account {otherAccountId} holds {netPos} net {order.Instrument}. " +
                        $"Account {order.AccountId} attempted to {(isAttemptingLong ? "Buy" : "Sell")}.");
                }
            }
            
            // Risk passed. Optimistically register the pending order IMMEDIATELY 
            // inside the lock to block concurrent opposite orders.
            int signedQty = isAttemptingLong ? order.Quantity : -order.Quantity;
            GetOrAddPositionState(order.Instrument, order.AccountId).AddPending(signedQty);
        }
        finally
        {
            _gateLock.Release();
        }
    }

    // Called by the WebSocket service when an ExecutionReport arrives
    public void ConfirmWebSocketFill(string accountId, string instrument, int requestedQty, int filledQty, OrderSide side)
    {
        var state = GetOrAddPositionState(instrument, accountId);
        
        int signedRequested = side == OrderSide.Buy ? requestedQty : -requestedQty;
        int signedFilled = side == OrderSide.Buy ? filledQty : -filledQty;

        state.ConfirmFill(signedRequested, signedFilled);
    }

    // Called by the Wrapper if the synchronous REST API call fails
    public void RevertFailedOrder(string accountId, string instrument, int requestedQty, OrderSide side)
    {
        var state = GetOrAddPositionState(instrument, accountId);
        int signedQty = side == OrderSide.Buy ? requestedQty : -requestedQty;
        
        state.RevertPending(signedQty);
    }
}

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:55 am
by PTScalper
3. The Refactored Execution Wrapper

The execution wrapper must now handle scenarios where the order passes the internal risk check but is rejected by the broker API (e.g., insufficient margin or network timeout). In these cases, it must revert the pending quantity so the system doesn't permanently lock up "ghost" exposure.

Code: Select all

public class OrderExecutionWrapper
{
    private readonly GlobalRiskManager _riskManager;
    private readonly IBrokerApi _broker;
    private readonly IAuditLogger _logger;

    public OrderExecutionWrapper(GlobalRiskManager riskManager, IBrokerApi broker, IAuditLogger logger)
    {
        _riskManager = riskManager;
        _broker = broker;
        _logger = logger;
    }

    public async Task<string> SubmitOrderAsync(TradeOrder order)
    {
        await _logger.LogOrderAttemptAsync(order);

        // Step 1: Lock, check risk, and register pending quantity
        await _riskManager.EvaluateAndRegisterOrderAsync(order);

        try
        {
            // Step 2: Fire to broker
            var brokerOrderId = await _broker.ExecuteMarketOrderAsync(order);
            await _logger.LogOrderSentAsync(order, brokerOrderId);
            
            return brokerOrderId; 
            // We DO NOT update position here anymore. 
            // The WebSocket handles the confirmation and releases the pending state.
        }
        catch (Exception ex)
        {
            // Step 3: If API submission fails, revert the pending state immediately
            _riskManager.RevertFailedOrder(order.AccountId, order.Instrument, order.Quantity, order.Side);
            await _logger.LogOrderRejectedAsync(order, ex.Message);
            throw;
        }
    }
}

Re: Multiple accounts risk on Apex fraud systems

Posted: Tue Sep 29, 2026 10:56 am
by PTScalper
This architecture ensures your net exposure calculation is always ahead of real-time trading flow while remaining perfectly self-correcting via the WebSocket reconciliation loop.