Skip to content

Dynamic Policy Reconfiguration

Eugene Palchukovsky edited this page Jun 18, 2026 · 2 revisions

Dynamic Policy Reconfiguration

Retune a policy while the engine is running, without rebuilding it.

The engine owns synchronization. Reconfiguration is a rare write against settings read on every order: reads stay on the hot-path optimal; the small coordination cost is paid only on the write.

A settings retune never resets live counters or accumulated state. A rate-limit barrier key that survives a replacement keeps counting in the same window; a spot-funds override change applies from the next order onward. The one deliberate exception is force-setting a P&L accumulator - an explicit call covered in Force-set Accumulated P&L below.

How It Works

A built-in policy holds its runtime-tunable configuration in a settings cell. Registering a built-in through the normal builder path auto-captures that cell, keyed by the policy's name - there is no separate "register as configurable" step. Updating through the engine and reading from the policy touch one shared value: a published update is visible to the running policy on its next check.

The Retune API

Obtain the configurator from the engine and call the typed method for the policy you want to retune:

  • Go: engine.Configure().RateLimit(name, broker, assets, accounts, accountAssets).
  • Python: engine.configure().rate_limit(name, broker=...).
  • Rust: engine.configure().rate_limit(name, |settings| ...).

The same surface exposes pnl bounds killswitch, order size limit, and spot funds for the other built-ins.

Broker barriers have one extra distinction because the compatibility retune calls use a missing value to mean "leave unchanged":

  • Go: use RateLimitUpdate or OrderSizeLimitUpdate when the broker axis itself changes. optional.None leaves it unchanged, optional.Some[*policies.RateLimitBrokerBarrier](nil) clears the rate-limit broker barrier, and optional.Some(&barrier) replaces it. For order-size use the matching optional.Some[*policies.OrderSizeBrokerBarrier](nil).
  • Python: broker=None leaves the broker barrier unchanged; pass clear_broker=True to remove it. broker and clear_broker=True cannot be provided together.
  • Rust: call settings.set_broker(None) inside the configure closure.

initial pnl is mandatory only when constructing a P&L account barrier, where it seeds P&L accumulated before engine startup. Runtime barrier replacement has no initial pnl and never re-seeds state: it always evaluates the engine's current live accumulated P&L. If tightened bounds are already breached by that P&L, subsequent orders fail; otherwise behavior continues normally. To set the live accumulator deliberately - instead of retuning the bounds around it - use the explicit call in Force-set Accumulated P&L.

Policy Names

The name selects which registered policy to retune. Each built-in registers under a fixed name:

Policy Name
Rate limit RateLimitPolicy
Order-size limit OrderSizeLimitPolicy
P&L bounds kill-switch PnlBoundsKillSwitchPolicy
Spot funds SpotFundsPolicy

Error Handling

A retune fails without touching the live settings when the name is unknown, the named policy has a different settings type than the call targets, or the new values fail validation. The failure surfaces as the language's idiomatic error type (a *configure.Error in Go, PolicyConfigureError carrying a ConfigureErrorKind in Python, and ConfigureError in Rust).

Retune a Built-in Policy

The example below registers a rate-limit policy with a generous broker limit, admits three orders, then tightens the limit at runtime. The next order is rejected against the new limit, proving the live policy reads the retuned value.

order is the AAPL/USD order built in Getting Started.

Go
// Register the rate-limit policy through Builtin so the engine keeps a
// handle to its settings; built-in policies are configurable by name.
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(
		policies.BuildRateLimit().BrokerBarrier(
			policies.RateLimitBrokerBarrier{
				Limit: policies.RateLimit{
					MaxOrders: 5,
					Window:    60 * time.Second,
				},
			},
		),
	).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

// The generous limit of 5 admits the first three orders.
for i := 0; i < 3; i++ {
	reservation, rejects, err := engine.ExecutePreTrade(order)
	if err != nil {
		return err
	}
	if rejects != nil {
		return fmt.Errorf("unexpected rejects: %v", rejects)
	}
	reservation.CommitAndClose()
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their type name (policies.RateLimitPolicyName).
err = engine.Configure().RateLimit(
	policies.RateLimitPolicyName,
	&policies.RateLimitBrokerBarrier{
		Limit: policies.RateLimit{MaxOrders: 2, Window: 60 * time.Second},
	},
	nil,
	nil,
	nil,
)
if err != nil {
	return err
}

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
_, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "rate limit exceeded: broker barrier"
Python
import datetime

import openpit
import openpit.pretrade.policies

# Register the rate-limit policy through builtin so the engine keeps a
# handle to its settings; built-in policies are configurable by name.
engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(
        openpit.pretrade.policies.build_rate_limit().broker_barrier(
            openpit.pretrade.policies.RateLimitBrokerBarrier(
                limit=openpit.pretrade.policies.RateLimit(
                    max_orders=5,
                    window=datetime.timedelta(seconds=60),
                ),
            ),
        )
    )
    .build()
)

