D13-F02-A05 / Complete engineering topic

Opportunistic Dark-Pool Execution

A production-minded guide to Opportunistic Dark-Pool Execution.

D13 · EXECUTION AND TRANSACTION COS…
D13-F02-A05Canonical / Tested / Open
D13 / D13-F02

Apply firm-probability, price-improvement, minimum-size, per-window, total-dark, and fallback-clock gates to synthetic dark opportunities.

The decision this tutorial makes visible

Undisplayed liquidity may reduce displayed footprint or offer improvement, but indications can fail to firm and a parent still needs a completion path.

The precise question is: How can a parent plan bounded dark exposure while preserving a deterministic lit fallback when indications are uncertain?

A trader needs to see why the plan changes; a builder needs the same objective, clock, units, thresholds, lot policy, and state to reproduce the decision across code, audit data, visuals, and the playground.

Intuition before notation

Dark opportunities are treated as uncertain decision inputs, not fills. The dark fraction and fallback window prevent indefinite exposure from hiding completion risk.

This transparent teaching controller is one package-selected variant. Broker algorithms may use richer forecasts, venue state, constraints, and proprietary calibration. Those variants must not be implied by the shared catalog title.

Scope and nearby methods

Exposure-plan teaching model for U.S. ATS context. It does not recommend a venue, infer hidden liquidity, predict fills, model conditional-order protocols, or replace broker and regulatory review.

VariantDefinitionBest useMain limitation
Package exposure planBound dark exposure and reserve lit fallbackTeaching risk controlsNo fills or venue recommendation
Conditional-order workflowIndication then firm-up protocolInstitutional block interactionRequires ATS disclosure and messaging state
Dark-only strategySeek all completion undisplayedSpecialized mandateHigh non-fill risk

What is sourced, selected, synthetic, and derived

RoleMaterial claimEvidenceBoundary
Sourced factNMS Stock ATS operational disclosures, safeguards, and the regulatory context for systems commonly called dark pools.S1, S2, S3, S4, S5Does not imply SEC approval of a venue or define the package's exposure policy.
Implementation choiceExposure-plan teaching model for U.S. ATS context. It does not recommend a venue, infer hidden liquidity, predict fills, model conditional-order protocols, or replace broker and regulatory review.Frozen package definitionProprietary broker logic and nearby catalog methods are excluded.
Synthetic teaching inputAll orders, prices, coefficients, events, probabilities, and opportunities are repository-authored.datasets/canonical-input.json and scenario-results.jsonThey are not observed customer orders, live venue state, or calibrated forecasts.
Author-derived calculationTen synthetic opportunities span accepted, low-probability, low-improvement, small-size, cap, and fallback states. The parent allows at most 60% planned dark exposure, so any unexposed quantity remains visible as a lit fallback requirement.Formula, fixture, Python/TypeScript parity, and independent arithmeticDefinition correctness does not establish execution quality.

Primary sources establish the named research, benchmark, interface, regulatory, or market-structure context. The algorithm, thresholds, fixtures, expected values, and scenario paths are repository-authored implementation choices and synthetic teaching data.

Formula, symbols, and numerical policy

Plain text
Set $D_{cap}=\lfloor Xf_{dark}\rfloor_L$. Before fallback, expose $d_i=\min(D_{cap}-D_{i-1},q_{window},\lfloor I_i\rfloor_L)$ only when probability, improvement, and minimum-size gates pass; send $X-D$ to the declared lit fallback plan.
SymbolMeaningUnitPolicy
D_capMaximum dark exposuresharesLot-floored parent fraction
I_iIndicated quantitysharesSynthetic uncertain input
p_iFirm probability scorebpsCaller-supplied, not calibrated
FFallback windowwindow indexActive at equality
  • Use integer shares and an explicit lot size for every parent, child, exposure, and residual quantity.
  • Represent prices as integer atoms; convert to basis points only with the declared side and benchmark.
  • Apply equality, cap, freshness, and fallback rules before formatting any displayed value.
  • Round floating-point scores for display only after the deterministic decision has been made.

Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.

