Skip to main content

Overview

Dynamic rules let you make optimization config values change automatically based on time (cron schedules) and conditions (expressions evaluated against portfolio context). Instead of manually updating configs when market conditions change or calendar events occur, you define rules that activate automatically. Common use cases:
  • Increase tax-loss harvesting aggressiveness in December
  • Adjust position weight limits based on AUM
  • Change optimization mode based on portfolio characteristics
  • Apply different parameters for accounts meeting specific criteria

Rule Structure

Dynamic rules are attached to a config (organization, portfolio, or account level) via the dynamic_rules array. Each entry targets a specific config field and contains one or more rules:
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "cron": "",
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 0
        },
        {
          "cron": "",
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 1
        }
      ]
    }
  ]
}

Fields

FieldTypeRequiredDescription
field_namestringYesConfig field to set dynamically (e.g. max_weight, tax_preferences.tax_gamma)
rulesarrayYesOne or more rule entries

Rule Entry Fields

FieldTypeRequiredDefaultDescription
cronstringNo""Cron expression for time-based activation (e.g. * * * 12 * for December)
conditionstringNo""Boolean expression evaluated against context variables (e.g. aum > 1000000)
valueanyYesValue to apply when the rule matches
priorityintegerNo0Evaluation order (0-9999). Lowest number is evaluated first; first matching rule wins
A rule matches when both its cron schedule and condition are satisfied. An empty cron or condition field is treated as always matching, but at least one of the two must be provided.

Rule Resolution

For each dynamic rule field (e.g. max_weight), the system follows a three-step resolution path:

Step 1: Collect rules from all config levels

Rules from every config level are gathered and given a priority offset so that higher-level configs always take precedence:
Config levelPriority offsetPrecedence
Account rulespriority + 0Highest
Portfolio rulespriority + 10,000Middle
Org rulespriority + 20,000Lowest

Step 2: Find first static value and prune

Walk the config chain from most specific to least. The first level that sets a static value for the field becomes the default. Rules from levels below that level are pruned — rules at the same level still apply.
Account config has field?
 |
 |-- YES --> default = account value
 |           prune rules with priority >= 10,000
 |           (portfolio + org rules removed; account rules survive)
 |
 NO
 |
 v
Portfolio config has field?
 |
 |-- YES --> default = portfolio value
 |           prune rules with priority >= 20,000
 |           (org rules removed; account + portfolio rules survive)
 |
 NO
 |
 v
Org config has field?
 |
 |-- YES --> default = org value
 |           no pruning needed (nothing below org)
 |           (all rules survive)
 |
 NO
 |
 v
default = None (no static value anywhere, no rules pruned)

Step 3: Evaluate remaining rules

Remaining rules are sorted by priority ascending. First matching rule wins — the first rule whose cron and condition both evaluate to true is used. If no rule matches, the default value from step 2 is used. Key concepts:
  • Priority offsets: Account rules keep their raw priority, portfolio rules add 10,000, and org rules add 20,000. This means account-level rules are always evaluated before lower levels.
  • Static value pruning: When a static value is found at a config level, rules from levels below are pruned. Rules at the same level as the static value still apply. This prevents lower-level dynamic rules from overriding an explicit static value set at a higher level.
  • First match wins: Rules are evaluated in ascending priority order. The first rule whose cron and condition both match is used.
  • Fallback: If no rule matches, the static default value from step 2 is used.

Supported Fields

Config Fields

Dynamic rules can target any config field, including:
  • max_weight, min_weight — position weight limits
  • optimization_mode — optimization strategy
  • tax_gamma — tax-loss harvesting aggressiveness
  • cash_buffer, cash_buffer_usd, cash_buffer_cad — cash reserves
  • round_lot_size — trade rounding

Tax Preference Fields

Use the tax_preferences. prefix for tax preference fields:
  • tax_preferences.tax_gamma
  • tax_preferences.short_term_rate
  • tax_preferences.long_term_rate
Use the Get Dynamic Rules Schema endpoint to discover all supported fields, available context variables, functions, and operators for your organization.

Context Variables

Conditions in dynamic rules are evaluated against context variables that describe the portfolio and account at optimization time.

Portfolio-Level Variables

Variables like aum, cash_balance, and position_count describe the current state of the portfolio being optimized.

Account-Level Variables

Variables like organization_name and client_external_id describe the account and organization.

Fundamental Per-Ticker Variables

Variables like market_cap and pe_ratio provide fundamental data for individual securities. These are available when rules are evaluated at the ticker level.

Enrichment Columns

Custom enrichment data columns uploaded to your organization are also available as context variables.
The exact set of available variables depends on your organization’s configuration. Use the schema endpoint to discover what’s available.

Workflow

Step 1: Explore the Schema

Discover available fields, variables, operators, and functions:
curl --request GET \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/schema/ \
  --header 'X-Api-Key: <api-key>'
View API Reference → The response includes:
  • fields: Config fields that support dynamic rules
  • variables: Context variables available in conditions
  • functions: Functions you can use in expressions (e.g. abs(), max())
  • operators: Supported operators (>, <, ==, and, or, etc.)
  • examples: Example expressions

Step 2: Validate Expressions

Before deploying rules, validate that your condition expressions are syntactically correct:
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/validate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "expression": "aum > 1000000 and position_count < 50"
  }'
View API Reference → You can also pass a sample_context to test evaluation:
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/validate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "expression": "aum > 1000000",
    "sample_context": {"aum": 2000000}
  }'

Step 3: Attach Rules to a Config

