scale-manager
Purpose
Turn "take partial off here" / "add on this break" / "trail the stop" into correct math and approved order payloads. This skill is a sibling to pretrade-risk (which handles the initial entry) and position-watchdog (which monitors a live position). This skill owns all in-flight position adjustments.
Environment routing
Demo (simulation) and live are two separate MCP servers. Account names are unique to one server. Once a workflow resolves an account on a server, every downstream call must go through that same server. This includes my_portfolio, market_snapshot, place_order, create_alert, history tools, etc. Cross-routing fails or hits the wrong environment.
MCP tools used
Tool names below are bare. The NinjaTrader MCP server provides them. Your client adds its own prefix. See AGENTS.md at the repo root.
my_portfolio— currentnetPos,netPrice, and working orders (the source of bracket stop/target prices). Prune withfields=["positions[].symbol","positions[].netPos","positions[].netPrice","workingOrders[]"].order_details— event/fill history for one knownorderId.market_snapshot—lastPrice,tickSize,valuePerPoint. Prune withfields=["snapshots[].symbol","snapshots[].lastPrice","snapshots[].tickSize","snapshots[].valuePerPoint"].estimate_order— final feasibility gate for scale-ins (the server's pre-trade risk check)risk_settings— daily-loss budget, for the cross-check in Step 4- Final execution paths (user approves):
modify_order,place_order,close_position
Skills consumed
market-context— ATR for band spacing, regime for pyramid-eligibilitypretrade-risk— pending-qty rule for scale-ins (included-in-estimate)
Workflow
1. Classify the intent
| User phrasing | Action |
|---|---|
| "take partial", "trim 50%", "half off" | scale_math.py --action partial_exit --pct 0.5 |
| "scale out", "close 2 of 4" | scale_math.py --action scale_out |
| "add to winner", "pyramid", "add more" | scale_math.py --action scale_in |
| "move stop to breakeven" | modify_order on bracket stop leg |
| "trail stop to X" | modify_order on bracket stop leg |
| "ladder into 3 at 7148–7142" | ladder.py --mode even |
The full pattern library, with preconditions and narrations, lives in references/scale-patterns.md.
2. Gather current state
my_portfolio # netPos, netPrice, workingOrders[] (bracket stop/target prices)
market_snapshot(...) # lastPrice, tickSize, valuePerPoint
If netPos == 0 and the action is scale_in/out, defer to pretrade-risk. This is an initial entry, not a scale.
3. Compute the scale math
Save the script's JSON input to a file and pass its path with --file. Never re-type or inline a large JSON payload into the command.
For scale-in / scale-out / partial-exit:
python3 scripts/scale_math.py --file position_state.json --action <scale_in|scale_out|partial_exit>
The caller maps input fields from the MCP response's camelCase into the script's own JSON contract. Fields: direction, current_qty, current_basis, stop_price, current_mark (from lastPrice), value_per_point (from valuePerPoint), tick_size (from tickSize), plus action_qty and action_price (or --pct for partial_exit).
Output fields:
basis_after— new weighted cost basis (scale_in only; scale_out leaves the basis unchanged on the remainder)dollar_risk_before_action/dollar_risk_after_action— critical for scale-in. A scale-in typically RAISES dollar risk, because the combined qty × (new_basis − stop) exceeds the original risk.R_before/R_afterat the current markrealized_dollars— money booked on a scale_out (0 for scale_in)
For ladder entry:
python3 scripts/ladder.py --file anchor_state.json --mode <even|atr|custom> --legs N ...
Returns per-leg price + qty + weight, combined weighted basis, and best/worst-case fill scenarios.
4. Surface the dollar and R impact to the user
Three must-haves in the narration:
- Dollar realized (for exits) or dollar risk change (for adds).
- R before vs after — did the trade get easier or harder to manage?
- Compliance with the daily-loss budget (cross-check against
risk_settings.dailyLossAutoLiqfor scale-ins that expand risk).
5. Build the MCP payload (for user approval)
Never call modify_order, place_order, or close_position from this skill. This step writes payload text only. Show the plan and the exact payloads, then wait for user approval. The user submits every call in this step. So every block below describes the user's approved sequence, not an action this skill takes.
See references/scale-patterns.md § "Converting output to MCP payloads".
Scale out — close_position always closes the full position. It takes no quantity parameter. A partial exit needs a separate market order for the offset quantity. It also needs a resize of BOTH remaining bracket legs to match — never the target leg alone. The server does not link the exit order and the two bracket legs together. A stop that keeps the ORIGINAL quantity over-protects the smaller remaining position. Worse, a same-quantity stop can flip the trade to the opposite side if it fills. Example: close 2 of a long 4, and leave a same-quantity Sell Stop for 4. If that stop triggers, the trader now holds a short 2, not a flat position. A same-quantity target carries the mirror risk — a full fill flips the position too.
place_order(action=<opposing side>, orderType=Market, quantity=<off_qty>, symbol=<sym>, timeInForce=<Day|GTC|IOC|FOK|GTD>)
The user waits for the exit fill to confirm. The user then pulls my_portfolio again, and checks the new netPos against remaining_qty. The plan needs both leg IDs from workingOrders[] (see the "Stop move" step below for the filter). When the server sets bracket.parentId or bracket.ocoId, use it to confirm the pair.
modify_order(orderId=<bracket_stop_leg_id>, quantity=<remaining_qty>)
modify_order(orderId=<bracket_target_leg_id>, quantity=<remaining_qty>)
remaining_qty is the current quantity on either leg minus off_qty.
If a resize call fails, the position now carries a mis-sized leg. The caller can also skip the resize. In either case, do not treat the scale as done. Tell the user which leg still carries the ORIGINAL quantity. Propose the resize again first, before any other change to the position.
Scale in — the place_order payload for the add:
place_order(
action=<Buy|Sell>, # same side as current direction
orderType=<Market|Limit>,
quantity=<add_qty>,
price=<add_price>, # for Limit
symbol=<sym>,
timeInForce=<Day|GTC|IOC|FOK|GTD>,
)
For limit scale-ins, consider the pending-qty rule. The server-side pre-trade risk check includes all open, filled, and pending qty when it rates the order. If you chain multiple scale-ins as limits, the SECOND order's feasibility check includes the FIRST's pending qty. See pretrade-risk/references/risk-rules.md for details.
Stop move (breakeven / trail):
modify_order(orderId=<bracket_stop_leg_id>, stopPrice=<new>)
Find the stop leg id in my_portfolio's workingOrders[] — filter by symbol, an opposing action, and orderType: Stop|StopLimit. Find the target leg the same way, with orderType: Limit and price set instead of stopPrice. When bracket.parentId or bracket.ocoId is present, use it to confirm the two legs share one entry. The scale-out resize above needs both leg IDs — reuse this same filter for that step.
modify_order.stopPrice is an absolute price, in contract-native units — the same convention as scale_math.py's output. This is asymmetric with place_order.brackets[].stopLoss, which is a signed delta from entry (see pretrade-risk/SKILL.md §4a). Pass the new absolute stop price straight through. Do not convert it to an offset.
No server-side pre-flight exists for modify_order — estimate_order covers new orders only; it takes no orderId parameter. So validate the proposed stop yourself, before you show the payload:
- Long position: the new stop must sit below the current mark. It must also stay on the safe side of any breakeven cross. A breakeven move that lands at or just past entry is fine. A trail that crosses through the current mark flattens the market position.
- Short position: mirror this — the new stop must sit above the current mark.
Wrong-side stops trigger immediately on submission with no warning.
Ladder entry — one order per leg:
After approval, the user runs one pre-flight and one order per leg:
for leg in legs:
# Pre-flight each leg — the server's pre-trade risk check counts
# already-pending working orders from prior legs in this loop.
estimate_order(
account=acct, symbol=sym, action=..., quantity=leg.qty,
orderType=Limit, price=leg.price
)
place_order(
action=..., orderType=Limit, quantity=leg.qty, price=leg.price,
timeInForce=<Day|GTC|IOC|FOK|GTD>,
)
Each placed leg becomes pending qty for the next leg's pre-trade check. See pretrade-risk/references/risk-rules.md § pending-qty rule. A scale-in with its own bracket follows the offset rule from pretrade-risk/SKILL.md §4a — place_order.brackets[].profitTarget and .stopLoss carry a sign. They are deltas, not absolute prices.
6. Final feasibility check
For any scale-in that expands qty (a single add or each ladder leg), end with estimate_order on the proposed payload. It surfaces the authoritative pre-trade risk check result. If feasible: false, report the reason and do not submit.
estimate_order does NOT cover modify_order. The LLM must sanity-check stop-move proposals against the side-of-current-mark rule in Step 5's "Stop move" subsection. There is no server-side dry-run for modifies.
7. Emit the plan and payloads; wait for user approval
This restates the gate at the top of Step 5. Never call modify_order, place_order, or close_position from this skill. Always show the plan and the exact payloads, then stop. The user submits the approved calls.
Output idioms
Classic partial at 1R:
"Long 4 ES @ 7150, mark 7168 (2R). Propose: close 2 at market — realizes $1,800, leaves 2 at the same stop price 7141 ($900 at risk). After the fill confirms, resize BOTH remaining legs to quantity 2:
place_order(action=\"Sell\", orderType=\"Market\", quantity=2, symbol=\"ESU6\"), thenmodify_order(orderId=<stop_leg>, quantity=2)andmodify_order(orderId=<target_leg>, quantity=2). Approve?"
Pyramid with risk flag:
"Add 2 ES @ 7168 → new basis 7156, dollar risk at stop 7141 jumps from $1,800 to $4,500 (2.5× current). Options: (a) Approve as-is (accepts higher risk). (b) Tighten stop to 7155 simultaneously → risk stays $300. (c) Treat as a separate trade with its own tighter stop.
estimate_ordersays feasible with margin headroom."
Ladder entry:
"3-leg ladder 7148 / 7145 / 7142 (6pt band, ≈0.75×ATR). Qty 1/1/1. If only the top leg fills, basis $7148 (1 contract, lightly sized). If all fill, avg basis $7145 across 3 contracts. Combined risk at stop 7140 = $450 (anchor only) to $750 (all fill). Payloads: 3×
place_order(orderType=Limit, quantity=1, price=...)."
Design constraints
- Never executes. Produces plans and payloads. Even after the user approves, the user triggers the MCP call, not the skill.
- Dollar-risk-aware. Scale-ins typically grow dollar risk. Always surface the before/after delta so the user decides eyes-open.
- Tick-snapped.
ladder.pysnaps leg prices to the symbol's tick grid.place_orderrejects bad prices anyway, but the snap keeps the output clean. - Server-authoritative.
estimate_orderis the last gate on scale-ins. If it says no, the skill reports that and stops.
Disambiguation
- vs
pretrade-risk: pretrade handles the initial entry (netPos = 0). scale-manager handles netPos ≠ 0 adjustments. Always checknetPosfirst and route accordingly. - vs
position-watchdog: watchdog reports health (P&L, R, distance to stop, flags) informationally. scale-manager produces executable plans. Watchdog can PROPOSE a scale via its alert hook; scale-manager validates and composes the payload. - vs
close_positionMCP directly:close_positionalways closes the full position — it takes noquantityparameter. For a literal "close half the ES position", route through this skill's partial-exit pattern (Step 5) instead. Useclose_positiondirectly only when the user wants the whole position flat.
Explicit non-goals
- No execution. The MCP order tools stay in the user's approval loop.
- No indicator-driven triggers. This skill proposes a scale when the user asks. It does not watch the market and auto-propose. (Alert-based monitoring is
alerts-composer+position-watchdog.) - No cross-account rebalancing. Scales apply to ONE symbol on ONE account.
- No pyramid recursion. The math supports nested scale-ins, but the narrations cover only one add at a time. Ask the user explicitly before you compound them.
Resource layout
scripts/scale_math.py— scale-in / scale-out / partial-exit math. Stdin JSON for position state and action params. Actions:--action scale_in|scale_out|partial_exit.scripts/ladder.py— ladder entry splits. Modes:even,atr,custom. Weighting:equal,front,back,custom. Integer-quantity allocation uses the largest-remainder method, so the total exactly matches the inputtotal_qty.references/scale-patterns.md— 7 named patterns: partial at 1R, runner, pyramid, and ladder mean-reversion. It also covers ladder breakout retest, pre-event scale-out, and full exit on reversal. The file also has preconditions, narrations, and the MCP-payload translation table. Load it when you pick a pattern or build the MCP payload.