Predictu
Trading Engine

Risk Management

Predictu employs a 5-wall defense system that evaluates every incoming trade before execution. Each wall acts as an independent gate - a trade must pass all five to proceed. If any wall rejects the trade, the entire request is denied and a risk event is logged with the appropriate severity.

Design principle: Sells (position exits) always bypass risk checks. Allowing users to close positions reduces the platform’s exposure, so there is no reason to block them.

Defense System Overview

The five walls are evaluated in order. Execution stops at the first rejection, which minimizes unnecessary computation. The walls progress from cheapest to most expensive in terms of query cost.

WallNameScopeDefault Limit
1Per-trade limitSingle trade, per user tierVaries by tier
2Per-market exposure capTotal platform exposure on one market$10,000
3Per-category exposure capTotal exposure across a category$25,000
4Global exposure capTotal platform-wide exposure$100,000
5Circuit breakersPer-user and system-wide halts$5,000 daily loss / user

Wall 1: Per-Trade Limits

Every user belongs to a tier. The tier determines the maximum dollar amount that can be risked on a single trade. Tiers are assigned automatically based on user behavior scoring and can be overridden manually by a God Mode admin.

Tier Limit Table

TierMax Per-TradeDescription
new$10Default tier for newly registered users. Low limit while behavior is assessed.
regular$100Users with normal trading patterns. Upgraded automatically after sufficient history.
vip$1,000High-value users with clean history. Can be assigned manually by operators.
restricted$5Users flagged as sharp bettors or exhibiting suspicious patterns.

How Tier is Evaluated

function getPerTradeLimit(user: User): number {
  const tierLimits: Record<UserTier, number> = {
    new: 10,
    regular: 100,
    vip: 1000,
    restricted: 5,
  };
  return tierLimits[user.tier] ?? tierLimits.new;
}

// Wall 1 check
if (tradeAmount > getPerTradeLimit(user)) {
  logRiskEvent("wall_1_rejected", {
    severity: "warning",
    user_id: user.id,
    attempted: tradeAmount,
    limit: getPerTradeLimit(user),
    tier: user.tier,
  });
  throw new RiskError("Trade exceeds per-trade limit for your tier");
}

Wall 2: Per-Market Exposure Cap

This wall prevents the platform from accumulating too much risk on any single market. The exposure is calculated as the sum of all open position costs across all users for a given market.

Calculation

const marketExposure = await getMarketExposure(marketId);
const MAX_MARKET_EXPOSURE = 10_000; // $10k default

if (marketExposure + tradeAmount > MAX_MARKET_EXPOSURE) {
  logRiskEvent("wall_2_rejected", {
    severity: "warning",
    market_id: marketId,
    current_exposure: marketExposure,
    attempted: tradeAmount,
    cap: MAX_MARKET_EXPOSURE,
  });
  throw new RiskError("Market exposure cap reached");
}
Operator override: Per-market caps can be adjusted per operator via the God Mode dashboard. High-traffic operators may negotiate higher limits as part of their service agreement.

Wall 3: Per-Category Exposure Cap

Markets are grouped into categories (e.g., politics, sports,crypto, entertainment). Wall 3 caps total exposure across all markets within a category at $25,000 by default.

Rationale

Correlated events within a category can create systemic risk. For example, if multiple political markets resolve the same way due to a single election result, the platform could face outsized losses. Category caps limit this correlation risk.

const categoryExposure = await getCategoryExposure(market.category);
const MAX_CATEGORY_EXPOSURE = 25_000; // $25k default

if (categoryExposure + tradeAmount > MAX_CATEGORY_EXPOSURE) {
  logRiskEvent("wall_3_rejected", {
    severity: "warning",
    category: market.category,
    current_exposure: categoryExposure,
    attempted: tradeAmount,
    cap: MAX_CATEGORY_EXPOSURE,
  });
  throw new RiskError("Category exposure cap reached");
}