Build the algorithm

  1. Validate parent, dark fraction, thresholds, lots, fallback, and opportunities.
  2. Compute the total lot-floored dark cap.
  3. Before fallback, accept only opportunities that pass probability, improvement, and minimum-size gates.
  4. Cap each exposure and send the unexposed residual to the lit fallback plan.

Production-minded operational checklist

  1. Verify ATS identity/disclosure separately
  2. Treat indications as uncertain
  3. Cap dark fraction
  4. Activate fallback deterministically

A precise output is unsafe when its decision clock, side, benchmark, limit, lot size, parameter vintage, or source state is ambiguous. Reject ambiguity rather than silently guessing.

Worked synthetic example

The canonical fixture is synthetic teaching data, not an observed control event, customer order, or broker execution. Its primary author-derived output, planned_dark_exposure_quantity, is The canonical output is shown as a bounded decision summary; full schedules and audit rows remain in the fixture and playground.. The complete input and output are in datasets/canonical-input.json and datasets/expected-output.json.

Ten synthetic opportunities span accepted, low-probability, low-improvement, small-size, cap, and fallback states. The parent allows at most 60% planned dark exposure, so any unexposed quantity remains visible as a lit fallback requirement.

Counterfactual checkpoint

Fallback equality. Move an otherwise eligible opportunity to the exact fallback window. The output changes because The selected policy activates fallback at equality.

The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.

Boundary and counterexample workbook

The playground computes every scenario at 61 deterministic parameter states. The table uses the declared focus step and states whether that focus reproduces the canonical fixture. The full state ledger and compressed transition segments are in datasets/scenario-results.json.

ScenarioReview focusPurposeStatePrimary outputDiagnosticDecision segments
Canonical first-indication sweepStep 30 · canonical fixtureChange the first dark indication and land on the canonical opportunity set. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 3300 sharesfallback 2700; 5 windows1
Firm-probability equalityStep 30 · comparison focusSet an otherwise eligible opportunity exactly at the probability threshold. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 3300 sharesfallback 2700; 5 windows1
Minimum-size failureStep 30 · comparison focusMove indications below the required minimum execution size. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 0 sharesfallback 6000; 0 windows1
Price-improvement failureStep 30 · comparison focusRemove required improvement from otherwise strong opportunities. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 0 sharesfallback 6000; 0 windows1
Early fallbackStep 30 · comparison focusActivate fallback before later eligible opportunities can receive exposure. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 1400 sharesfallback 4600; 2 windows1
Full dark-cap allowanceStep 30 · comparison focusAllow up to 100% exposure while retaining all other gates. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 3300 sharesfallback 2700; 5 windows1
Low dark-fraction capStep 30 · comparison focusRestrict planned dark exposure to a small parent fraction. Synthetic inputs; the output is a teaching plan, not a fill or best-execution claim.lit-fallback-requireddark exposure 500 sharesfallback 5500; 1 windows1

These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.

Visualize the boundary

Opportunistic Dark-Pool Execution annotated teaching map

Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.

The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.

Implementation walkthrough

The Python and TypeScript references validate the complete contract, compute one deterministic result, retain reason codes and intermediate quantities, and expose enough state for independent arithmetic. They do not connect to a venue or broker.

The main implementation branches are:

  • window >= fallback — Reject dark exposure, because Completion clock is active.
  • probability/improvement/size fails — Reject with exact reason, because Selected opportunity quality gate.
  • all gates pass — Expose capped quantity, because Bounded dark opportunity.
  • after audit — Reserve residual for lit fallback, because Parent conservation.

Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.

Testing and validation

Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.

The audit must preserve these invariants:

  • Planned dark exposure never exceeds its cap.
  • Dark exposure plus lit fallback equals the parent.
  • Opportunities at or after fallback release no dark quantity.
  • The output retains planned_dark_exposure_quantity, state, limits, and the decision explanation.