Add dynamic rules when creating or updating a config at any level. For example, to add rules to a portfolio config:
curl --request PATCH \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/portfolio_configs/{portfolio_config_uuid}/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "customization_config": {
      "dynamic_rules": [
        {
          "field_name": "max_weight",
          "rules": [
            {
              "condition": "aum > 1000000",
              "value": 0.08,
              "priority": 0
            }
          ]
        }
      ]
    }
  }'

Step 4: Simulate Rule Evaluation

Test how rules resolve for a specific account and date without running an optimization:
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/simulate/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_account_uuid": "account-uuid-here",
    "trade_date": "2025-12-15"
  }'
View API Reference → The response shows:
  • resolved_values: The final config values after rule evaluation
  • context: The context variables used during evaluation
  • rule_evaluations: Which rules matched and which didn’t, with details
You can also pass dynamic_rules directly to simulate rules that haven’t been saved yet.

Step 5: Preview Optimization Impact

See how dynamic rules affect actual optimization results by comparing scenarios:
curl --request POST \
  --url https://api.tilt.io/api/v1/custom/org/{organization_uuid}/dynamic_rules/preview/ \
  --header 'X-Api-Key: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_account_uuid": "account-uuid-here",
    "scenarios": [
      {"trade_date": "2025-06-15"},
      {"trade_date": "2025-12-15"}
    ],
    "include_baseline": true
  }'
View API Reference →

Examples

Year-End Tax-Loss Harvesting

Increase the tax-loss harvesting aggressiveness during December:
{
  "dynamic_rules": [
    {
      "field_name": "tax_preferences.tax_gamma",
      "rules": [
        {
          "cron": "* * * 12 *",
          "value": 5.0,
          "priority": 0
        }
      ]
    }
  ]
}
Resolution in December (e.g. trade date 2025-12-15):
Rule priority 0: cron "* * * 12 *" → matches → value = 5.0 ✓ MATCH
                                                ────────
                                      Result:   tax_gamma = 5.0
Resolution outside December (e.g. trade date 2025-06-15):
Rule priority 0: cron "* * * 12 *" → does not match → skipped
                 No rules matched → use default from static config

AUM-Based Weight Limits

Tighten position concentration limits for larger portfolios:
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 0
        },
        {
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 1
        }
      ]
    }
  ]
}
Resolution for a portfolio with 2M AUM:
Rule priority 0: condition "aum > 1000000" → true  → value = 0.08 ✓ MATCH
                                                     ────────
                                           Result:   max_weight = 0.08
Resolution for a portfolio with 10M AUM:
Rule priority 0: condition "aum > 1000000" → true → value = 0.08 ✓ MATCH
                                                    ────────
                                          Result:   max_weight = 0.08
Both portfolios match the first rule. If you want different behavior for larger portfolios, put the more specific condition at a lower priority number so it matches first:
{
  "dynamic_rules": [
    {
      "field_name": "max_weight",
      "rules": [
        {
          "condition": "aum > 5000000",
          "value": 0.05,
          "priority": 0
        },
        {
          "condition": "aum > 1000000",
          "value": 0.08,
          "priority": 1
        }
      ]
    }
  ]
}
Now a 10M AUM portfolio matches priority 0 first and gets 0.05, while a 2M AUM portfolio skips priority 0 and matches priority 1 for 0.08.

Multi-Level Rules with Static Value Pruning

This example shows how rules accumulate across config levels and how a static value prunes lower-level rules. Config state for max_weight:
Account:    rule {cron: Dec → 0.50, priority: 1}   max_weight = NULL
Portfolio:  rule {always → 0.06, priority: 1}       max_weight = 0.05
Org:        rule {always → 0.03, priority: 1}       max_weight = NULL
Step 1 — Collect and offset rules:
Account rule:   {cron: Dec, value: 0.50, priority: 1 + 0     = 1}
Portfolio rule: {always,     value: 0.06, priority: 1 + 10000 = 10001}
Org rule:       {always,     value: 0.03, priority: 1 + 20000 = 20001}
Step 2 — Find static value and prune:
Account config  → max_weight = NULL → skip
Portfolio config → max_weight = 0.05 → default = 0.05
                   Prune rules with priority ≥ 10,000
                   ├── Portfolio rule (pri 10001) → PRUNED
                   └── Org rule       (pri 20001) → PRUNED

Remaining rules: [{cron: Dec, value: 0.50, priority: 1}]
Default value:   0.05
Step 3 — Evaluate remaining rules: In December:
Rule priority 1: cron "Dec" → matches → value = 0.50 ✓ MATCH
                                         ────────
                               Result:   max_weight = 0.50
In June:
Rule priority 1: cron "Dec" → does not match → skipped
                  No rules matched → use default
                                     ────────
                           Result:   max_weight = 0.05 (portfolio static)
The portfolio’s static max_weight = 0.05 serves two purposes: it becomes the fallback default, and it prunes both the portfolio-level and org-level dynamic rules. Only the account-level rule (which has higher precedence) survives.

Best Practices

  • Simulate before deploying — Always use the simulate endpoint to verify rule behavior before saving rules to a config. Check that the right rules match and produce expected values.
  • Start priorities at 0 — Use sequential integers starting from 0 (0, 1, 2, …). You can leave gaps if you anticipate inserting rules later.
  • Understand static blocking — Be intentional about static values at higher config levels, as they suppress dynamic rules from lower levels for that field.
  • Use the schema endpoint — Discover available fields and variables programmatically rather than hardcoding. The available set may expand over time.
  • Keep conditions simple — Prefer clear, readable expressions. Use the validate endpoint to catch syntax errors early.