# The generous limit of 5 admits the first three orders.
for _ in range(3):
    execute_result = engine.execute_pre_trade(order=order)
    assert execute_result.ok
    execute_result.reservation.commit()

# Tighten the broker limit to 2 at runtime, without rebuilding the engine.
# Built-in policies register under their type name (RateLimitBuilder.NAME).
engine.configure().rate_limit(
    openpit.pretrade.policies.RateLimitBuilder.NAME,
    broker=openpit.pretrade.policies.RateLimitBrokerBarrier(
        limit=openpit.pretrade.policies.RateLimit(
            max_orders=2,
            window=datetime.timedelta(seconds=60),
        ),
    ),
)

# The next order would have passed under the old limit of 5; the new limit
# of 2 rejects it, proving the live policy reads the retuned value.
execute_result = engine.execute_pre_trade(order=order)
assert not execute_result.ok
assert execute_result.rejects[0].reason == "rate limit exceeded: broker barrier"
Rust
use std::time::Duration;

use openpit::pretrade::policies::{
    RateLimit,
    RateLimitBrokerBarrier,
    RateLimitPolicy,
    RateLimitPolicyError,
    RateLimitSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};

type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;

// Register the rate-limit policy so the engine keeps a handle to its
// settings cell; built-in policies are configurable by name.
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = RateLimitPolicy::new(
    RateLimitSettings::new(
        Some(RateLimitBrokerBarrier {
            limit: RateLimit {
                max_orders: 5,
                window: Duration::from_secs(60),
            },
        }),
        [],
        [],
        [],
    )?,
    builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;

// The generous limit of 5 admits the first three orders. `order` is the
// AAPL/USD order built in Getting Started.
for _ in 0..3 {
    engine.execute_pre_trade(order())?.commit();
}

// Tighten the broker limit to 2 at runtime, without rebuilding the engine.
// Built-in policies register under their own name (RateLimitPolicy::NAME).
let name = RateLimitPolicy::<NoLocking>::NAME;
engine
    .configure()
    .rate_limit::<RateLimitPolicyError>(name, |settings| {
        settings.set_broker(Some(RateLimitBrokerBarrier {
            limit: RateLimit {
                max_orders: 2,
                window: Duration::from_secs(60),
            },
        }))
    })?;

// The next order would have passed under the old limit of 5; the new limit
// of 2 rejects it, proving the live policy reads the retuned value.
let rejects = engine.execute_pre_trade(order())
    .err()
    .expect("order beyond the tightened limit must be rejected");
assert_eq!(rejects[0].reason, "rate limit exceeded: broker barrier");

Force-set Accumulated P&L

A bounds retune never moves booked P&L: it changes the thresholds and re-reads the engine's current live accumulator. Sometimes you need the opposite - to seed or correct the live accumulator itself, for example to reconcile against an external ledger of record after a restart or a manual position transfer.

Force-setting the P&L is an absolute assignment (upsert) of the live accumulated P&L for one (account, settlement asset) entry. It creates the entry if absent, exactly as a construction-time seed would, and the new value is evaluated against the live bounds on the next order.

One caveat follows from the kill-switch semantics. Forcing the accumulator past a bound trips the kill switch, and a tripped kill switch latches an engine-level account block. That block is a separate concern owned by the engine; force-setting the accumulator back inside the bound does not clear it - only an explicit admin unblock does. So this call can move an account into a blocked state but never out of one.

The example below registers a kill-switch policy with a broker barrier whose lower bound is -100 USD, admits an order while the account's P&L is 0, then force-sets the account's accumulated P&L to -150 USD. The next order for that account is rejected against the breached lower bound.

order is the AAPL/USD order built in Getting Started.

Go
usd, err := param.NewAsset("USD")
lowerBound, err := param.NewPnlFromString("-100")

// Register the kill-switch policy through Builtin so the engine keeps a
// handle to its accumulator; built-in policies are configurable by name.
engine, err := openpit.NewEngineBuilder().
	NoSync().
	Builtin(
		policies.BuildPnlBoundsKillswitch().BrokerBarriers(
			policies.PnlBoundsBrokerBarrier{
				SettlementAsset: usd,
				LowerBound:      optional.Some(lowerBound),
			},
		),
	).
	Build()
if err != nil {
	return err
}
defer engine.Stop()

// With no P&L history the order passes against the lower bound of -100.
reservation, rejects, err := engine.ExecutePreTrade(order)
if err != nil {
	return err
}
if rejects != nil {
	return fmt.Errorf("unexpected rejects: %v", rejects)
}
reservation.CommitAndClose()

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their type name (policies.PnlBoundsKillSwitchPolicyName).
forced, err := param.NewPnlFromString("-150")
if err != nil {
	return err
}
err = engine.Configure().SetAccountPnl(
	policies.PnlBoundsKillSwitchPolicyName,
	account,
	usd,
	forced,
)
if err != nil {
	return err
}

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
_, rejects, err = engine.ExecutePreTrade(order)
if err != nil {
	return err
}
fmt.Println(rejects[0].Reason) // "pnl kill switch triggered: broker barrier"
Python
import openpit
import openpit.pretrade.policies

# Register the kill-switch policy through builtin so the engine keeps a
# handle to its accumulator; built-in policies are configurable by name.
engine = (
    openpit.Engine.builder()
    .no_sync()
    .builtin(
        openpit.pretrade.policies.build_pnl_bounds_killswitch().broker_barriers(
            openpit.pretrade.policies.PnlBoundsBrokerBarrier(
                settlement_asset=openpit.param.Asset("USD"),
                lower_bound=openpit.param.Pnl(-100),
            ),
        )
    )
    .build()
)

# With no P&L history the order passes against the lower bound of -100.
execute_result = engine.execute_pre_trade(order=order)
assert execute_result.ok
execute_result.reservation.commit()

# Force-set the account's accumulated P&L to -150 USD, below the bound.
# Built-in policies register under their type name (PnlBoundsKillswitchBuilder.NAME).
engine.configure().set_account_pnl(
    openpit.pretrade.policies.PnlBoundsKillswitchBuilder.NAME,
    account=account,
    settlement_asset=openpit.param.Asset("USD"),
    pnl=openpit.param.Pnl(-150),
)

# The next order for that account breaches the lower bound and is rejected;
# the breach also latches an engine-level block on the account.
execute_result = engine.execute_pre_trade(order=order)
assert not execute_result.ok
assert execute_result.rejects[0].reason == "pnl kill switch triggered: broker barrier"
Rust
use openpit::param::{Asset, Pnl};
use openpit::pretrade::policies::{
    PnlBoundsBrokerBarrier,
    PnlBoundsKillSwitchPolicy,
    PnlBoundsKillSwitchSettings,
};
use openpit::storage::NoLocking;
use openpit::{Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact};

type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;

// Register the kill-switch policy so the engine keeps a handle to its
// accumulator; built-in policies are configurable by name.
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
    PnlBoundsKillSwitchSettings::new(
        [PnlBoundsBrokerBarrier {
            settlement_asset: Asset::new("USD")?,
            lower_bound: Some(Pnl::from_str("-100")?),
            upper_bound: None,
        }],
        [],
    )?,
    builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;

// With no P&L history the order passes against the lower bound of -100.
// `order` is the AAPL/USD order built in Getting Started.
engine.execute_pre_trade(order())?.commit();

// Force-set the account's accumulated P&L to -150 USD, below the bound.
// Built-in policies register under their own name (PnlBoundsKillSwitchPolicy::NAME).
let name = PnlBoundsKillSwitchPolicy::<NoLocking>::NAME;
engine
    .configure()
    .set_account_pnl(name, account, Asset::new("USD")?, Pnl::from_str("-150")?)?;

// The next order for that account breaches the lower bound and is rejected;
// the breach also latches an engine-level block on the account.
let rejects = engine.execute_pre_trade(order())
    .err()
    .expect("order beyond the breached bound must be rejected");
assert_eq!(rejects[0].reason, "pnl kill switch triggered: broker barrier");

Related Pages

Clone this wiki locally