Passing these checks proves deterministic behavior under the selected contract. It does not prove model calibration, fill quality, best execution, cost reduction, or profitability.

Failure modes and misuse

  • A plan, score, accepted opportunity, or dark exposure is not an execution and does not guarantee acknowledgement or fill.
  • Synthetic impact, volatility, confidence, depth, and probability inputs do not establish live-market calibration or execution quality.
  • Passing definition tests does not establish best execution, lower realized cost, profitability, or predictive power.

Debugging order

When a result looks surprising, inspect the state in this order:

  1. Confirm the parent side, quantity, lot size, decision clock, and benchmark.
  2. Confirm impact/risk inputs or opportunity observations were available at that decision.
  3. Recalculate thresholds, lot floors, caps, residual quantity, and the selected reason code.
  4. Only then compare Python, TypeScript, article, visual, and playground outputs.

Evidence and historical boundary

Historical decision: deferred. Named broker algorithms and customer executions remain deferred because reproducible evaluation requires the parent instruction, decision clock, algorithm parameters and versions, complete child lifecycle, contemporaneous market data, venue disclosures, fees, corrections, and redistribution rights.

The primary sources are SEC Regulation ATS-N final rule, SEC current ATS list, FINRA OTC transparency, SEC Algorithmic Trading Report, FIXatdl 1.2 RC1. They support the source roles listed in the research ledger, not a redistributable historical observation, a broker algorithm, venue recommendation, customer execution, fill guarantee, best-execution conclusion, cost-savings claim, profitability claim, or prediction claim

Summary and next topic

You can now Continue from Opportunistic Dark-Pool Execution to Best-Price Smart Order Router without carrying over a benchmark, clock, or decision rule that the next method does not use.. The learning flow is: Liquidity-Seeking Execution → Opportunistic Dark-Pool Execution → Best-Price Smart Order Router. Carry the result forward only with its scope, clock, state, and evidence label.

Opportunistic Dark-Pool Execution calculation flow

This flow identifies the selected calculation stages and the structured output.

Rendering system map…

Takeaway: The fallback is part of the algorithm, not an afterthought: uncertain dark opportunity can never erase the residual parent.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

S1 — Regulation of NMS Stock Alternative Trading Systems

  • Organization or authors: U.S. Securities and Exchange Commission
  • Source type: Official final rule and disclosure framework
  • Publication or effective date: 2018-07-18
  • Version: Release 34-83663 and related Form ATS-N framework
  • URL or DOI: https://www.sec.gov/rules-regulations/2018/07/regulation-nms-stock-alternative-trading-systems
  • Accessed: 2026-08-01
  • Jurisdiction: United States NMS Stock ATSs
  • Supports: NMS Stock ATS operational disclosures, safeguards, and the regulatory context for systems commonly called dark pools.
  • Limitations: Does not imply SEC approval of a venue or define the package's exposure policy.

S2 — Alternative Trading System (ATS) List

  • Organization or authors: U.S. Securities and Exchange Commission
  • Source type: Official current regulatory list
  • Publication or effective date: 2026-05-31
  • Version: May 2026 list; page accessed 2026-08-01
  • URL or DOI: https://www.sec.gov/foia/frequently-requested-documents/alternative-trading-system-ats-list
  • Accessed: 2026-08-01
  • Jurisdiction: United States alternative trading systems
  • Supports: Regulation ATS reporting context and the fact that the list changes as filings and cessations occur.
  • Limitations: A list entry does not establish fill quality, liquidity, anonymity, or suitability for a parent order.

S3 — OTC (ATS and Non-ATS) Transparency

  • Organization or authors: Financial Industry Regulatory Authority
  • Source type: Official SRO reporting resource
  • Publication or effective date: current
  • Version: Accessed 2026-08-01
  • URL or DOI: https://www.finra.org/filing-reporting/otc-transparency
  • Accessed: 2026-08-01
  • Jurisdiction: United States OTC equity trade reporting
  • Supports: FINRA publishes delayed security-level OTC trading information derived from member trade reports.
  • Limitations: Delayed aggregate volume cannot reconstruct a point-in-time opportunity, conditional indication, fill probability, or customer execution.