Wall 4: Global Exposure Cap

The final exposure wall caps total platform-wide risk at $100,000. This is the sum of all open positions across all markets, categories, and users. It acts as the ultimate backstop to prevent runaway exposure.

const globalExposure = await getGlobalExposure();
const MAX_GLOBAL_EXPOSURE = 100_000; // $100k default

if (globalExposure + tradeAmount > MAX_GLOBAL_EXPOSURE) {
  logRiskEvent("wall_4_rejected", {
    severity: "critical",
    current_exposure: globalExposure,
    attempted: tradeAmount,
    cap: MAX_GLOBAL_EXPOSURE,
  });
  throw new RiskError("Global exposure cap reached");
}
Critical severity: A Wall 4 rejection is logged at critical severity because it means the entire platform is at its risk limit. This should trigger an immediate admin notification.

Wall 5: Circuit Breakers

Circuit breakers are dynamic halts that activate based on loss patterns. Unlike the exposure caps (Walls 2–4), circuit breakers monitor realized losses over rolling time windows.

Circuit Breaker Types

BreakerTriggerEffectAuto-Reset
daily_loss_haltUser loses > $5,000 in 24hBlock all new buys for that userAfter 24h window rolls
rapid_loss_haltUser loses > $2,000 in 1hBlock all new buys for that userAfter 1h window rolls
system_haltPlatform loses > $50,000 in 24hBlock all new buys platform-wideManual reset by God Mode admin

Configuration

interface CircuitBreakerConfig {
  daily_loss_halt: {
    threshold: number;   // Default: 5000
    window_hours: number; // Default: 24
    scope: "user";
  };
  rapid_loss_halt: {
    threshold: number;   // Default: 2000
    window_hours: number; // Default: 1
    scope: "user";
  };
  system_halt: {
    threshold: number;   // Default: 50000
    window_hours: number; // Default: 24
    scope: "platform";
    auto_reset: false;   // Requires manual intervention
  };
}

Evaluation Flow

1
Check system halt - If the platform-wide circuit breaker is active, reject immediately. No per-user checks needed.
2
Load user loss history - Query realized losses for the user over both the 1h and 24h windows.
3
Evaluate rapid halt - If 1h losses exceed $2,000, activate the per-user breaker and reject.
4
Evaluate daily halt - If 24h losses exceed $5,000, activate the per-user breaker and reject.
5
Pass - No breakers tripped. Trade proceeds to execution.

Sharp Bettor Detection

The user scoring system assigns a sharpness_score (0–100) based on historical trading patterns. Users with high scores consistently beat the spread, which indicates informed or professional betting. The risk engine uses this score to automatically widen spreads for sharp users.

Spread Adjustments

ConditionSpread AdjustmentReason
Tier = restricted+3%Flagged as sharp bettor. Maximum spread penalty.
Sharpness score > 80+2%Consistently profitable. Likely informed bettor.
Sharpness score > 60+1%Above-average performance. Minor spread widening.
Sharpness score ≤ 60+0% (none)Normal user. Standard spread applied.
Stacking rule: Spread adjustments do not stack. A restricteduser with a score of 85 gets +3% (the tier penalty), not +3% + 2%. The highest applicable adjustment wins.

Spread Calculation Example

function getSpreadAdjustment(user: User): number {
  if (user.tier === "restricted") return 0.03;
  if (user.sharpness_score > 80) return 0.02;
  if (user.sharpness_score > 60) return 0.01;
  return 0;
}

// Applied during price calculation
const baseSpread = spreadEngine.getSpread(market);
const adjustment = getSpreadAdjustment(user);
const effectiveSpread = baseSpread + adjustment;

// Example: base spread 4%, restricted user
// effectiveSpread = 0.04 + 0.03 = 0.07 (7%)
// Buy price for 50% market: 0.50 + (0.07 / 2) = 0.535
// Sell price for 50% market: 0.50 - (0.07 / 2) = 0.465

