Skip to main content

nautilus_infrastructure/sql/
queries.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use ahash::AHashMap;
17use nautilus_common::signal::Signal;
18use nautilus_model::{
19    accounts::AccountAny,
20    data::{Bar, CustomData, DataType, HasTsInit, InstrumentClose, QuoteTick, TradeTick},
21    events::{
22        AccountState, OrderEvent, OrderEventAny, OrderFilled, OrderInitialized, OrderSnapshot,
23        position::snapshot::PositionSnapshot,
24    },
25    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, TraderId},
26    instruments::{Instrument, InstrumentAny},
27    orders::OrderAny,
28    position::Position,
29    types::Currency,
30};
31use sqlx::{PgPool, Postgres, Row, Transaction};
32
33use super::models::{orders::OrderSnapshotRow, positions::PositionSnapshotRow, types::SignalRow};
34use crate::sql::models::{
35    accounts::AccountEventRow,
36    data::{BarRow, InstrumentCloseRow, QuoteTickRow, TradeTickRow},
37    enums::{
38        AggregationSourcePg, AggressorSidePg, AssetClassPg, BarAggregationPg, CurrencyTypePg,
39        PriceTypePg, TrailingOffsetTypePg,
40    },
41    general::{GeneralRow, OrderEventOrderClientIdCombination, OrderPositionIndexRow},
42    instruments::InstrumentAnyRow,
43    orders::{OrderEventAnyRow, OrderFilledRow},
44    types::CurrencyRow,
45};
46
47#[derive(Debug)]
48pub struct DatabaseQueries;
49
50impl DatabaseQueries {
51    /// Truncates all tables in the cache database via the provided Postgres `pool`.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the TRUNCATE operation fails.
56    pub async fn truncate(pool: &PgPool) -> anyhow::Result<()> {
57        sqlx::query("SELECT truncate_all_tables()")
58            .execute(pool)
59            .await
60            .map(|_| ())
61            .map_err(|e| anyhow::anyhow!("Failed to truncate tables: {e}"))
62    }
63
64    /// Deletes the order, position and account rows of `trader_id` via the provided `pool`.
65    ///
66    /// Shared reference data (currencies, instruments, instrument closes), market data, the
67    /// general key-value table, signals, custom data and blockchain tables are left untouched.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if any DELETE operation fails. Any error rolls back the complete flush.
72    pub async fn delete_trader(pool: &PgPool, trader_id: &TraderId) -> anyhow::Result<()> {
73        let mut transaction = pool.begin().await?;
74
75        let statements = [
76            r#"DELETE FROM "order_position_index" WHERE trader_id = $1"#,
77            r#"DELETE FROM "order_event" WHERE trader_id = $1"#,
78            r#"DELETE FROM "order" WHERE trader_id = $1"#,
79            r#"DELETE FROM "position_event" WHERE trader_id = $1"#,
80            r#"DELETE FROM "position" WHERE trader_id = $1"#,
81            r#"DELETE FROM "account_event" WHERE trader_id = $1"#,
82        ];
83
84        for statement in statements {
85            sqlx::query(statement)
86                .bind(trader_id.to_string())
87                .execute(&mut *transaction)
88                .await
89                .map_err(|e| anyhow::anyhow!("Failed to delete rows for {trader_id}: {e}"))?;
90        }
91
92        transaction
93            .commit()
94            .await
95            .map_err(|e| anyhow::anyhow!("Failed to commit delete for {trader_id}: {e}"))
96    }
97
98    /// Inserts or replaces a raw key-value entry in the `general` table via the provided `pool`.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the INSERT or UPDATE operation fails.
103    pub async fn add(pool: &PgPool, key: String, value: Vec<u8>) -> anyhow::Result<()> {
104        sqlx::query(
105            "INSERT INTO general (id, value) VALUES ($1, $2) \
106             ON CONFLICT (id) DO UPDATE SET value = EXCLUDED.value",
107        )
108        .bind(key)
109        .bind(value)
110        .execute(pool)
111        .await
112        .map(|_| ())
113        .map_err(|e| anyhow::anyhow!("Failed to insert into general table: {e}"))
114    }
115
116    /// Loads all entries from the `general` table via the provided `pool`.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the SELECT operation fails.
121    pub async fn load(pool: &PgPool) -> anyhow::Result<AHashMap<String, Vec<u8>>> {
122        sqlx::query_as::<_, GeneralRow>("SELECT * FROM general")
123            .fetch_all(pool)
124            .await
125            .map(|rows| {
126                let mut cache: AHashMap<String, Vec<u8>> = AHashMap::new();
127                for row in rows {
128                    cache.insert(row.id, row.value);
129                }
130                cache
131            })
132            .map_err(|e| anyhow::anyhow!("Failed to load general table: {e}"))
133    }
134
135    /// Inserts or ignores a `Currency` row via the provided `pool`.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the INSERT operation fails.
140    pub async fn add_currency(pool: &PgPool, currency: Currency) -> anyhow::Result<()> {
141        sqlx::query(
142            "INSERT INTO currency (id, precision, iso4217, name, currency_type) VALUES ($1, $2, $3, $4, $5::currency_type) ON CONFLICT (id) DO NOTHING"
143        )
144            .bind(currency.code.as_str())
145            .bind(i32::from(currency.precision))
146            .bind(i32::from(currency.iso4217))
147            .bind(currency.name.as_str())
148            .bind(CurrencyTypePg(currency.currency_type))
149            .execute(pool)
150            .await
151            .map(|_| ())
152            .map_err(|e| anyhow::anyhow!("Failed to insert into currency table: {e}"))
153    }
154
155    /// Loads all `Currency` entries via the provided `pool`.
156    ///
157    /// # Errors
158    ///
159    /// Returns an error if the SELECT operation fails.
160    pub async fn load_currencies(pool: &PgPool) -> anyhow::Result<Vec<Currency>> {
161        sqlx::query_as::<_, CurrencyRow>("SELECT * FROM currency ORDER BY id ASC")
162            .fetch_all(pool)
163            .await
164            .map(|rows| rows.into_iter().map(|row| row.0).collect())
165            .map_err(|e| anyhow::anyhow!("Failed to load currencies: {e}"))
166    }
167
168    /// Loads a single `Currency` entry by `code` via the provided `pool`.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if the SELECT operation fails.
173    pub async fn load_currency(pool: &PgPool, code: &str) -> anyhow::Result<Option<Currency>> {
174        sqlx::query_as::<_, CurrencyRow>("SELECT * FROM currency WHERE id = $1")
175            .bind(code)
176            .fetch_optional(pool)
177            .await
178            .map(|currency| currency.map(|row| row.0))
179            .map_err(|e| anyhow::anyhow!("Failed to load currency: {e}"))
180    }
181
182    /// Inserts or updates an `InstrumentAny` entry via the provided `pool`.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the INSERT or UPDATE operation fails.
187    pub async fn add_instrument(
188        pool: &PgPool,
189        kind: &str,
190        instrument: Box<dyn Instrument>,
191    ) -> anyhow::Result<()> {
192        sqlx::query(r#"
193            INSERT INTO "instrument" (
194                id, kind, raw_symbol, base_currency, underlying, quote_currency, settlement_currency, isin, asset_class, exchange,
195                strategy_type, multiplier, option_kind, is_inverse, strike_price, activation_ns, expiration_ns, price_precision, size_precision,
196                price_increment, size_increment, margin_init, margin_maint, lot_size, max_quantity, min_quantity, max_notional,
197                min_notional, max_price, min_price, ts_init, ts_event, info, created_at, updated_at
198            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::asset_class, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33::json, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
199            ON CONFLICT (id)
200            DO UPDATE
201            SET
202                kind = $2, raw_symbol = $3, base_currency= $4, underlying = $5, quote_currency = $6, settlement_currency = $7, isin = $8, asset_class = $9, exchange = $10,
203                 strategy_type = $11, multiplier = $12, option_kind = $13, is_inverse = $14, strike_price = $15, activation_ns = $16, expiration_ns = $17 , price_precision = $18, size_precision = $19,
204                 price_increment = $20, size_increment = $21, margin_init = $22, margin_maint = $23, lot_size = $24, max_quantity = $25,
205                 min_quantity = $26, max_notional = $27, min_notional = $28, max_price = $29, min_price = $30, ts_init = $31,  ts_event = $32, info = $33::json, updated_at = CURRENT_TIMESTAMP
206            "#)
207            .bind(instrument.id().to_string())
208            .bind(kind)
209            .bind(instrument.raw_symbol().to_string())
210            .bind(instrument.base_currency().map(|x| x.code.as_str()))
211            .bind(instrument.underlying().map(|x| x.to_string()))
212            .bind(instrument.quote_currency().code.as_str())
213            .bind(instrument.settlement_currency().code.as_str())
214            .bind(instrument.isin().map(|x| x.to_string()))
215            .bind(AssetClassPg(instrument.asset_class()))
216            .bind(instrument.exchange().map(|x| x.to_string()))
217            .bind(instrument.strategy_type().map(|x| x.to_string()))
218            .bind(instrument.multiplier().to_string())
219            .bind(instrument.option_kind().map(|x| x.to_string()))
220            .bind(instrument.is_inverse())
221            .bind(instrument.strike_price().map(|x| x.to_string()))
222            .bind(instrument.activation_ns().map(|x| x.to_string()))
223            .bind(instrument.expiration_ns().map(|x| x.to_string()))
224            .bind(i32::from(instrument.price_precision()))
225            .bind(i32::from(instrument.size_precision()))
226            .bind(instrument.price_increment().to_string())
227            .bind(instrument.size_increment().to_string())
228            .bind(instrument.margin_init().to_string())
229            .bind(instrument.margin_maint().to_string())
230            .bind(instrument.lot_size().map(|x| x.to_string()))
231            .bind(instrument.max_quantity().map(|x| x.to_string()))
232            .bind(instrument.min_quantity().map(|x| x.to_string()))
233            .bind(instrument.max_notional().map(|x| x.to_string()))
234            .bind(instrument.min_notional().map(|x| x.to_string()))
235            .bind(instrument.max_price().map(|x| x.to_string()))
236            .bind(instrument.min_price().map(|x| x.to_string()))
237            .bind(instrument.ts_init().to_string())
238            .bind(instrument.ts_event().to_string())
239            .bind(instrument.info().map(serde_json::to_string).transpose()?)
240            .execute(pool)
241            .await
242            .map(|_| ())
243            .map_err(|e| anyhow::anyhow!("Failed to insert item {} into instrument table: {:?}", instrument.id(), e))
244    }
245
246    /// Loads a single `InstrumentAny` entry by `instrument_id` via the provided `pool`.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the SELECT operation fails.
251    pub async fn load_instrument(
252        pool: &PgPool,
253        instrument_id: &InstrumentId,
254    ) -> anyhow::Result<Option<InstrumentAny>> {
255        sqlx::query_as::<_, InstrumentAnyRow>("SELECT * FROM instrument WHERE id = $1")
256            .bind(instrument_id.to_string())
257            .fetch_optional(pool)
258            .await
259            .map(|instrument| instrument.map(|row| row.0))
260            .map_err(|e| {
261                anyhow::anyhow!("Failed to load instrument with id {instrument_id},error is: {e}")
262            })
263    }
264
265    /// Loads all `InstrumentAny` entries via the provided `pool`.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if the SELECT operation fails.
270    pub async fn load_instruments(pool: &PgPool) -> anyhow::Result<Vec<InstrumentAny>> {
271        sqlx::query_as::<_, InstrumentAnyRow>("SELECT * FROM instrument")
272            .fetch_all(pool)
273            .await
274            .map(|rows| rows.into_iter().map(|row| row.0).collect())
275            .map_err(|e| anyhow::anyhow!("Failed to load instruments: {e}"))
276    }
277
278    /// Inserts or replaces an `InstrumentClose`.
279    ///
280    /// # Errors
281    ///
282    /// Returns an error if the SQL INSERT or UPDATE fails.
283    pub async fn add_instrument_close(
284        pool: &PgPool,
285        close: &InstrumentClose,
286    ) -> anyhow::Result<()> {
287        sqlx::query(
288            r#"
289            INSERT INTO "instrument_close" (
290                instrument_id, close_price, close_type, ts_event, ts_init, created_at
291            ) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
292            ON CONFLICT (instrument_id) DO UPDATE
293            SET close_price = EXCLUDED.close_price,
294                close_type = EXCLUDED.close_type,
295                ts_event = EXCLUDED.ts_event,
296                ts_init = EXCLUDED.ts_init
297            "#,
298        )
299        .bind(close.instrument_id.to_string())
300        .bind(close.close_price.to_string())
301        .bind(close.close_type.to_string())
302        .bind(close.ts_event.to_string())
303        .bind(close.ts_init.to_string())
304        .execute(pool)
305        .await
306        .map(|_| ())
307        .map_err(|e| anyhow::anyhow!("Failed to insert instrument close: {e}"))
308    }
309
310    /// Loads all `InstrumentClose` entries.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if the SQL SELECT or row decoding fails.
315    pub async fn load_instrument_closes(pool: &PgPool) -> anyhow::Result<Vec<InstrumentClose>> {
316        sqlx::query_as::<_, InstrumentCloseRow>(
317            "SELECT * FROM instrument_close ORDER BY instrument_id ASC",
318        )
319        .fetch_all(pool)
320        .await
321        .map(|rows| rows.into_iter().map(|row| row.0).collect())
322        .map_err(|e| anyhow::anyhow!("Failed to load instrument closes: {e}"))
323    }
324
325    /// Inserts an `OrderInitialized` event via the provided `pool`.
326    ///
327    /// # Errors
328    ///
329    /// Returns an error if the SQL INSERT or UPDATE operation fails.
330    pub async fn add_order(
331        pool: &PgPool,
332        event: OrderInitialized,
333        client_id: Option<ClientId>,
334    ) -> anyhow::Result<()> {
335        Self::add_order_event(pool, Box::new(event), client_id).await
336    }
337
338    /// Inserts an `OrderSnapshot` entry via the provided `pool`.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error if the SQL INSERT operation fails.
343    ///
344    /// # Panics
345    ///
346    /// Panics if serialization of `snapshot.exec_algorithm_params` fails.
347    #[expect(
348        clippy::too_many_lines,
349        reason = "order snapshot persistence maps the full database schema in one transaction"
350    )]
351    pub async fn add_order_snapshot(pool: &PgPool, snapshot: OrderSnapshot) -> anyhow::Result<()> {
352        let mut transaction = pool.begin().await?;
353
354        // Insert trader if it does not exist
355        // TODO remove this when node and trader initialization is implemented
356        sqlx::query(
357            r#"
358            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
359            "#,
360        )
361        .bind(snapshot.trader_id.to_string())
362        .execute(&mut *transaction)
363        .await
364        .map(|_| ())
365        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
366
367        sqlx::query(
368            r#"
369            INSERT INTO "order" (
370                id, trader_id, strategy_id, instrument_id, client_order_id, venue_order_id, position_id,
371                account_id, last_trade_id, order_type, order_side, quantity, price, trigger_price,
372                trigger_type, limit_offset, trailing_offset, trailing_offset_type, time_in_force,
373                expire_time, filled_qty, liquidity_side, avg_px, slippage, commissions, status,
374                is_post_only, is_reduce_only, is_quote_quantity, display_qty, emulation_trigger,
375                trigger_instrument_id, contingency_type, order_list_id, linked_order_ids,
376                parent_order_id, exec_algorithm_id, exec_algorithm_params, exec_spawn_id, tags, init_id, ts_init, ts_last,
377                activation_price, created_at, updated_at
378            ) VALUES (
379                $1, $2, $3, $4, $1, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
380                $17::TRAILING_OFFSET_TYPE, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28,
381                $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43,
382                CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
383            )
384            ON CONFLICT (trader_id, id)
385            DO UPDATE SET
386                strategy_id = $3,
387                instrument_id = $4,
388                venue_order_id = $5,
389                position_id = $6,
390                account_id = $7,
391                last_trade_id = $8,
392                order_type = $9,
393                order_side = $10,
394                quantity = $11,
395                price = $12,
396                trigger_price = $13,
397                trigger_type = $14,
398                limit_offset = $15,
399                trailing_offset = $16,
400                trailing_offset_type = $17::TRAILING_OFFSET_TYPE,
401                time_in_force = $18,
402                expire_time = $19,
403                filled_qty = $20,
404                liquidity_side = $21,
405                avg_px = $22,
406                slippage = $23,
407                commissions = $24,
408                status = $25,
409                is_post_only = $26,
410                is_reduce_only = $27,
411                is_quote_quantity = $28,
412                display_qty = $29,
413                emulation_trigger = $30,
414                trigger_instrument_id = $31,
415                contingency_type = $32,
416                order_list_id = $33,
417                linked_order_ids = $34,
418                parent_order_id = $35,
419                exec_algorithm_id = $36,
420                exec_algorithm_params = $37,
421                exec_spawn_id = $38,
422                tags = $39,
423                init_id = $40,
424                ts_init = $41,
425                ts_last = $42,
426                activation_price = $43,
427                updated_at = CURRENT_TIMESTAMP
428        "#)
429            .bind(snapshot.client_order_id.to_string())  // Used for both id and client_order_id
430            .bind(snapshot.trader_id.to_string())
431            .bind(snapshot.strategy_id.to_string())
432            .bind(snapshot.instrument_id.to_string())
433            .bind(snapshot.venue_order_id.map(|x| x.to_string()))
434            .bind(snapshot.position_id.map(|x| x.to_string()))
435            .bind(snapshot.account_id.map(|x| x.to_string()))
436            .bind(snapshot.last_trade_id.map(|x| x.to_string()))
437            .bind(snapshot.order_type.to_string())
438            .bind(snapshot.order_side.to_string())
439            .bind(snapshot.quantity.to_string())
440            .bind(snapshot.price.map(|x| x.to_string()))
441            .bind(snapshot.trigger_price.map(|x| x.to_string()))
442            .bind(snapshot.trigger_type.map(|x| x.to_string()))
443            .bind(snapshot.limit_offset.map(|x| x.to_string()))
444            .bind(snapshot.trailing_offset.map(|x| x.to_string()))
445            .bind(
446                snapshot
447                    .trailing_offset_type
448                    .map(|value| TrailingOffsetTypePg(Some(value))),
449            )
450            .bind(snapshot.time_in_force.to_string())
451            .bind(snapshot.expire_time.map(|x| x.to_string()))
452            .bind(snapshot.filled_qty.to_string())
453            .bind(snapshot.liquidity_side.map(|x| x.to_string()))
454            .bind(snapshot.avg_px)
455            .bind(snapshot.slippage)
456            .bind(snapshot.commissions.iter().map(ToString::to_string).collect::<Vec<String>>())
457            .bind(snapshot.status.to_string())
458            .bind(snapshot.is_post_only)
459            .bind(snapshot.is_reduce_only)
460            .bind(snapshot.is_quote_quantity)
461            .bind(snapshot.display_qty.map(|x| x.to_string()))
462            .bind(
463                snapshot
464                    .emulation_trigger
465                    .map_or_else(|| "NO_TRIGGER".to_string(), |value| value.to_string()),
466            )
467            .bind(snapshot.trigger_instrument_id.map(|x| x.to_string()))
468            .bind(snapshot.contingency_type.map_or_else(
469                || "NO_CONTINGENCY".to_string(),
470                |value| value.to_string(),
471            ))
472            .bind(snapshot.order_list_id.map(|x| x.to_string()))
473            .bind(snapshot.linked_order_ids.map(|x| x.iter().map(ToString::to_string).collect::<Vec<String>>()))
474            .bind(snapshot.parent_order_id.map(|x| x.to_string()))
475            .bind(snapshot.exec_algorithm_id.map(|x| x.to_string()))
476            .bind(snapshot.exec_algorithm_params.map(|x| serde_json::to_value(x).unwrap()))
477            .bind(snapshot.exec_spawn_id.map(|x| x.to_string()))
478            .bind(snapshot.tags.map(|x| x.iter().map(ToString::to_string).collect::<Vec<String>>()))
479            .bind(snapshot.init_id.to_string())
480            .bind(snapshot.ts_init.to_string())
481            .bind(snapshot.ts_last.to_string())
482            .bind(snapshot.activation_price.map(|x| x.to_string()))
483            .execute(&mut *transaction)
484            .await
485            .map(|_| ())
486            .map_err(|e| anyhow::anyhow!("Failed to insert into order table: {e}"))?;
487
488        transaction
489            .commit()
490            .await
491            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
492    }
493
494    /// Loads an `OrderSnapshot` entry by client order ID via the provided `pool`.
495    ///
496    /// # Errors
497    ///
498    /// Returns an error if the SQL SELECT or deserialization fails.
499    pub async fn load_order_snapshot(
500        pool: &PgPool,
501        client_order_id: &ClientOrderId,
502        trader_id: &TraderId,
503    ) -> anyhow::Result<Option<OrderSnapshot>> {
504        sqlx::query_as::<_, OrderSnapshotRow>(
505            r#"SELECT * FROM "order" WHERE client_order_id = $1 AND trader_id = $2"#,
506        )
507        .bind(client_order_id.to_string())
508        .bind(trader_id.to_string())
509        .fetch_optional(pool)
510        .await
511        .map(|row| row.map(|row| row.0))
512        .map_err(|e| anyhow::anyhow!("Failed to load order snapshot: {e}"))
513    }
514
515    /// Inserts or updates a `PositionSnapshot` entry via the provided `pool`.
516    ///
517    /// # Errors
518    ///
519    /// Returns an error if the SQL INSERT or UPDATE operation fails, or if beginning the transaction fails.
520    pub async fn add_position_snapshot(
521        pool: &PgPool,
522        snapshot: PositionSnapshot,
523    ) -> anyhow::Result<()> {
524        let mut transaction = pool.begin().await?;
525
526        // Insert trader if it does not exist
527        // TODO remove this when node and trader initialization is implemented
528        sqlx::query(
529            r#"
530            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
531        "#,
532        )
533        .bind(snapshot.trader_id.to_string())
534        .execute(&mut *transaction)
535        .await
536        .map(|_| ())
537        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
538
539        sqlx::query(r#"
540            INSERT INTO "position" (
541                id, trader_id, strategy_id, instrument_id, account_id, opening_order_id, closing_order_id, entry, side, signed_qty, quantity, peak_qty,
542                quote_currency, base_currency, settlement_currency, avg_px_open, avg_px_close, realized_return, realized_pnl, unrealized_pnl, commissions,
543                duration_ns, ts_opened, ts_closed, ts_init, ts_last, replay_state, created_at, updated_at
544            ) VALUES (
545                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
546                $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
547            )
548            ON CONFLICT (trader_id, id)
549            DO UPDATE
550            SET
551                strategy_id = $3, instrument_id = $4, account_id = $5, opening_order_id = $6, closing_order_id = $7, entry = $8, side = $9, signed_qty = $10, quantity = $11,
552                peak_qty = $12, quote_currency = $13, base_currency = $14, settlement_currency = $15, avg_px_open = $16, avg_px_close = $17, realized_return = $18, realized_pnl = $19, unrealized_pnl = $20,
553                commissions = $21, duration_ns = $22, ts_opened = $23, ts_closed = $24, ts_init = $25, ts_last = $26,
554                replay_state = $27, updated_at = CURRENT_TIMESTAMP
555        "#)
556            .bind(snapshot.position_id.to_string())
557            .bind(snapshot.trader_id.to_string())
558            .bind(snapshot.strategy_id.to_string())
559            .bind(snapshot.instrument_id.to_string())
560            .bind(snapshot.account_id.to_string())
561            .bind(snapshot.opening_order_id.to_string())
562            .bind(snapshot.closing_order_id.map(|x| x.to_string()))
563            .bind(snapshot.entry.to_string())
564            .bind(snapshot.side.to_string())
565            .bind(snapshot.signed_qty)
566            .bind(snapshot.quantity.to_string())
567            .bind(snapshot.peak_qty.to_string())
568            .bind(snapshot.quote_currency.to_string())
569            .bind(snapshot.base_currency.map(|x| x.to_string()))
570            .bind(snapshot.settlement_currency.to_string())
571            .bind(snapshot.avg_px_open)
572            .bind(snapshot.avg_px_close)
573            .bind(snapshot.realized_return)
574            .bind(snapshot.realized_pnl.map(|x| x.to_string()))
575            .bind(snapshot.unrealized_pnl.map(|x| x.to_string()))
576            .bind(snapshot.commissions.iter().map(ToString::to_string).collect::<Vec<String>>())
577            .bind(snapshot.duration_ns.map(|x| x.to_string()))
578            .bind(snapshot.ts_opened.to_string())
579            .bind(snapshot.ts_closed.map(|x| x.to_string()))
580            .bind(snapshot.ts_init.to_string())
581            .bind(snapshot.ts_last.to_string())
582            .bind(snapshot.replay_state)
583            .execute(&mut *transaction)
584            .await
585            .map(|_| ())
586            .map_err(|e| anyhow::anyhow!("Failed to insert into position table: {e}"))?;
587        transaction
588            .commit()
589            .await
590            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
591    }
592
593    /// Loads a `PositionSnapshot` entry by `position_id` via the provided `pool`.
594    ///
595    /// # Errors
596    ///
597    /// Returns an error if the SQL SELECT or deserialization fails.
598    pub async fn load_position_snapshot(
599        pool: &PgPool,
600        position_id: &PositionId,
601        trader_id: &TraderId,
602    ) -> anyhow::Result<Option<PositionSnapshot>> {
603        sqlx::query_as::<_, PositionSnapshotRow>(
604            r#"SELECT * FROM "position" WHERE id = $1 AND trader_id = $2"#,
605        )
606        .bind(position_id.to_string())
607        .bind(trader_id.to_string())
608        .fetch_optional(pool)
609        .await
610        .map(|row| row.map(|row| row.0))
611        .map_err(|e| anyhow::anyhow!("Failed to load position snapshot: {e}"))
612    }
613
614    /// Checks if an `OrderInitialized` event exists for the given `client_order_id` via the provided `pool`.
615    ///
616    /// # Errors
617    ///
618    /// Returns an error if the SQL SELECT operation fails.
619    pub async fn check_if_order_initialized_exists(
620        pool: &PgPool,
621        client_order_id: ClientOrderId,
622        trader_id: &TraderId,
623    ) -> anyhow::Result<bool> {
624        sqlx::query(
625            r#"
626            SELECT EXISTS(
627                SELECT 1 FROM "order_event"
628                WHERE client_order_id = $1 AND trader_id = $2 AND kind = 'OrderInitialized'
629            )
630        "#,
631        )
632        .bind(client_order_id.to_string())
633        .bind(trader_id.to_string())
634        .fetch_one(pool)
635        .await
636        .map(|row| row.get(0))
637        .map_err(|e| anyhow::anyhow!("Failed to check if order initialized exists: {e}"))
638    }
639
640    /// Checks if any account event exists for the given `account_id` via the provided `pool`.
641    ///
642    /// # Errors
643    ///
644    /// Returns an error if the SQL SELECT operation fails.
645    pub async fn check_if_account_event_exists(
646        pool: &PgPool,
647        account_id: AccountId,
648        trader_id: &TraderId,
649    ) -> anyhow::Result<bool> {
650        sqlx::query(
651            r#"
652            SELECT EXISTS(
653                SELECT 1 FROM "account_event"
654                WHERE account_id = $1 AND trader_id = $2
655            )
656        "#,
657        )
658        .bind(account_id.to_string())
659        .bind(trader_id.to_string())
660        .fetch_one(pool)
661        .await
662        .map(|row| row.get(0))
663        .map_err(|e| anyhow::anyhow!("Failed to check if account event exists: {e}"))
664    }
665
666    /// Inserts or updates an order event entry via the provided `pool`.
667    ///
668    /// An event already persisted for another trader is left unchanged.
669    ///
670    /// # Errors
671    ///
672    /// Returns an error if the SQL INSERT or UPDATE operation fails, if serialization of
673    /// `exec_algorithm_params` fails, or if the event is already persisted for another trader.
674    #[expect(
675        clippy::too_many_lines,
676        reason = "order event persistence maps the full database schema in one transaction"
677    )]
678    pub async fn add_order_event(
679        pool: &PgPool,
680        order_event: Box<dyn OrderEvent>,
681        client_id: Option<ClientId>,
682    ) -> anyhow::Result<()> {
683        let mut transaction = pool.begin().await?;
684
685        // Insert trader if it does not exist
686        // TODO remove this when node and trader initialization is implemented
687        sqlx::query(
688            r#"
689            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
690        "#,
691        )
692        .bind(order_event.trader_id().to_string())
693        .execute(&mut *transaction)
694        .await
695        .map(|_| ())
696        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
697
698        // Insert client if it does not exist
699        // TODO remove this when client initialization is implemented
700        if let Some(client_id) = client_id {
701            sqlx::query(
702                r#"
703                INSERT INTO "client" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
704            "#,
705            )
706            .bind(client_id.to_string())
707            .execute(&mut *transaction)
708            .await
709            .map(|_| ())
710            .map_err(|e| anyhow::anyhow!("Failed to insert into client table: {e}"))?;
711        }
712
713        let exec_algorithm_params = order_event
714            .exec_algorithm_params()
715            .map(serde_json::to_value)
716            .transpose()
717            .map_err(|e| anyhow::anyhow!("Failed to serialize exec algorithm params: {e}"))?;
718        let info = order_event
719            .info()
720            .map(serde_json::to_value)
721            .transpose()
722            .map_err(|e| anyhow::anyhow!("Failed to serialize order event info: {e}"))?;
723
724        let inserted = sqlx::query(r#"
725            INSERT INTO "order_event" (
726                id, kind, client_order_id, order_type, order_side, trader_id, client_id, reason, strategy_id, instrument_id, trade_id, currency, quantity, time_in_force, liquidity_side,
727                post_only, reduce_only, quote_quantity, reconciliation, price, last_px, last_qty, trigger_price, trigger_type, limit_offset, trailing_offset,
728                trailing_offset_type, expire_time, display_qty, emulation_trigger, trigger_instrument_id, contingency_type,
729                order_list_id, linked_order_ids, parent_order_id,
730                exec_algorithm_id, exec_spawn_id, venue_order_id, account_id, position_id, commission, ts_event, ts_init, activation_price, exec_algorithm_params, tags,
731                released_price, protection_price, due_post_only, correction_id, is_reopened, info, causation_id, created_at, updated_at
732            ) VALUES (
733                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
734                $21, $22, $23, $24, $25, $26::trailing_offset_type, $27, $28, $29, $30, $31, $32, $33, $34,
735                $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46,
736                $47, $48, $49, $50, $51, $52, $53, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
737            )
738            ON CONFLICT (id)
739            DO UPDATE
740            SET
741                kind = $2, client_order_id = $3, order_type = $4, order_side=$5, client_id = $7, reason = $8, strategy_id = $9, instrument_id = $10, trade_id = $11, currency = $12,
742                quantity = $13, time_in_force = $14, liquidity_side = $15, post_only = $16, reduce_only = $17, quote_quantity = $18, reconciliation = $19, price = $20, last_px = $21,
743                last_qty = $22, trigger_price = $23, trigger_type = $24, limit_offset = $25, trailing_offset = $26, trailing_offset_type = $27, expire_time = $28, display_qty = $29,
744                emulation_trigger = $30, trigger_instrument_id = $31, contingency_type = $32, order_list_id = $33, linked_order_ids = $34, parent_order_id = $35, exec_algorithm_id = $36,
745                exec_spawn_id = $37, venue_order_id = $38, account_id = $39, position_id = $40, commission = $41, ts_event = $42, ts_init = $43, activation_price = $44,
746                exec_algorithm_params = $45, tags = $46, released_price = $47, protection_price = $48, due_post_only = $49, correction_id = $50,
747                is_reopened = $51, info = $52, causation_id = $53, updated_at = CURRENT_TIMESTAMP
748            WHERE "order_event".trader_id = $6
749        "#)
750            .bind(order_event.id().to_string())
751            .bind(order_event.type_name())
752            .bind(order_event.client_order_id().to_string())
753            .bind(order_event.order_type().map(|x| x.to_string()))
754            .bind(order_event.order_side().map(|x| x.to_string()))
755            .bind(order_event.trader_id().to_string())
756            .bind(client_id.map(|x| x.to_string()))
757            .bind(order_event.reason().map(|x| x.to_string()))
758            .bind(order_event.strategy_id().to_string())
759            .bind(order_event.instrument_id().to_string())
760            .bind(order_event.trade_id().map(|x| x.to_string()))
761            .bind(order_event.currency().map(|x| x.code.as_str()))
762            .bind(order_event.quantity().map(|x| x.to_string()))
763            .bind(order_event.time_in_force().map(|x| x.to_string()))
764            .bind(order_event.liquidity_side().map(|x| x.to_string()))
765            .bind(order_event.post_only())
766            .bind(order_event.reduce_only())
767            .bind(order_event.quote_quantity())
768            .bind(order_event.reconciliation())
769            .bind(order_event.price().map(|x| x.to_string()))
770            .bind(order_event.last_px().map(|x| x.to_string()))
771            .bind(order_event.last_qty().map(|x| x.to_string()))
772            .bind(order_event.trigger_price().map(|x| x.to_string()))
773            .bind(order_event.trigger_type().map(|x| x.to_string()))
774            .bind(order_event.limit_offset().map(|x| x.to_string()))
775            .bind(order_event.trailing_offset().map(|x| x.to_string()))
776            .bind(
777                order_event
778                    .trailing_offset_type()
779                    .map(|value| TrailingOffsetTypePg(Some(value))),
780            )
781            .bind(order_event.expire_time().map(|x| x.to_string()))
782            .bind(order_event.display_qty().map(|x| x.to_string()))
783            .bind(
784                order_event
785                    .emulation_trigger()
786                    .map_or_else(|| "NO_TRIGGER".to_string(), |value| value.to_string()),
787            )
788            .bind(order_event.trigger_instrument_id().map(|x| x.to_string()))
789            .bind(order_event.contingency_type().map_or_else(
790                || "NO_CONTINGENCY".to_string(),
791                |value| value.to_string(),
792            ))
793            .bind(order_event.order_list_id().map(|x| x.to_string()))
794            .bind(order_event.linked_order_ids().map(|x| x.iter().map(ToString::to_string).collect::<Vec<String>>()))
795            .bind(order_event.parent_order_id().map(|x| x.to_string()))
796            .bind(order_event.exec_algorithm_id().map(|x| x.to_string()))
797            .bind(order_event.exec_spawn_id().map(|x| x.to_string()))
798            .bind(order_event.venue_order_id().map(|x| x.to_string()))
799            .bind(order_event.account_id().map(|x| x.to_string()))
800            .bind(order_event.position_id().map(|x| x.to_string()))
801            .bind(order_event.commission().map(|x| x.to_string()))
802            .bind(order_event.ts_event().to_string())
803            .bind(order_event.ts_init().to_string())
804            .bind(order_event.activation_price().map(|x| x.to_string()))
805            .bind(exec_algorithm_params)
806            .bind(order_event.tags().map(|x| x.iter().map(ToString::to_string).collect::<Vec<String>>()))
807            .bind(order_event.released_price().map(|x| x.to_string()))
808            .bind(order_event.protection_price().map(|x| x.to_string()))
809            .bind(order_event.due_post_only())
810            .bind(order_event.correction_id().map(|x| x.to_string()))
811            .bind(order_event.is_reopened())
812            .bind(info)
813            .bind(order_event.causation_id().map(|x| x.to_string()))
814            .execute(&mut *transaction)
815            .await
816            .map_err(|e| anyhow::anyhow!("Failed to insert into order_event table: {e}"))?;
817
818        // Event IDs are shared when two traders persist the same event, and the existing row
819        // belongs to its first writer
820        if inserted.rows_affected() == 0 {
821            anyhow::bail!(
822                "Order event {} is already persisted for another trader, not {}",
823                order_event.id(),
824                order_event.trader_id()
825            );
826        }
827
828        transaction
829            .commit()
830            .await
831            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
832    }
833
834    /// Loads all order events for a `client_order_id` via the provided `pool`.
835    ///
836    /// # Errors
837    ///
838    /// Returns an error if the SQL SELECT or deserialization fails.
839    pub async fn load_order_events(
840        pool: &PgPool,
841        client_order_id: &ClientOrderId,
842        trader_id: &TraderId,
843    ) -> anyhow::Result<Vec<OrderEventAny>> {
844        sqlx::query_as::<_, OrderEventAnyRow>(
845            r#"
846            SELECT * FROM "order_event" event
847            WHERE event.client_order_id = $1 AND event.trader_id = $2
848            ORDER BY created_at ASC
849        "#,
850        )
851        .bind(client_order_id.to_string())
852        .bind(trader_id.to_string())
853        .fetch_all(pool)
854        .await
855        .map(|rows| rows.into_iter().map(|row| row.0).collect())
856        .map_err(|e| anyhow::anyhow!("Failed to load order events: {e}"))
857    }
858
859    /// Loads and assembles a complete `OrderAny` for a `client_order_id` via the provided `pool`.
860    ///
861    /// # Errors
862    ///
863    /// Returns an error if assembling events or SQL operations fail.
864    pub async fn load_order(
865        pool: &PgPool,
866        client_order_id: &ClientOrderId,
867        trader_id: &TraderId,
868    ) -> anyhow::Result<Option<OrderAny>> {
869        let order_events = Self::load_order_events(pool, client_order_id, trader_id).await;
870
871        match order_events {
872            Ok(order_events) => {
873                if order_events.is_empty() {
874                    return Ok(None);
875                }
876                let order = OrderAny::from_events(order_events).map_err(|e| {
877                    anyhow::anyhow!("Failed to assemble order {client_order_id} from events: {e}")
878                })?;
879                Ok(Some(order))
880            }
881            Err(e) => anyhow::bail!("Failed to load order events: {e}"),
882        }
883    }
884
885    /// Loads and assembles all `OrderAny` entries via the provided `pool`.
886    ///
887    /// Only orders written by `trader_id` are loaded.
888    ///
889    /// # Errors
890    ///
891    /// Returns an error if loading events or SQL operations fail.
892    pub async fn load_orders(pool: &PgPool, trader_id: &TraderId) -> anyhow::Result<Vec<OrderAny>> {
893        let mut orders: Vec<OrderAny> = Vec::new();
894        let client_order_ids: Vec<ClientOrderId> = sqlx::query(
895            r#"
896            SELECT DISTINCT client_order_id FROM "order_event"
897            WHERE trader_id = $1
898        "#,
899        )
900        .bind(trader_id.to_string())
901        .fetch_all(pool)
902        .await
903        .map(|rows| {
904            rows.into_iter()
905                .map(|row| ClientOrderId::from(row.get::<&str, _>(0)))
906                .collect()
907        })
908        .map_err(|e| anyhow::anyhow!("Failed to load order ids: {e}"))?;
909        for id in client_order_ids {
910            let order = Self::load_order(pool, &id, trader_id).await?;
911            if let Some(order) = order {
912                orders.push(order);
913            }
914        }
915        Ok(orders)
916    }
917
918    /// Replaces the fill event log for a `position_id` via the provided `pool`.
919    ///
920    /// # Errors
921    ///
922    /// Returns an error if the fill is invalid or if the SQL operations fail.
923    pub async fn add_position(
924        pool: &PgPool,
925        position_id: PositionId,
926        event: &OrderFilled,
927    ) -> anyhow::Result<()> {
928        let event_position_id = Self::event_position_id(event)?;
929        if event_position_id != position_id {
930            anyhow::bail!(
931                "Cannot persist position event {} for mismatched position_id: expected {}, was {}",
932                event.event_id,
933                position_id,
934                event_position_id
935            );
936        }
937
938        let mut transaction = pool.begin().await?;
939
940        // Position IDs are only unique per trader (NETTING IDs are `{instrument_id}-{strategy_id}`),
941        // so the replaced event log is the fill trader's alone
942        sqlx::query(r#"DELETE FROM "position_event" WHERE position_id = $1 AND trader_id = $2"#)
943            .bind(position_id.to_string())
944            .bind(event.trader_id.to_string())
945            .execute(&mut *transaction)
946            .await
947            .map(|_| ())
948            .map_err(|e| anyhow::anyhow!("Failed to delete position_event rows: {e}"))?;
949
950        Self::insert_position_event(&mut transaction, event).await?;
951        transaction
952            .commit()
953            .await
954            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
955    }
956
957    /// Appends a fill event for a `Position` via the provided `pool`.
958    ///
959    /// # Errors
960    ///
961    /// Returns an error if the fill is invalid or if the SQL operations fail.
962    pub async fn update_position(pool: &PgPool, event: &OrderFilled) -> anyhow::Result<()> {
963        let mut transaction = pool.begin().await?;
964
965        Self::insert_position_event(&mut transaction, event).await?;
966        transaction
967            .commit()
968            .await
969            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
970    }
971
972    /// Appends an `OrderFilled` event to the position event log via the provided `pool`.
973    ///
974    /// # Errors
975    ///
976    /// Returns an error if the SQL INSERT operation fails.
977    pub async fn add_position_event(pool: &PgPool, event: &OrderFilled) -> anyhow::Result<()> {
978        let mut transaction = pool.begin().await?;
979
980        Self::insert_position_event(&mut transaction, event).await?;
981        transaction
982            .commit()
983            .await
984            .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {e}"))
985    }
986
987    /// Loads all fill events for a `position_id` via the provided `pool`.
988    ///
989    /// # Errors
990    ///
991    /// Returns an error if the SQL SELECT or deserialization fails.
992    pub async fn load_position_events(
993        pool: &PgPool,
994        position_id: &PositionId,
995        trader_id: &TraderId,
996    ) -> anyhow::Result<Vec<OrderFilled>> {
997        sqlx::query_as::<_, OrderFilledRow>(
998            r#"
999            SELECT *
1000            FROM "position_event"
1001            WHERE position_id = $1 AND trader_id = $2
1002            ORDER BY event_sequence ASC
1003        "#,
1004        )
1005        .bind(position_id.to_string())
1006        .bind(trader_id.to_string())
1007        .fetch_all(pool)
1008        .await
1009        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1010        .map_err(|e| anyhow::anyhow!("Failed to load position events: {e}"))
1011    }
1012
1013    /// Loads and replays a complete `Position` for a `position_id` via the provided `pool`.
1014    ///
1015    /// # Errors
1016    ///
1017    /// Returns an error if loading events, loading instruments, or replaying fills fails.
1018    pub async fn load_position(
1019        pool: &PgPool,
1020        position_id: &PositionId,
1021        trader_id: &TraderId,
1022    ) -> anyhow::Result<Option<Position>> {
1023        if let Some(snapshot) = Self::load_position_snapshot(pool, position_id, trader_id).await?
1024            && let Some(replay_state) = snapshot.replay_state
1025        {
1026            return serde_json::from_value(replay_state)
1027                .map(Some)
1028                .map_err(|e| anyhow::anyhow!("Failed to decode position replay state: {e}"));
1029        }
1030
1031        let fills = Self::load_position_events(pool, position_id, trader_id).await?;
1032        let Some((first_fill, remaining_fills)) = fills.split_first() else {
1033            return Ok(None);
1034        };
1035        let Some(instrument) = Self::load_instrument(pool, &first_fill.instrument_id).await? else {
1036            log::error!(
1037                "Instrument not found for position {position_id}: {}",
1038                first_fill.instrument_id
1039            );
1040            return Ok(None);
1041        };
1042
1043        let mut position = Position::new(&instrument, first_fill.clone());
1044        for fill in remaining_fills {
1045            if position.trade_ids.contains(&fill.trade_id) {
1046                anyhow::bail!(
1047                    "Duplicate fill event for position {position_id}: {}",
1048                    fill.trade_id
1049                );
1050            }
1051            position.apply(fill);
1052        }
1053
1054        Ok(Some(position))
1055    }
1056
1057    /// Loads and replays all `Position` entries via the provided `pool`.
1058    ///
1059    /// Only positions written by `trader_id` are loaded.
1060    ///
1061    /// # Errors
1062    ///
1063    /// Returns an error if loading position IDs or replaying any position fails.
1064    pub async fn load_positions(
1065        pool: &PgPool,
1066        trader_id: &TraderId,
1067    ) -> anyhow::Result<Vec<Position>> {
1068        let position_ids: Vec<PositionId> = sqlx::query(
1069            r#"
1070            SELECT DISTINCT position_id
1071            FROM "position_event"
1072            WHERE trader_id = $1
1073            ORDER BY position_id ASC
1074        "#,
1075        )
1076        .bind(trader_id.to_string())
1077        .fetch_all(pool)
1078        .await
1079        .map(|rows| {
1080            rows.into_iter()
1081                .map(|row| PositionId::from(row.get::<&str, _>(0)))
1082                .collect()
1083        })
1084        .map_err(|e| anyhow::anyhow!("Failed to load position ids: {e}"))?;
1085
1086        let mut positions = Vec::new();
1087
1088        for id in position_ids {
1089            match Self::load_position(pool, &id, trader_id).await {
1090                Ok(Some(position)) => positions.push(position),
1091                Ok(None) => log::error!("Position not found: {id}"),
1092                Err(e) => log::error!("Failed to load position {id}: {e}"),
1093            }
1094        }
1095
1096        Ok(positions)
1097    }
1098
1099    async fn insert_position_event(
1100        transaction: &mut Transaction<'_, Postgres>,
1101        event: &OrderFilled,
1102    ) -> anyhow::Result<()> {
1103        let position_id = Self::event_position_id(event)?;
1104
1105        sqlx::query(
1106            r#"
1107            INSERT INTO "trader" (id)
1108            VALUES ($1)
1109            ON CONFLICT (id) DO NOTHING
1110        "#,
1111        )
1112        .bind(event.trader_id.to_string())
1113        .execute(&mut **transaction)
1114        .await
1115        .map(|_| ())
1116        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
1117
1118        let position_event_info = event
1119            .info
1120            .clone()
1121            .map(serde_json::to_value)
1122            .transpose()
1123            .map_err(|e| anyhow::anyhow!("Failed to serialize fill info: {e}"))?;
1124
1125        sqlx::query(
1126            r#"
1127            INSERT INTO "position_event" (
1128                id, kind, trader_id, strategy_id, instrument_id, client_order_id, venue_order_id,
1129                account_id, trade_id, currency, order_type, order_side, last_px, last_qty,
1130                liquidity_side, position_id, commission, reconciliation, info, causation_id,
1131                ts_event, ts_init, created_at, updated_at
1132            ) VALUES (
1133                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17,
1134                $18, $19, $20, $21, $22, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1135            )
1136        "#,
1137        )
1138        .bind(event.event_id.to_string())
1139        .bind("OrderFilled")
1140        .bind(event.trader_id.to_string())
1141        .bind(event.strategy_id.to_string())
1142        .bind(event.instrument_id.to_string())
1143        .bind(event.client_order_id.to_string())
1144        .bind(event.venue_order_id.to_string())
1145        .bind(event.account_id.to_string())
1146        .bind(event.trade_id.to_string())
1147        .bind(event.currency.code.as_str())
1148        .bind(event.order_type.to_string())
1149        .bind(event.order_side.to_string())
1150        .bind(event.last_px.to_string())
1151        .bind(event.last_qty.to_string())
1152        .bind(event.liquidity_side.to_string())
1153        .bind(position_id.to_string())
1154        .bind(event.commission.map(|commission| commission.to_string()))
1155        .bind(event.reconciliation)
1156        .bind(position_event_info)
1157        .bind(
1158            event
1159                .causation_id
1160                .map(|causation_id| causation_id.to_string()),
1161        )
1162        .bind(event.ts_event.to_string())
1163        .bind(event.ts_init.to_string())
1164        .execute(&mut **transaction)
1165        .await
1166        .map(|_| ())
1167        .map_err(|e| anyhow::anyhow!("Failed to insert into position_event table: {e}"))
1168    }
1169
1170    fn event_position_id(event: &OrderFilled) -> anyhow::Result<PositionId> {
1171        event.position_id.ok_or_else(|| {
1172            anyhow::anyhow!(
1173                "Cannot persist position event with no position_id: {}",
1174                event.event_id
1175            )
1176        })
1177    }
1178
1179    /// Inserts or updates an `AccountState` event for `trader_id` via the provided `pool`.
1180    ///
1181    /// An event already persisted for another trader, or awaiting assignment, is left unchanged.
1182    ///
1183    /// # Errors
1184    ///
1185    /// Returns an error if the SQL INSERT or UPDATE operation fails, or if the event is already
1186    /// persisted for another trader or awaiting assignment.
1187    pub async fn add_account(
1188        pool: &PgPool,
1189        updated: bool,
1190        account_event: AccountState,
1191        trader_id: &TraderId,
1192    ) -> anyhow::Result<()> {
1193        if updated {
1194            let exists =
1195                Self::check_if_account_event_exists(pool, account_event.account_id, trader_id)
1196                    .await?;
1197
1198            if !exists {
1199                anyhow::bail!(
1200                    "Account event does not exist for account: {}",
1201                    account_event.account_id
1202                );
1203            }
1204        }
1205
1206        let mut transaction = pool.begin().await?;
1207        let event = serde_json::to_value(&account_event)
1208            .map_err(|e| anyhow::anyhow!("Failed to serialize account event: {e}"))?;
1209        let balances = event
1210            .get("balances")
1211            .cloned()
1212            .ok_or_else(|| anyhow::anyhow!("Serialized account event has no balances"))?;
1213        let margins = event
1214            .get("margins")
1215            .cloned()
1216            .ok_or_else(|| anyhow::anyhow!("Serialized account event has no margins"))?;
1217
1218        sqlx::query(
1219            r#"
1220            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
1221        "#,
1222        )
1223        .bind(trader_id.to_string())
1224        .execute(&mut *transaction)
1225        .await
1226        .map(|_| ())
1227        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
1228
1229        sqlx::query(
1230            r#"
1231            INSERT INTO "account" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
1232        "#,
1233        )
1234        .bind(account_event.account_id.to_string())
1235        .execute(&mut *transaction)
1236        .await
1237        .map(|_| ())
1238        .map_err(|e| anyhow::anyhow!("Failed to insert into account table: {e}"))?;
1239
1240        let inserted = sqlx::query(r#"
1241            INSERT INTO "account_event" (
1242                id, kind, account_id, trader_id, base_currency, balances, margins, is_reported, ts_event, ts_init, created_at, updated_at
1243            ) VALUES (
1244                $1, $2, $3, $10, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1245            )
1246            ON CONFLICT (id)
1247            DO UPDATE
1248            SET
1249                kind = $2, account_id = $3,
1250                base_currency = $4, balances = $5, margins = $6, is_reported = $7,
1251                ts_event = $8, ts_init = $9, updated_at = CURRENT_TIMESTAMP
1252            WHERE "account_event".trader_id = $10
1253        "#)
1254            .bind(account_event.event_id.to_string())
1255            .bind(account_event.account_type.to_string())
1256            .bind(account_event.account_id.to_string())
1257            .bind(account_event.base_currency.map(|x| x.code.as_str()))
1258            .bind(balances)
1259            .bind(margins)
1260            .bind(account_event.is_reported)
1261            .bind(account_event.ts_event.to_string())
1262            .bind(account_event.ts_init.to_string())
1263            .bind(trader_id.to_string())
1264            .execute(&mut *transaction)
1265            .await
1266            .map_err(|e| anyhow::anyhow!("Failed to insert into account_event table: {e}"))?;
1267
1268        // Event IDs are shared when two traders persist the same account state, and the existing
1269        // row belongs to its first writer (or awaits assignment if it has no trader)
1270        if inserted.rows_affected() == 0 {
1271            anyhow::bail!(
1272                "Account event {} for {} is already persisted for another trader, not {trader_id}",
1273                account_event.event_id,
1274                account_event.account_id
1275            );
1276        }
1277
1278        transaction
1279            .commit()
1280            .await
1281            .map_err(|e| anyhow::anyhow!("Failed to commit add_account transaction: {e}"))
1282    }
1283
1284    /// Loads all account events for `account_id` via the provided `pool`.
1285    ///
1286    /// # Errors
1287    ///
1288    /// Returns an error if the SQL SELECT or deserialization fails.
1289    pub async fn load_account_events(
1290        pool: &PgPool,
1291        account_id: &AccountId,
1292        trader_id: &TraderId,
1293    ) -> anyhow::Result<Vec<AccountState>> {
1294        sqlx::query_as::<_, AccountEventRow>(
1295            r#"
1296            SELECT * FROM "account_event"
1297            WHERE account_id = $1 AND trader_id = $2
1298            ORDER BY created_at ASC
1299        "#,
1300        )
1301        .bind(account_id.to_string())
1302        .bind(trader_id.to_string())
1303        .fetch_all(pool)
1304        .await
1305        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1306        .map_err(|e| anyhow::anyhow!("Failed to load account events: {e}"))
1307    }
1308
1309    /// Loads and assembles a complete `AccountAny` for `account_id` via the provided `pool`.
1310    ///
1311    /// # Errors
1312    ///
1313    /// Returns an error if assembling events or SQL operations fail.
1314    pub async fn load_account(
1315        pool: &PgPool,
1316        account_id: &AccountId,
1317        trader_id: &TraderId,
1318    ) -> anyhow::Result<Option<AccountAny>> {
1319        let account_events = Self::load_account_events(pool, account_id, trader_id).await;
1320        match account_events {
1321            Ok(account_events) => {
1322                if account_events.is_empty() {
1323                    return Ok(None);
1324                }
1325                let account = AccountAny::from_events(&account_events).map_err(|e| {
1326                    anyhow::anyhow!("Failed to assemble account {account_id} from events: {e}")
1327                })?;
1328                Ok(Some(account))
1329            }
1330            Err(e) => anyhow::bail!("Failed to load account events: {e}"),
1331        }
1332    }
1333
1334    /// Loads and assembles all `AccountAny` entries via the provided `pool`.
1335    ///
1336    /// Only accounts with events written by `trader_id` are loaded.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns an error if loading events or SQL operations fail.
1341    pub async fn load_accounts(
1342        pool: &PgPool,
1343        trader_id: &TraderId,
1344    ) -> anyhow::Result<Vec<AccountAny>> {
1345        let mut accounts: Vec<AccountAny> = Vec::new();
1346        let account_ids: Vec<AccountId> = sqlx::query(
1347            r#"
1348            SELECT DISTINCT account_id FROM "account_event"
1349            WHERE trader_id = $1
1350        "#,
1351        )
1352        .bind(trader_id.to_string())
1353        .fetch_all(pool)
1354        .await
1355        .map(|rows| {
1356            rows.into_iter()
1357                .map(|row| AccountId::from(row.get::<&str, _>(0)))
1358                .collect()
1359        })
1360        .map_err(|e| anyhow::anyhow!("Failed to load account ids: {e}"))?;
1361        for id in account_ids {
1362            let account = Self::load_account(pool, &id, trader_id).await?;
1363            if let Some(account) = account {
1364                accounts.push(account);
1365            }
1366        }
1367        Ok(accounts)
1368    }
1369
1370    /// Loads the IDs of accounts that have events with no trader via the provided `pool`.
1371    ///
1372    /// These events predate trader-scoped account persistence, so no trader-scoped cache loads
1373    /// them until they are assigned with [`Self::assign_account_trader`].
1374    ///
1375    /// # Errors
1376    ///
1377    /// Returns an error if the SQL SELECT operation fails.
1378    pub async fn load_unassigned_account_ids(pool: &PgPool) -> anyhow::Result<Vec<AccountId>> {
1379        sqlx::query_scalar::<_, String>(
1380            r#"
1381            SELECT DISTINCT account_id FROM "account_event"
1382            WHERE trader_id IS NULL AND account_id IS NOT NULL
1383            ORDER BY account_id
1384        "#,
1385        )
1386        .fetch_all(pool)
1387        .await
1388        .map(|ids| ids.iter().map(|id| AccountId::from(id.as_str())).collect())
1389        .map_err(|e| anyhow::anyhow!("Failed to load unassigned account ids: {e}"))
1390    }
1391
1392    /// Assigns the account events of `account_id` that have no trader to `trader_id` via the
1393    /// provided `pool`, returning the number of events assigned.
1394    ///
1395    /// Events already stamped with a trader are left unchanged.
1396    ///
1397    /// # Errors
1398    ///
1399    /// Returns an error if the account has no events, or if any SQL operation fails.
1400    pub async fn assign_account_trader(
1401        pool: &PgPool,
1402        account_id: &AccountId,
1403        trader_id: &TraderId,
1404    ) -> anyhow::Result<u64> {
1405        let mut transaction = pool.begin().await?;
1406
1407        let has_events: bool = sqlx::query_scalar(
1408            r#"SELECT EXISTS(SELECT 1 FROM "account_event" WHERE account_id = $1)"#,
1409        )
1410        .bind(account_id.to_string())
1411        .fetch_one(&mut *transaction)
1412        .await
1413        .map_err(|e| anyhow::anyhow!("Failed to check account events for {account_id}: {e}"))?;
1414
1415        if !has_events {
1416            anyhow::bail!("No account events found for {account_id}");
1417        }
1418
1419        sqlx::query(
1420            r#"
1421            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
1422        "#,
1423        )
1424        .bind(trader_id.to_string())
1425        .execute(&mut *transaction)
1426        .await
1427        .map(|_| ())
1428        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
1429
1430        let assigned = sqlx::query(
1431            r#"
1432            UPDATE "account_event"
1433            SET trader_id = $2, updated_at = CURRENT_TIMESTAMP
1434            WHERE account_id = $1 AND trader_id IS NULL
1435        "#,
1436        )
1437        .bind(account_id.to_string())
1438        .bind(trader_id.to_string())
1439        .execute(&mut *transaction)
1440        .await
1441        .map_err(|e| anyhow::anyhow!("Failed to assign {account_id} to {trader_id}: {e}"))?
1442        .rows_affected();
1443
1444        transaction
1445            .commit()
1446            .await
1447            .map_err(|e| anyhow::anyhow!("Failed to commit account assignment: {e}"))?;
1448
1449        Ok(assigned)
1450    }
1451
1452    /// Inserts a `TradeTick` entry via the provided `pool`.
1453    ///
1454    /// # Errors
1455    ///
1456    /// Returns an error if the SQL INSERT operation fails.
1457    pub async fn add_trade(pool: &PgPool, trade: &TradeTick) -> anyhow::Result<()> {
1458        sqlx::query(r#"
1459            INSERT INTO "trade" (
1460                instrument_id, price, quantity, aggressor_side, venue_trade_id,
1461                ts_event, ts_init, created_at, updated_at
1462            ) VALUES (
1463                $1, $2, $3, $4::aggressor_side, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1464            )
1465            ON CONFLICT (id)
1466            DO UPDATE
1467            SET
1468                instrument_id = $1, price = $2, quantity = $3, aggressor_side = $4, venue_trade_id = $5,
1469                ts_event = $6, ts_init = $7, updated_at = CURRENT_TIMESTAMP
1470        "#)
1471            .bind(trade.instrument_id.to_string())
1472            .bind(trade.price.to_string())
1473            .bind(trade.size.to_string())
1474            .bind(AggressorSidePg(trade.aggressor_side))
1475            .bind(trade.trade_id.to_string())
1476            .bind(trade.ts_event.to_string())
1477            .bind(trade.ts_init.to_string())
1478            .execute(pool)
1479            .await
1480            .map(|_| ())
1481            .map_err(|e| anyhow::anyhow!("Failed to insert into trade table: {e}"))
1482    }
1483
1484    /// Loads all `TradeTick` entries for `instrument_id` via the provided `pool`.
1485    ///
1486    /// # Errors
1487    ///
1488    /// Returns an error if the SQL SELECT or deserialization fails.
1489    pub async fn load_trades(
1490        pool: &PgPool,
1491        instrument_id: &InstrumentId,
1492    ) -> anyhow::Result<Vec<TradeTick>> {
1493        sqlx::query_as::<_, TradeTickRow>(
1494            r#"SELECT * FROM "trade" WHERE instrument_id = $1 ORDER BY ts_event ASC"#,
1495        )
1496        .bind(instrument_id.to_string())
1497        .fetch_all(pool)
1498        .await
1499        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1500        .map_err(|e| anyhow::anyhow!("Failed to load trades: {e}"))
1501    }
1502
1503    /// Inserts a `QuoteTick` entry via the provided `pool`.
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns an error if the SQL INSERT operation fails.
1508    pub async fn add_quote(pool: &PgPool, quote: &QuoteTick) -> anyhow::Result<()> {
1509        sqlx::query(r#"
1510            INSERT INTO "quote" (
1511                instrument_id, bid_price, ask_price, bid_size, ask_size, ts_event, ts_init, created_at, updated_at
1512            ) VALUES (
1513                $1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1514            )
1515            ON CONFLICT (id)
1516            DO UPDATE
1517            SET
1518                instrument_id = $1, bid_price = $2, ask_price = $3, bid_size = $4, ask_size = $5,
1519                ts_event = $6, ts_init = $7, updated_at = CURRENT_TIMESTAMP
1520        "#)
1521            .bind(quote.instrument_id.to_string())
1522            .bind(quote.bid_price.to_string())
1523            .bind(quote.ask_price.to_string())
1524            .bind(quote.bid_size.to_string())
1525            .bind(quote.ask_size.to_string())
1526            .bind(quote.ts_event.to_string())
1527            .bind(quote.ts_init.to_string())
1528            .execute(pool)
1529            .await
1530            .map(|_| ())
1531            .map_err(|e| anyhow::anyhow!("Failed to insert into quote table: {e}"))
1532    }
1533
1534    /// Loads all `QuoteTick` entries for `instrument_id` via the provided `pool`.
1535    ///
1536    /// # Errors
1537    ///
1538    /// Returns an error if the SQL SELECT or deserialization fails.
1539    pub async fn load_quotes(
1540        pool: &PgPool,
1541        instrument_id: &InstrumentId,
1542    ) -> anyhow::Result<Vec<QuoteTick>> {
1543        sqlx::query_as::<_, QuoteTickRow>(
1544            r#"SELECT * FROM "quote" WHERE instrument_id = $1 ORDER BY ts_event ASC"#,
1545        )
1546        .bind(instrument_id.to_string())
1547        .fetch_all(pool)
1548        .await
1549        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1550        .map_err(|e| anyhow::anyhow!("Failed to load quotes: {e}"))
1551    }
1552
1553    /// Inserts a `Bar` entry via the provided `pool`.
1554    ///
1555    /// # Errors
1556    ///
1557    /// Returns an error if the SQL INSERT operation fails.
1558    pub async fn add_bar(pool: &PgPool, bar: &Bar) -> anyhow::Result<()> {
1559        if bar.bar_type.is_composite() {
1560            anyhow::bail!(
1561                "Cannot persist bar with composite bar type {}: the bar table stores only \
1562                 the standard form; standardize the bar type before persisting",
1563                bar.bar_type,
1564            );
1565        }
1566
1567        let bar_step = i32::try_from(bar.bar_type.spec().step.get())
1568            .map_err(|e| anyhow::anyhow!("invalid bar step: {e}"))?;
1569
1570        sqlx::query(r#"
1571            INSERT INTO "bar" (
1572                instrument_id, step, bar_aggregation, price_type, aggregation_source, open, high, low, close, volume, ts_event, ts_init, created_at, updated_at
1573            ) VALUES (
1574                $1, $2, $3::bar_aggregation, $4::price_type, $5::aggregation_source, $6, $7, $8, $9, $10, $11, $12, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1575            )
1576            ON CONFLICT (id)
1577            DO UPDATE
1578            SET
1579                instrument_id = $1, step = $2, bar_aggregation = $3::bar_aggregation, price_type = $4::price_type, aggregation_source = $5::aggregation_source,
1580                open = $6, high = $7, low = $8, close = $9, volume = $10, ts_event = $11, ts_init = $12, updated_at = CURRENT_TIMESTAMP
1581        "#)
1582            .bind(bar.bar_type.instrument_id().to_string())
1583            .bind(bar_step)
1584            .bind(BarAggregationPg(bar.bar_type.spec().aggregation))
1585            .bind(PriceTypePg(bar.bar_type.spec().price_type))
1586            .bind(AggregationSourcePg(bar.bar_type.aggregation_source()))
1587            .bind(bar.open.to_string())
1588            .bind(bar.high.to_string())
1589            .bind(bar.low.to_string())
1590            .bind(bar.close.to_string())
1591            .bind(bar.volume.to_string())
1592            .bind(bar.ts_event.to_string())
1593            .bind(bar.ts_init.to_string())
1594            .execute(pool)
1595            .await
1596            .map(|_| ())
1597            .map_err(|e| anyhow::anyhow!("Failed to insert into bar table: {e}"))
1598    }
1599
1600    /// Loads all `Bar` entries for `instrument_id` via the provided `pool`.
1601    ///
1602    /// # Errors
1603    ///
1604    /// Returns an error if the SQL SELECT or deserialization fails.
1605    pub async fn load_bars(
1606        pool: &PgPool,
1607        instrument_id: &InstrumentId,
1608    ) -> anyhow::Result<Vec<Bar>> {
1609        sqlx::query_as::<_, BarRow>(
1610            r#"SELECT * FROM "bar" WHERE instrument_id = $1 ORDER BY ts_event ASC"#,
1611        )
1612        .bind(instrument_id.to_string())
1613        .fetch_all(pool)
1614        .await
1615        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1616        .map_err(|e| anyhow::anyhow!("Failed to load bars: {e}"))
1617    }
1618
1619    /// Loads all distinct client order IDs from order events via the provided `pool`.
1620    ///
1621    /// Only order events written by `trader_id` are read.
1622    ///
1623    /// # Errors
1624    ///
1625    /// Returns an error if the SQL SELECT or iteration fails.
1626    pub async fn load_distinct_order_event_client_ids(
1627        pool: &PgPool,
1628        trader_id: &TraderId,
1629    ) -> anyhow::Result<AHashMap<ClientOrderId, ClientId>> {
1630        let mut map: AHashMap<ClientOrderId, ClientId> = AHashMap::new();
1631        let result = sqlx::query_as::<_, OrderEventOrderClientIdCombination>(
1632            r#"
1633            SELECT DISTINCT ON (client_order_id)
1634                client_order_id AS "client_order_id",
1635                client_id AS "client_id"
1636            FROM "order_event"
1637            WHERE client_id IS NOT NULL AND trader_id = $1
1638            ORDER BY client_order_id, created_at DESC
1639        "#,
1640        )
1641        .bind(trader_id.to_string())
1642        .fetch_all(pool)
1643        .await
1644        .map_err(|e| anyhow::anyhow!("Failed to load account ids: {e}"))?;
1645
1646        for id in result {
1647            map.insert(id.client_order_id, id.client_id);
1648        }
1649        Ok(map)
1650    }
1651
1652    /// Claims execution-client origins for existing order events in one transaction.
1653    ///
1654    /// # Errors
1655    ///
1656    /// Returns an error if an order has no persisted events, an order is already claimed by a
1657    /// different client, or any SQL operation fails. Any error rolls back the complete batch.
1658    pub async fn index_order_clients(
1659        pool: &PgPool,
1660        trader_id: &TraderId,
1661        claims: &[(ClientOrderId, ClientId)],
1662    ) -> anyhow::Result<()> {
1663        if claims.is_empty() {
1664            return Ok(());
1665        }
1666
1667        let mut transaction = pool.begin().await?;
1668
1669        for (client_order_id, client_id) in claims {
1670            let conflicting_client_id = sqlx::query_scalar::<_, String>(
1671                r#"
1672                SELECT client_id
1673                FROM "order_event"
1674                WHERE client_order_id = $1
1675                  AND trader_id = $3
1676                  AND client_id IS NOT NULL
1677                  AND client_id <> $2
1678                LIMIT 1
1679            "#,
1680            )
1681            .bind(client_order_id.to_string())
1682            .bind(client_id.to_string())
1683            .bind(trader_id.to_string())
1684            .fetch_optional(&mut *transaction)
1685            .await
1686            .map_err(|e| anyhow::anyhow!("Failed to validate order client origin: {e}"))?;
1687
1688            if let Some(conflicting_client_id) = conflicting_client_id {
1689                anyhow::bail!(
1690                    "Order {client_order_id} is already claimed by execution client \
1691                     {conflicting_client_id} and cannot be claimed by {client_id}"
1692                );
1693            }
1694
1695            sqlx::query(
1696                r#"
1697                INSERT INTO "client" (id)
1698                VALUES ($1)
1699                ON CONFLICT (id) DO NOTHING
1700            "#,
1701            )
1702            .bind(client_id.to_string())
1703            .execute(&mut *transaction)
1704            .await
1705            .map_err(|e| anyhow::anyhow!("Failed to persist execution client {client_id}: {e}"))?;
1706
1707            let result = sqlx::query(
1708                r#"
1709                UPDATE "order_event"
1710                SET client_id = $2
1711                WHERE client_order_id = $1
1712                  AND trader_id = $3
1713                  AND (client_id IS NULL OR client_id = $2)
1714            "#,
1715            )
1716            .bind(client_order_id.to_string())
1717            .bind(client_id.to_string())
1718            .bind(trader_id.to_string())
1719            .execute(&mut *transaction)
1720            .await
1721            .map_err(|e| anyhow::anyhow!("Failed to index order client origin: {e}"))?;
1722
1723            if result.rows_affected() == 0 {
1724                anyhow::bail!("No persisted order events found for {client_order_id}");
1725            }
1726        }
1727
1728        transaction
1729            .commit()
1730            .await
1731            .map_err(|e| anyhow::anyhow!("Failed to commit order client origins: {e}"))
1732    }
1733
1734    /// Inserts or updates an order ID to position ID index entry via the provided `pool`.
1735    ///
1736    /// # Errors
1737    ///
1738    /// Returns an error if the SQL INSERT or UPDATE operation fails.
1739    pub async fn index_order_position(
1740        pool: &PgPool,
1741        trader_id: &TraderId,
1742        client_order_id: ClientOrderId,
1743        position_id: PositionId,
1744    ) -> anyhow::Result<()> {
1745        let mut transaction = pool.begin().await?;
1746
1747        sqlx::query(
1748            r#"
1749            INSERT INTO "trader" (id) VALUES ($1) ON CONFLICT (id) DO NOTHING
1750        "#,
1751        )
1752        .bind(trader_id.to_string())
1753        .execute(&mut *transaction)
1754        .await
1755        .map(|_| ())
1756        .map_err(|e| anyhow::anyhow!("Failed to insert into trader table: {e}"))?;
1757
1758        sqlx::query(
1759            r#"
1760            INSERT INTO "order_position_index" (
1761                trader_id, client_order_id, position_id, created_at, updated_at
1762            ) VALUES (
1763                $1, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1764            )
1765            ON CONFLICT (trader_id, client_order_id)
1766            DO UPDATE
1767            SET
1768                position_id = $3, updated_at = CURRENT_TIMESTAMP
1769        "#,
1770        )
1771        .bind(trader_id.to_string())
1772        .bind(client_order_id.to_string())
1773        .bind(position_id.to_string())
1774        .execute(&mut *transaction)
1775        .await
1776        .map(|_| ())
1777        .map_err(|e| anyhow::anyhow!("Failed to insert into order_position_index table: {e}"))?;
1778
1779        transaction
1780            .commit()
1781            .await
1782            .map_err(|e| anyhow::anyhow!("Failed to commit order position index: {e}"))
1783    }
1784
1785    /// Loads the order ID to position ID index via the provided `pool`.
1786    ///
1787    /// Only entries written by `trader_id` are loaded.
1788    ///
1789    /// # Errors
1790    ///
1791    /// Returns an error if the SQL SELECT or iteration fails.
1792    pub async fn load_index_order_position(
1793        pool: &PgPool,
1794        trader_id: &TraderId,
1795    ) -> anyhow::Result<AHashMap<ClientOrderId, PositionId>> {
1796        let mut map: AHashMap<ClientOrderId, PositionId> = AHashMap::new();
1797        let result = sqlx::query_as::<_, OrderPositionIndexRow>(
1798            r#"
1799            SELECT
1800                client_order_id AS "client_order_id",
1801                position_id AS "position_id"
1802            FROM "order_position_index"
1803            WHERE trader_id = $1
1804        "#,
1805        )
1806        .bind(trader_id.to_string())
1807        .fetch_all(pool)
1808        .await
1809        .map_err(|e| anyhow::anyhow!("Failed to load order position index: {e}"))?;
1810
1811        for row in result {
1812            map.insert(row.client_order_id, row.position_id);
1813        }
1814        Ok(map)
1815    }
1816
1817    /// Inserts a `Signal` entry via the provided `pool`.
1818    ///
1819    /// # Errors
1820    ///
1821    /// Returns an error if the SQL INSERT operation fails.
1822    pub async fn add_signal(pool: &PgPool, signal: &Signal) -> anyhow::Result<()> {
1823        sqlx::query(
1824            r#"
1825            INSERT INTO "signal" (
1826                name, value, ts_event, ts_init, created_at, updated_at
1827            ) VALUES (
1828                $1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
1829            )
1830            ON CONFLICT (id)
1831            DO UPDATE
1832            SET
1833                name = $1, value = $2, ts_event = $3, ts_init = $4,
1834                updated_at = CURRENT_TIMESTAMP
1835        "#,
1836        )
1837        .bind(signal.name.to_string())
1838        .bind(signal.value.clone())
1839        .bind(signal.ts_event.to_string())
1840        .bind(signal.ts_init.to_string())
1841        .execute(pool)
1842        .await
1843        .map(|_| ())
1844        .map_err(|e| anyhow::anyhow!("Failed to insert into signal table: {e}"))
1845    }
1846
1847    /// Loads all `Signal` entries by `name` via the provided `pool`.
1848    ///
1849    /// # Errors
1850    ///
1851    /// Returns an error if the SQL SELECT or deserialization fails.
1852    pub async fn load_signals(pool: &PgPool, name: &str) -> anyhow::Result<Vec<Signal>> {
1853        sqlx::query_as::<_, SignalRow>(
1854            r#"SELECT * FROM "signal" WHERE name = $1 ORDER BY ts_init ASC"#,
1855        )
1856        .bind(name)
1857        .fetch_all(pool)
1858        .await
1859        .map(|rows| rows.into_iter().map(|row| row.0).collect())
1860        .map_err(|e| anyhow::anyhow!("Failed to load signals: {e}"))
1861    }
1862
1863    /// Inserts a `CustomData` entry via the provided `pool`.
1864    ///
1865    /// Serializes the model `CustomData` to full JSON and stores it in the JSONB `value` column.
1866    ///
1867    /// # Errors
1868    ///
1869    /// Returns an error if the SQL INSERT operation fails.
1870    pub async fn add_custom_data(pool: &PgPool, data: &CustomData) -> anyhow::Result<()> {
1871        let json_bytes = serde_json::to_vec(data)
1872            .map_err(|e| anyhow::anyhow!("CustomData must be valid JSON: {e}"))?;
1873        let value_json: serde_json::Value = serde_json::from_slice(&json_bytes)
1874            .map_err(|e| anyhow::anyhow!("CustomData value must be valid JSON: {e}"))?;
1875        let data_type_obj = value_json
1876            .get("data_type")
1877            .and_then(|v| v.as_object())
1878            .ok_or_else(|| anyhow::anyhow!("CustomData JSON missing data_type"))?;
1879        let data_type_name = data_type_obj
1880            .get("type_name")
1881            .and_then(|v| v.as_str())
1882            .unwrap_or("");
1883        let metadata_json = data_type_obj
1884            .get("metadata")
1885            .cloned()
1886            .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
1887        let identifier = data_type_obj
1888            .get("identifier")
1889            .and_then(|v| v.as_str())
1890            .unwrap_or("");
1891        sqlx::query(
1892            r#"
1893            INSERT INTO "custom" (data_type, metadata, identifier, value, ts_event, ts_init, created_at, updated_at)
1894            VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
1895            ON CONFLICT (id)
1896            DO UPDATE SET
1897                data_type = EXCLUDED.data_type,
1898                metadata = EXCLUDED.metadata,
1899                identifier = EXCLUDED.identifier,
1900                value = EXCLUDED.value,
1901                ts_event = EXCLUDED.ts_event,
1902                ts_init = EXCLUDED.ts_init,
1903                updated_at = CURRENT_TIMESTAMP
1904        "#,
1905        )
1906        .bind(data_type_name)
1907        .bind(&metadata_json)
1908        .bind(identifier)
1909        .bind(&value_json)
1910        .bind(
1911            value_json
1912                .get("ts_event")
1913                .and_then(serde_json::Value::as_u64)
1914                .unwrap_or_else(|| data.ts_init().as_u64())
1915                .to_string(),
1916        )
1917        .bind(data.ts_init().to_string())
1918        .execute(pool)
1919        .await
1920        .map(|_| ())
1921        .map_err(|e| anyhow::anyhow!("Failed to insert into custom table: {e}"))
1922    }
1923
1924    /// Loads all `CustomData` entries of `data_type` via the provided `pool`.
1925    ///
1926    /// Filters by `data_type`, `metadata`, and `identifier` columns to match the requested data type.
1927    ///
1928    /// # Errors
1929    ///
1930    /// Returns an error if the SQL SELECT or deserialization fails.
1931    pub async fn load_custom_data(
1932        pool: &PgPool,
1933        data_type: &DataType,
1934    ) -> anyhow::Result<Vec<CustomData>> {
1935        let metadata_json = data_type.metadata().as_ref().map_or(
1936            Ok(serde_json::Value::Object(serde_json::Map::new())),
1937            serde_json::to_value,
1938        )?;
1939
1940        let type_name = data_type.type_name();
1941        let short_type = type_name.rsplit([':', '.']).next().unwrap_or(type_name);
1942
1943        let rows = match data_type.identifier() {
1944            Some(identifier) => {
1945                sqlx::query(
1946                    r#"SELECT value, ts_event, ts_init FROM "custom"
1947                   WHERE (data_type = $1 OR data_type = $2)
1948                     AND metadata = $3
1949                     AND identifier = $4
1950                   ORDER BY ts_init ASC"#,
1951                )
1952                .bind(type_name)
1953                .bind(short_type)
1954                .bind(&metadata_json)
1955                .bind(identifier)
1956                .fetch_all(pool)
1957                .await
1958            }
1959            None => {
1960                sqlx::query(
1961                    r#"SELECT value, ts_event, ts_init FROM "custom"
1962                   WHERE (data_type = $1 OR data_type = $2)
1963                     AND metadata = $3
1964                     AND identifier = ''
1965                   ORDER BY ts_init ASC"#,
1966                )
1967                .bind(type_name)
1968                .bind(short_type)
1969                .bind(&metadata_json)
1970                .fetch_all(pool)
1971                .await
1972            }
1973        }
1974        .map_err(|e| anyhow::anyhow!("Failed to load custom data: {e}"))?;
1975
1976        let mut results = Vec::with_capacity(rows.len());
1977        for row in rows {
1978            let value_json: serde_json::Value = row.try_get("value")?;
1979            let json_bytes = serde_json::to_vec(&value_json)
1980                .map_err(|e| anyhow::anyhow!("Failed to serialize JSON: {e}"))?;
1981            let custom =
1982                CustomData::from_json_bytes(&json_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
1983            results.push(custom);
1984        }
1985        Ok(results)
1986    }
1987}