S4 — Staff Report on Algorithmic Trading in U.S. Capital Markets

  • Organization or authors: Staff of the U.S. Securities and Exchange Commission
  • Source type: Official regulator staff report
  • Publication or effective date: 2020-08-05
  • Version: Staff report to Congress
  • URL or DOI: https://www.sec.gov/about/reports-publications/algo_trading_report_2020
  • Accessed: 2026-08-01
  • Jurisdiction: United States capital markets
  • Supports: The distinction between demanding and providing liquidity and the operational context in which execution algorithms pursue customer objectives.
  • Limitations: Does not prescribe these formulas, parameters, venue logic, or synthetic fixtures.

S5 — FIX Algorithmic Trading Definition Language Online Specification

  • Organization or authors: FIX Trading Community
  • Source type: Official industry technical standard
  • Publication or effective date: 2021-05-20
  • Version: 1.2 Release Candidate 1
  • URL or DOI: https://www.fixtrading.org/standards/fixatdl-online/
  • Accessed: 2026-08-01
  • Jurisdiction: Cross-market electronic trading interfaces
  • Supports: Machine-readable strategy parameters, legal ranges, validation rules, effective times, expiry times, and disclosure links.
  • Limitations: Defines an interface-description standard, not a universal execution algorithm or fill model.

Evidence boundary

Sources support only their recorded role. They do not validate repository-authored parameters, synthetic opportunities, expected fills, or execution quality.

cost-risk-optimization.ts
/** Deterministic reference algorithms for D13-F02 Cost/Risk Optimization. */

export type JsonObject = Record<string, unknown>;

function integer(name: string, value: unknown, options: { positive?: boolean; nonnegative?: boolean } = {}): number {
  if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${name} must be an integer`);
  if (options.positive && value <= 0) throw new Error(`${name} must be positive`);
  if (options.nonnegative && value < 0) throw new Error(`${name} must be nonnegative`);
  return value;
}

function finiteNumber(name: string, value: unknown, options: { positive?: boolean; nonnegative?: boolean } = {}): number {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${name} must be a finite number`);
  if (options.positive && value <= 0) throw new Error(`${name} must be positive`);
  if (options.nonnegative && value < 0) throw new Error(`${name} must be nonnegative`);
  return value;
}

function sideValue(value: unknown): "buy" | "sell" {
  if (value !== "buy" && value !== "sell") throw new Error("side must be 'buy' or 'sell'");
  return value;
}

function validateQuantity(totalQuantity: unknown, lotSize: unknown): [number, number] {
  const total = integer("total_quantity", totalQuantity, { positive: true });
  const lot = integer("lot_size", lotSize, { positive: true });
  if (total % lot !== 0) throw new Error("total_quantity must be divisible by lot_size");
  return [total, lot];
}

function lotFloor(quantity: number, lotSize: number): number {
  return Math.max(0, Math.floor(quantity / lotSize + 1e-12) * lotSize);
}

function round(value: number, digits: number): number {
  const scale = 10 ** digits;
  return Math.round((value + Number.EPSILON) * scale) / scale;
}

function allocateLots(totalQuantity: number, weights: number[], lotSize: number): number[] {
  if (!weights.length || weights.some((value) => !Number.isFinite(value) || value < 0) || weights.reduce((a, b) => a + b, 0) <= 0) {
    throw new Error("allocation weights must be finite, nonnegative, and have positive total");
  }
  const totalLots = totalQuantity / lotSize;
  const weightSum = weights.reduce((a, b) => a + b, 0);
  const exact = weights.map((weight) => totalLots * weight / weightSum);
  const bases = exact.map((value) => Math.floor(value + 1e-12));
  let residual = totalLots - bases.reduce((a, b) => a + b, 0);
  const order = weights.map((_, index) => index).sort((left, right) => {
    const difference = (exact[right] - bases[right]) - (exact[left] - bases[left]);
    return difference !== 0 ? difference : left - right;
  });
  for (const index of order) {
    if (residual <= 0) break;
    bases[index] += 1;
    residual -= 1;
  }
  return bases.map((lots) => lots * lotSize);
}