Sells Always Bypass Risk

A core design decision: sell orders (position exits) never trigger risk walls.When a user sells, they are reducing their position and, by extension, the platform’s exposure. Blocking sells would increase risk, not decrease it.

function shouldApplyRiskChecks(side: "buy" | "sell"): boolean {
  // Sells always pass - they reduce exposure
  if (side === "sell") return false;
  return true;
}
Important: Even when a circuit breaker is active, sell orders still execute. A user halted by daily_loss_halt can still close their open positions.

Risk Event Logging

Every risk decision - whether a pass or rejection - is logged to therisk_events table. This provides a complete audit trail for compliance and debugging.

Severity Levels

SeverityMeaningAdmin Action
infoTrade passed all risk checks. Normal operation.None. Available in logs for audit.
warningTrade rejected by Wall 1, 2, or 3. User or market at limit.Review in operator dashboard. May indicate growing exposure.
criticalTrade rejected by Wall 4 or 5. Platform-level risk event.Immediate notification. God Mode admin should review.

Risk Event Schema

interface RiskEvent {
  id: string;
  timestamp: string;        // ISO 8601
  severity: "info" | "warning" | "critical";
  wall: 1 | 2 | 3 | 4 | 5 | null;  // null for info (passed)
  user_id: string;
  operator_id: string | null;
  market_id: string | null;
  trade_amount: number;
  details: {
    current_exposure?: number;
    cap?: number;
    tier?: string;
    sharpness_score?: number;
    circuit_breaker?: string;
    [key: string]: unknown;
  };
}

User Tier Changes

Tier changes are sensitive operations because they directly affect a user’s trading limits and spread adjustments. Every tier change is recorded with a full audit trail.

Tier Change Flow

1
Initiate change - An admin (God Mode or operator) selects a user and chooses a new tier from the dropdown.
2
Require reason - The admin must provide a written reason for the change. This is mandatory and cannot be empty.
3
Record audit entry - The system creates an entry inuser_tier_changes with the old tier, new tier, admin ID, reason, and timestamp.
4
Update user record - The user’s tier field is updated. The change takes effect immediately on all subsequent trades.
5
Notify operator - If the change was made by a God Mode admin, the user’s operator receives a notification in their dashboard.

Audit Record Schema

interface UserTierChange {
  id: string;
  user_id: string;
  old_tier: UserTier;
  new_tier: UserTier;
  changed_by: string;         // Admin user ID
  reason: string;             // Mandatory explanation
  changed_at: string;         // ISO 8601
  source: "godmode" | "operator" | "automatic";
}
Automatic restriction: When the scoring engine detects a sharpness score above 90 for three consecutive evaluation periods, the user is automatically moved to the restricted tier. The reason is recorded as"Automatic restriction: sharpness score exceeded threshold" and the source is set to automatic.

Configuration Reference

All risk parameters can be configured per operator via the God Mode dashboard. Defaults are shown below.

interface RiskConfig {
  // Wall 1: Per-trade limits
  tier_limits: {
    new: 10;
    regular: 100;
    vip: 1000;
    restricted: 5;
  };

  // Wall 2: Per-market cap
  max_market_exposure: 10_000;

  // Wall 3: Per-category cap
  max_category_exposure: 25_000;

  // Wall 4: Global cap
  max_global_exposure: 100_000;

  // Wall 5: Circuit breakers
  circuit_breakers: {
    daily_loss_halt: 5_000;
    rapid_loss_halt: 2_000;
    system_halt: 50_000;
  };

  // Sharp bettor adjustments
  spread_adjustments: {
    restricted: 0.03;
    sharp_high: 0.02;   // score > 80
    sharp_medium: 0.01; // score > 60
  };
}

Risk configuration changes are logged to the audit trail and require God Mode admin privileges. Operator admins can view their configuration but cannot modify it directly.