Four behavior-preserving DRY refactors on the autotrade-foundation branch. Files touched: commands.rs, execute.rs, reconcile.rs, positions.rs, runner.rs, repo.rs (all under pmv2/autotrade/). Gate result: EXIT 0, 333 tests pass, clippy clean, no behavior changed.

1. collecting_notify helper (commands.rs)

Both run_reconcile and run_sell previously built an identical Arc<Mutex<Vec<String>>> + closure inline. Extracted into:

fn collecting_notify() -> (Arc<Mutex<Vec<String>>>, impl Fn(String) + Send + Sync)

Each call site keeps its own drain policy:

  • run_reconcile — drains up to 20 + "(+N more)" suffix
  • run_sell — drains all via Arc::try_unwrap

2. AutotradeDeps::market() + build_autotrade_deps reuse in run_sell (runner.rs, positions.rs, commands.rs)

Three verbatim ClobMarketData { clob: &..., gamma: &..., pool } struct literals were spread across runner.rs::run_exits, positions.rs::run_autotrade_entry, and commands.rs::run_sell. Consolidated by:

  • Adding a method on AutotradeDeps:
    pub fn market<'a>(&'a self, pool: &'a PgPool) -> ClobMarketData<'a>
  • Promoting build_autotrade_deps from private to pub
  • run_sell now calls build_autotrade_deps instead of building clob + gamma + signer separately

All three call sites become deps.market(pool).

3. FillOutcome enum + record_fill_then_open fn (execute.rs)

record_placed_fill (in execute.rs) and heal_filled (in reconcile.rs) shared an identical DB sequence: update a fill row, then conditionally reopen. Extracted into a shared helper:

pub enum FillOutcome {
    Reopened,
    RecordedNotReopened,
    Noop,
    DbError(String),
}
 
pub async fn record_fill_then_open(
    pool, id, clob_order_id, filled_size, avg_fill_price,
    fee_usd, cost_basis, status,
) -> FillOutcome

Each call site keeps its own notify strings and return types — only the DB sequence is shared.

4. Dead repo functions removed (repo.rs)

Two functions carrying #[allow(dead_code)] with zero non-test callers were deleted:

FunctionWhy removed
repo::mark_dry_runDry path INSERTs status='dry_run' directly in try_reserve; this wrapper was never called
repo::load_autotrade_orderSuperseded by autotrade_sellable_by_id + load_reconcilable_entries; no callers

AutotradeOrderRow struct was retained — still used by load_reconcilable_entries.

For Agents

These are purely structural refactors. No API surface changed, no behavior changed, no new migrations. The FillOutcome enum and record_fill_then_open helper live in execute.rs and are the correct seam for any future fill-recording logic.

Quick Reference

  • Shared notify factory: collecting_notify() in commands.rs
  • Market data shorthand: deps.market(pool) — needs AutotradeDeps in scope
  • Fill+reopen seam: execute::record_fill_then_open — returns FillOutcome
  • Dead code removed: repo::mark_dry_run, repo::load_autotrade_order