function signedMoveBps(side: "buy" | "sell", current: number, benchmark: number): number {
  const direction = side === "buy" ? 1 : -1;
  return direction * (current - benchmark) * 10000 / benchmark;
}

export function almgrenChrissOptimalExecution(input: JsonObject): JsonObject {
  const [total, lot] = validateQuantity(input.total_quantity, input.lot_size);
  const intervals = integer("interval_count", input.interval_count, { positive: true });
  const impactWeight = finiteNumber("temporary_impact_weight", input.temporary_impact_weight, { positive: true });
  const riskWeight = finiteNumber("inventory_risk_weight", input.inventory_risk_weight, { nonnegative: true });
  if (total / lot < intervals) throw new Error("interval_count cannot exceed available lots");

  let kappa = 0;
  let rawTrades: number[];
  if (riskWeight === 0) {
    rawTrades = Array(intervals).fill(1);
  } else {
    kappa = Math.acosh(1 + riskWeight / (2 * impactWeight));
    const denominator = Math.sinh(kappa * intervals);
    const holdings = Array.from({ length: intervals + 1 }, (_, step) => total * Math.sinh(kappa * (intervals - step)) / denominator);
    rawTrades = Array.from({ length: intervals }, (_, index) => holdings[index] - holdings[index + 1]);
  }
  const trades = allocateLots(total, rawTrades, lot);
  let remaining = total;
  let impactScore = 0;
  let riskScore = 0;
  const schedule = trades.map((quantity, index) => {
    remaining -= quantity;
    const impactComponent = impactWeight * quantity * quantity;
    const riskComponent = riskWeight * remaining * remaining;
    impactScore += impactComponent;
    riskScore += riskComponent;
    return { interval: index + 1, target_quantity: quantity, target_remaining_quantity: remaining, temporary_impact_score: round(impactComponent, 6), inventory_risk_score: round(riskComponent, 6) };
  });
  return { total_quantity: total, interval_count: intervals, lot_size: lot, temporary_impact_weight: impactWeight, inventory_risk_weight: riskWeight, kappa: round(kappa, 12), scheduled_quantity: trades.reduce((a, b) => a + b, 0), remaining_quantity: remaining, temporary_impact_score: round(impactScore, 6), inventory_risk_score: round(riskScore, 6), objective_score: round(impactScore + riskScore, 6), schedule, state: riskWeight === 0 ? "risk-neutral" : "risk-adjusted" };
}

export function implementationShortfallExecution(input: JsonObject): JsonObject {
  const [remaining, lot] = validateQuantity(input.remaining_quantity, input.lot_size);
  const side = sideValue(input.side);
  const decisionPrice = integer("decision_price_atoms", input.decision_price_atoms, { positive: true });
  const currentPrice = integer("current_price_atoms", input.current_price_atoms, { positive: true });
  const spread = finiteNumber("spread_bps", input.spread_bps, { nonnegative: true });
  const impact = finiteNumber("impact_coefficient_bps", input.impact_coefficient_bps, { nonnegative: true });
  const adv = integer("average_daily_volume", input.average_daily_volume, { positive: true });
  const volatility = finiteNumber("volatility_bps", input.volatility_bps, { nonnegative: true });
  const aversion = finiteNumber("risk_aversion", input.risk_aversion, { nonnegative: true });
  const urgency = finiteNumber("urgency_bps", input.urgency_bps, { nonnegative: true });
  const intervals = integer("intervals_remaining", input.intervals_remaining, { positive: true });
  const maxChild = Math.min(remaining, integer("max_child_quantity", input.max_child_quantity, { positive: true }));
  const candidateStep = integer("candidate_step_quantity", input.candidate_step_quantity, { positive: true });
  if (maxChild % lot !== 0 || candidateStep % lot !== 0) throw new Error("max_child_quantity and candidate_step_quantity must be lot-aligned");
  const signedMove = signedMoveBps(side, currentPrice, decisionPrice);
  const adverseMove = Math.max(0, signedMove);
  const quantities: number[] = [];
  for (let quantity = 0; quantity <= maxChild; quantity += candidateStep) quantities.push(quantity);
  if (quantities[quantities.length - 1] !== maxChild) quantities.push(maxChild);
  const candidates = quantities.map((quantity) => {
    const residual = remaining - quantity;
    const immediate = quantity * (spread / 2 + impact * quantity / adv);
    const opportunity = residual * (adverseMove + urgency / intervals);
    const risk = residual * (aversion * volatility / Math.sqrt(intervals));
    return { child_quantity: quantity, residual_quantity: residual, immediate_cost_score: round(immediate, 6), opportunity_cost_score: round(opportunity, 6), inventory_risk_score: round(risk, 6), total_score: round(immediate + opportunity + risk, 6) };
  });
  const selected = [...candidates].sort((left, right) => left.total_score - right.total_score || left.child_quantity - right.child_quantity)[0];
  return { side, decision_price_atoms: decisionPrice, current_price_atoms: currentPrice, signed_move_bps: round(signedMove, 6), adverse_move_bps: round(adverseMove, 6), remaining_quantity: remaining, selected_child_quantity: selected.child_quantity, post_child_quantity: selected.residual_quantity, selected_total_score: selected.total_score, selected_immediate_cost_score: selected.immediate_cost_score, selected_opportunity_cost_score: selected.opportunity_cost_score, selected_inventory_risk_score: selected.inventory_risk_score, candidates, state: selected.child_quantity > 0 ? "execute-now" : "wait" };
}

export function arrivalPriceExecution(input: JsonObject): JsonObject {
  const [remaining, lot] = validateQuantity(input.remaining_quantity, input.lot_size);
  const side = sideValue(input.side);
  const arrival = integer("arrival_price_atoms", input.arrival_price_atoms, { positive: true });
  const current = integer("current_price_atoms", input.current_price_atoms, { positive: true });
  const tolerance = finiteNumber("tolerance_bps", input.tolerance_bps, { nonnegative: true });
  const marketVolume = integer("observed_market_volume", input.observed_market_volume, { nonnegative: true });
  const baseRate = integer("base_participation_bps", input.base_participation_bps, { nonnegative: true });
  const defensiveRate = integer("defensive_participation_bps", input.defensive_participation_bps, { nonnegative: true });
  const maxRate = integer("max_participation_bps", input.max_participation_bps, { positive: true });
  const intervals = integer("intervals_remaining", input.intervals_remaining, { positive: true });
  const deadline = integer("deadline_threshold_intervals", input.deadline_threshold_intervals, { positive: true });
  const maxChild = integer("max_child_quantity", input.max_child_quantity, { positive: true });
  if (!(0 <= defensiveRate && defensiveRate <= baseRate && baseRate <= maxRate && maxRate <= 10000)) throw new Error("participation rates must satisfy 0 <= defensive <= base <= max <= 10000");
  if (maxChild % lot !== 0) throw new Error("max_child_quantity must be lot-aligned");
  const signedMove = signedMoveBps(side, current, arrival);
  let priceState: string;
  let chosenRate: number;
  if (intervals <= deadline) { priceState = "deadline-override"; chosenRate = maxRate; }
  else if (signedMove <= -tolerance) { priceState = "favorable"; chosenRate = maxRate; }
  else if (signedMove >= tolerance) { priceState = "adverse"; chosenRate = defensiveRate; }
  else { priceState = "inside-band"; chosenRate = baseRate; }
  const target = lotFloor(marketVolume * chosenRate / 10000, lot);
  const child = Math.min(remaining, maxChild, target);
  return { side, arrival_price_atoms: arrival, current_price_atoms: current, signed_move_bps: round(signedMove, 6), tolerance_bps: tolerance, price_state: priceState, chosen_participation_bps: chosenRate, observed_market_volume: marketVolume, target_from_volume_quantity: target, child_quantity: child, remaining_quantity: remaining - child, state: child === remaining ? "parent-complete" : priceState };
}

export function liquiditySeekingExecution(input: JsonObject): JsonObject {
  const [total, lot] = validateQuantity(input.total_quantity, input.lot_size);
  const side = sideValue(input.side);
  const limitPrice = integer("limit_price_atoms", input.limit_price_atoms, { positive: true });
  const minimumConfidence = integer("minimum_confidence_bps", input.minimum_confidence_bps, { nonnegative: true });
  const maxAge = integer("maximum_age_ms", input.maximum_age_ms, { nonnegative: true });
  const maxTake = integer("max_take_per_event", input.max_take_per_event, { positive: true });
  if (minimumConfidence > 10000) throw new Error("minimum_confidence_bps cannot exceed 10000");
  if (maxTake % lot !== 0) throw new Error("max_take_per_event must be lot-aligned");
  if (!Array.isArray(input.liquidity_events) || input.liquidity_events.length === 0) throw new Error("liquidity_events must be a nonempty array");
  let remaining = total;
  let accepted = 0;
  const events = (input.liquidity_events as unknown[]).map((raw, index) => {
    if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`liquidity_events[${index}] must be an object`);
    const event = raw as JsonObject;
    if (typeof event.event_id !== "string" || !event.event_id.trim()) throw new Error(`liquidity_events[${index}].event_id must be a nonempty string`);
    const price = integer(`liquidity_events[${index}].price_atoms`, event.price_atoms, { positive: true });
    const available = integer(`liquidity_events[${index}].available_quantity`, event.available_quantity, { nonnegative: true });
    const age = integer(`liquidity_events[${index}].age_ms`, event.age_ms, { nonnegative: true });
    const confidence = integer(`liquidity_events[${index}].confidence_bps`, event.confidence_bps, { nonnegative: true });
    if (confidence > 10000) throw new Error(`liquidity_events[${index}].confidence_bps cannot exceed 10000`);
    let reason: string;
    let quantity = 0;
    if (remaining === 0) reason = "parent-complete";
    else if (age > maxAge) reason = "stale";
    else if (confidence < minimumConfidence) reason = "confidence-below-threshold";
    else if ((side === "buy" && price > limitPrice) || (side === "sell" && price < limitPrice)) reason = "outside-limit";
    else { quantity = Math.min(remaining, maxTake, lotFloor(available, lot)); reason = quantity > 0 ? "accepted" : "below-one-lot"; }
    remaining -= quantity;
    accepted += quantity;
    return { event_id: event.event_id.trim(), price_atoms: price, available_quantity: available, age_ms: age, confidence_bps: confidence, planned_take_quantity: quantity, reason, remaining_quantity: remaining };
  });
  return { side, limit_price_atoms: limitPrice, total_quantity: total, accepted_quantity: accepted, remaining_quantity: remaining, accepted_event_count: events.filter((row) => row.reason === "accepted").length, rejected_event_count: events.filter((row) => row.reason !== "accepted").length, events, state: remaining === 0 ? "parent-complete" : accepted > 0 ? "opportunity-found" : "no-eligible-liquidity" };
}

export function opportunisticDarkPoolExecution(input: JsonObject): JsonObject {
  const [total, lot] = validateQuantity(input.total_quantity, input.lot_size);
  const maxFraction = integer("maximum_dark_fraction_bps", input.maximum_dark_fraction_bps, { positive: true });
  const minProbability = integer("minimum_firm_probability_bps", input.minimum_firm_probability_bps, { nonnegative: true });
  const minImprovement = integer("minimum_price_improvement_atoms", input.minimum_price_improvement_atoms, { nonnegative: true });
  const minExecution = integer("minimum_execution_quantity", input.minimum_execution_quantity, { positive: true });
  const maxWindow = integer("max_exposure_per_window", input.max_exposure_per_window, { positive: true });
  const fallback = integer("fallback_window", input.fallback_window, { positive: true });
  if (maxFraction > 10000 || minProbability > 10000) throw new Error("basis-point parameters cannot exceed 10000");
  if (minExecution % lot !== 0 || maxWindow % lot !== 0) throw new Error("minimum_execution_quantity and max_exposure_per_window must be lot-aligned");
  if (!Array.isArray(input.dark_opportunities) || input.dark_opportunities.length === 0) throw new Error("dark_opportunities must be a nonempty array");
  const darkCap = lotFloor(total * maxFraction / 10000, lot);
  let exposed = 0;
  const opportunities = (input.dark_opportunities as unknown[]).map((raw, index) => {
    if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`dark_opportunities[${index}] must be an object`);
    const opportunity = raw as JsonObject;
    if (typeof opportunity.opportunity_id !== "string" || !opportunity.opportunity_id.trim()) throw new Error(`dark_opportunities[${index}].opportunity_id must be a nonempty string`);
    const window = integer(`dark_opportunities[${index}].window`, opportunity.window, { positive: true });
    const indicated = integer(`dark_opportunities[${index}].indicated_quantity`, opportunity.indicated_quantity, { nonnegative: true });
    const probability = integer(`dark_opportunities[${index}].firm_probability_bps`, opportunity.firm_probability_bps, { nonnegative: true });
    const improvement = integer(`dark_opportunities[${index}].price_improvement_atoms`, opportunity.price_improvement_atoms, { nonnegative: true });
    if (probability > 10000) throw new Error(`dark_opportunities[${index}].firm_probability_bps cannot exceed 10000`);
    const capRemaining = Math.max(0, darkCap - exposed);
    let reason: string;
    let quantity = 0;
    if (window >= fallback) reason = "fallback-active";
    else if (probability < minProbability) reason = "firm-probability-below-threshold";
    else if (improvement < minImprovement) reason = "price-improvement-below-threshold";
    else if (indicated < minExecution) reason = "minimum-size-not-met";
    else if (capRemaining === 0) reason = "dark-cap-reached";
    else { quantity = Math.min(capRemaining, maxWindow, lotFloor(indicated, lot)); reason = quantity >= minExecution ? "expose-dark" : "minimum-size-not-met"; if (reason !== "expose-dark") quantity = 0; }
    exposed += quantity;
    return { opportunity_id: opportunity.opportunity_id.trim(), window, indicated_quantity: indicated, firm_probability_bps: probability, price_improvement_atoms: improvement, planned_dark_exposure_quantity: quantity, reason, dark_cap_remaining_quantity: Math.max(0, darkCap - exposed) };
  });
  const fallbackQuantity = total - exposed;
  return { total_quantity: total, maximum_dark_fraction_bps: maxFraction, dark_quantity_cap: darkCap, planned_dark_exposure_quantity: exposed, lit_fallback_quantity: fallbackQuantity, dark_opportunity_count: opportunities.length, accepted_dark_window_count: opportunities.filter((row) => row.reason === "expose-dark").length, opportunities, state: fallbackQuantity === 0 ? "dark-plan-complete" : "lit-fallback-required" };
}

export function calculate(topicId: string, input: JsonObject): JsonObject {
  if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("inputs must be an object");
  if (topicId === "D13-F02-A01") return almgrenChrissOptimalExecution(input);
  if (topicId === "D13-F02-A02") return implementationShortfallExecution(input);
  if (topicId === "D13-F02-A03") return arrivalPriceExecution(input);
  if (topicId === "D13-F02-A04") return liquiditySeekingExecution(input);
  if (topicId === "D13-F02-A05") return opportunisticDarkPoolExecution(input);
  throw new Error(`unsupported topic_id: ${topicId}`);
}
Full-height labplaygroundOpen full screen