Skip to content

Commit 8790a1a

Browse files
authored
Merge pull request #87 from usherlabs/feat/order-archive-authorship
Archive the authoring component on order events
2 parents 7b8e0be + 79dcca6 commit 8790a1a

7 files changed

Lines changed: 146 additions & 5 deletions

File tree

schema/clickhouse/broker_execution.sql

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@
1515
CREATE DATABASE IF NOT EXISTS broker_execution;
1616

1717
-- Order lifecycle events: execute-action results and user-stream order updates.
18+
-- Plain MergeTree intentionally retains duplicates. They are expected from status
19+
-- polling (every GetOrderDetails observation is archived, while the strategy polls
20+
-- twice per cycle) and at-least-once WebSocket delivery. The canonical read-time
21+
-- dedup key is (exchange, account_selector, symbol, order_id, status,
22+
-- filled_amount), taking argMin(..., broker_observed_timestamp); there is no
23+
-- sequence or updated_at column from which to infer a later authoritative row.
1824
CREATE TABLE IF NOT EXISTS broker_execution.order_events
1925
(
2026
source LowCardinality(String),
2127
deployment_id LowCardinality(String),
2228
account_selector LowCardinality(String),
29+
-- Caller-declared order origin; a primary read key, default-empty when absent.
30+
order_author LowCardinality(String) DEFAULT '',
2331

2432
exchange LowCardinality(String),
2533
symbol LowCardinality(String),
@@ -65,6 +73,9 @@ ENGINE = MergeTree
6573
PARTITION BY toYYYYMM(parseDateTimeBestEffortOrZero(broker_observed_timestamp))
6674
ORDER BY (exchange, symbol, broker_observed_timestamp);
6775

76+
ALTER TABLE broker_execution.order_events
77+
ADD COLUMN IF NOT EXISTS order_author LowCardinality(String) DEFAULT '' AFTER account_selector;
78+
6879
-- CEX value movements: withdrawals, deposits, and sub<->master internal transfers.
6980
--
7081
-- Column names/types/ORDER BY match the fiet-maker consumer contract
@@ -131,9 +142,11 @@ ADD COLUMN IF NOT EXISTS client_withdrawal_id String DEFAULT '' AFTER external_i
131142
-- Column names/types/ORDER BY match the fiet-maker consumer contract: MergeTree,
132143
-- DateTime64 timestamps, string quantities, fill_index UInt32. The contract does
133144
-- not constrain retention; fills are execution audit facts and carry no TTL. Plain
134-
-- MergeTree (contract): the poller re-scans a lookback window after a restart, so
135-
-- the same trade can be re-inserted; dedup is at read time (GROUP BY / argMax over
136-
-- exchange, account_selector, symbol, order_id, fill_id).
145+
-- MergeTree (contract): the fill poller re-scans a 24-hour lookback window, so the
146+
-- same trade can be re-inserted. The canonical read-time dedup key is (exchange,
147+
-- account_selector, symbol, order_id, fill_id). fill_index is NOT a stable
148+
-- identifier and must not be used as a dedup key. There is no sequence or
149+
-- updated_at column from which to infer a later authoritative row.
137150
CREATE TABLE IF NOT EXISTS broker_execution.fill_events
138151
(
139152
broker_observed_timestamp DateTime64(3, 'UTC'),

src/handlers/execute-action/orders.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ async function handleCreateOrder(ctx: ExecuteActionContext): Promise<void> {
155155
orderType: orderValue.orderType,
156156
requestedQuantity: resolvedOrderTelemetry.requestedQuantity,
157157
requestedNotional: orderValue.amount * orderValue.price,
158+
orderAuthor: orderValue.orderAuthor,
158159
brokerObservedTimestamp: submissionTimestamp,
159160
...telemetryIds,
160161
};
@@ -191,6 +192,7 @@ async function handleCreateOrder(ctx: ExecuteActionContext): Promise<void> {
191192
requestedQuantity:
192193
resolvedOrderTelemetry.requestedQuantity ?? orderValue.amount,
193194
requestedNotional: orderValue.amount * orderValue.price,
195+
orderAuthor: orderValue.orderAuthor,
194196
...extractOrderTelemetryIds(createOrderParams),
195197
};
196198
emitOrderExecutionTelemetryInBackground(

src/handlers/execute-action/treasury-call.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export async function handleTreasuryCall(
9191
side: asNonEmptyString(side),
9292
requestedQuantity,
9393
requestedNotional,
94+
orderAuthor: callValue.orderAuthor,
9495
brokerObservedTimestamp: submissionTimestamp,
9596
...telemetryIds,
9697
};

src/helpers/broker-execution-archive/rows.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export function buildOrderEventArchiveRow(input: {
287287
action,
288288
subscription_type: input.subscriptionType,
289289
order_id: telemetry.orderId,
290+
order_author: telemetry.orderAuthor ?? "",
290291
client_order_id: telemetry.clientOrderId,
291292
idempotency_id: telemetry.idempotencyId,
292293
maker_action_id: telemetry.makerActionId,
@@ -357,11 +358,12 @@ export function buildSubscribeStreamArchiveRow(input: {
357358

358359
// Column shapes below follow the fiet-maker CEX_EXECUTION_ARCHIVE_CONTRACT: shared
359360
// tags + contract columns, all quantities/prices as strings (venue precision
360-
// varies by asset), fill_index/result_index as numbers (UInt32 columns). Two
361+
// varies by asset), fill_index/result_index as numbers (UInt32 columns). Three
361362
// deliberate ADDITIVE divergences the consumer contract doesn't yet list:
363+
// order_events carries order_author as a caller-declared primary read key;
362364
// transfer_events carries client_withdrawal_id plus fee_amount/fee_currency (the
363365
// ccxt withdrawal object exposes the fee, which is the dominant small-commit
364-
// cost), and fill_events.event_kind is stamped with the true trade-history-poller
366+
// cost); and fill_events.event_kind is stamped with the true trade-history-poller
365367
// source rather than "create_order_fill".
366368

367369
// Preserve venue precision: prefer the raw string the venue returned (usually in

src/helpers/order-telemetry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type OrderTelemetryContext = {
1919
orderType?: string;
2020
requestedQuantity?: number;
2121
requestedNotional?: number;
22+
orderAuthor?: string;
2223
clientOrderId?: string;
2324
idempotencyId?: string;
2425
makerActionId?: string;
@@ -34,6 +35,7 @@ export type OrderExecutionTelemetry = {
3435
side: string;
3536
orderType: string;
3637
orderId?: string;
38+
orderAuthor?: string;
3739
clientOrderId?: string;
3840
idempotencyId?: string;
3941
makerActionId?: string;
@@ -166,6 +168,7 @@ export function buildOrderExecutionTelemetry(
166168
orderType:
167169
firstString(record?.type, info?.type, context.orderType) ?? "unknown",
168170
orderId: firstString(record?.id, info?.orderId, info?.orderID),
171+
orderAuthor: context.orderAuthor,
169172
clientOrderId: firstString(
170173
context.clientOrderId,
171174
record?.clientOrderId,

src/schemas/action-payloads.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const DepositPayloadSchema = z.object({
4141
export const CallPayloadSchema = z.object({
4242
functionName: z.string().regex(/^[A-Za-z][A-Za-z0-9]*$/),
4343
args: z.preprocess(parseJsonString, z.array(z.unknown())).default([]),
44+
orderAuthor: z.string().min(1).optional(),
4445
params: z
4546
.preprocess(parseJsonString, z.record(z.string(), z.unknown()))
4647
.default({}),
@@ -84,6 +85,7 @@ export const CreateOrderPayloadSchema = z.object({
8485
price: z.coerce.number().positive(),
8586
marketType: marketTypeSchema,
8687
clientOrderId: z.string().min(1).optional(),
88+
orderAuthor: z.string().min(1).optional(),
8789
params: z.preprocess(parseJsonString, stringNumberRecordSchema).default({}),
8890
});
8991

test/broker-execution-archive.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import type { LogRecord } from "@opentelemetry/api-logs";
1414
import type { Exchange } from "@usherlabs/ccxt";
1515
import { MAX_ARCHIVE_BODY_BYTES } from "../services/archive-forwarder/limits";
1616
import type { ExecuteActionContext } from "../src/handlers/execute-action/context";
17+
import { handleOrders } from "../src/handlers/execute-action/orders";
18+
import { handleTreasuryCall } from "../src/handlers/execute-action/treasury-call";
1719
import { handleWithdraw } from "../src/handlers/execute-action/withdraw";
1820
import {
1921
redactSecretLiterals,
@@ -44,9 +46,11 @@ import {
4446
resolveArchiveForwarderUrlFromEnv,
4547
rethrowArchiveDurabilityError,
4648
} from "../src/helpers/broker-execution-archive/writer";
49+
import { Action } from "../src/helpers/constants";
4750
import { log } from "../src/helpers/logger";
4851
import { buildOrderExecutionTelemetry } from "../src/helpers/order-telemetry";
4952
import type { OtelLogs } from "../src/helpers/otel";
53+
import type { PolicyConfig } from "../src/types";
5054
import { startForwarderServer } from "./archive-forwarder-server";
5155

5256
const archiveTestDirectory = mkdtempSync(
@@ -286,6 +290,7 @@ describe("broker execution archive rows", () => {
286290
cex: "binance",
287291
accountLabel: "primary",
288292
symbol: "ARB/USDT",
293+
orderAuthor: "maker-alpha",
289294
clientOrderId: "client-1",
290295
makerActionId: "maker-1",
291296
},
@@ -311,6 +316,7 @@ describe("broker execution archive rows", () => {
311316
action: "CancelOrder",
312317
event_kind: "execute_action",
313318
order_id: "99",
319+
order_author: "maker-alpha",
314320
client_order_id: "client-1",
315321
maker_action_id: "maker-1",
316322
});
@@ -368,6 +374,7 @@ describe("broker execution archive rows", () => {
368374
]) {
369375
expect(orderRow.row).not.toHaveProperty(key);
370376
}
377+
expect(orderRow.row.order_author).toBe("");
371378

372379
const snapshotRow = buildMarketMetadataSnapshotRow({
373380
tags: buildCommonArchiveTags({
@@ -661,6 +668,117 @@ describe("broker execution archive rows", () => {
661668
});
662669
});
663670

671+
describe("order author archive plumbing", () => {
672+
test("archives authors from typed and Call createOrder without forwarding them to the venue", async () => {
673+
const forwarder = await startForwarderServer();
674+
const archiver = BrokerExecutionArchiver.create({
675+
forwarderUrl: forwarder.url,
676+
deadLetterPath: createDeadLetterPath(),
677+
deploymentId: "test-deploy",
678+
batchSize: 100,
679+
flushIntervalMs: 60_000,
680+
});
681+
const createOrderCalls: unknown[][] = [];
682+
const broker = {
683+
loadMarkets: async () => {},
684+
markets: {
685+
"USDC/USDT": {
686+
symbol: "USDC/USDT",
687+
base: "USDC",
688+
quote: "USDT",
689+
spot: true,
690+
type: "spot",
691+
},
692+
},
693+
createOrder: async (...args: unknown[]) => {
694+
createOrderCalls.push(args);
695+
return {
696+
id: `order-${createOrderCalls.length}`,
697+
symbol: args[0],
698+
type: args[1],
699+
side: args[2],
700+
amount: args[3],
701+
status: "open",
702+
filled: 0,
703+
};
704+
},
705+
} as unknown as Exchange;
706+
const policy = {
707+
order: { rule: { markets: ["*"], limits: [] } },
708+
} as unknown as PolicyConfig;
709+
const context = (
710+
action: (typeof Action)[keyof typeof Action],
711+
payload: Record<string, unknown>,
712+
) =>
713+
({
714+
action,
715+
call: { request: { payload } },
716+
wrappedCallback: () => {},
717+
policy,
718+
brokers: {},
719+
normalizedCex: "binance",
720+
cex: "binance",
721+
symbol: "USDC/USDT",
722+
selectedBrokerAccount: { exchange: broker, label: "primary" },
723+
broker,
724+
verity: { proof: "" },
725+
brokerArchiver: archiver,
726+
}) as unknown as ExecuteActionContext;
727+
const typedPayload = (orderAuthor?: string) => ({
728+
orderType: "limit",
729+
amount: "10",
730+
fromToken: "USDC",
731+
toToken: "USDT",
732+
price: "1",
733+
marketType: "spot",
734+
...(orderAuthor !== undefined && { orderAuthor }),
735+
params: JSON.stringify({ timeInForce: "GTC" }),
736+
});
737+
738+
try {
739+
await handleOrders(
740+
context(Action.CreateOrder, typedPayload("maker-alpha")),
741+
);
742+
await handleTreasuryCall(
743+
context(Action.Call, {
744+
functionName: "createOrder",
745+
args: JSON.stringify(["USDC/USDT", "limit", "buy", 5, 1]),
746+
orderAuthor: "funding-executor",
747+
params: JSON.stringify({ postOnly: true }),
748+
}),
749+
);
750+
await handleOrders(context(Action.CreateOrder, typedPayload()));
751+
752+
expect(createOrderCalls[0]?.[5]).toEqual({ timeInForce: "GTC" });
753+
expect(createOrderCalls[1]?.[5]).toEqual({ postOnly: true });
754+
expect(createOrderCalls[2]?.[5]).toEqual({ timeInForce: "GTC" });
755+
for (const call of createOrderCalls) {
756+
expect(call[5]).not.toHaveProperty("orderAuthor");
757+
}
758+
759+
await Promise.resolve();
760+
await archiver.flush();
761+
const orderRows = forwarder.requests
762+
.flatMap((request) => request.body.rows ?? [])
763+
.filter(
764+
(entry) => entry.table === "broker_execution.order_events",
765+
) as Array<{ row: Record<string, unknown> }>;
766+
const orderAuthors = Object.fromEntries(
767+
orderRows.map(({ row }) => [row.order_id, row.order_author]),
768+
);
769+
770+
expect(orderAuthors).toEqual({
771+
"order-1": "maker-alpha",
772+
"order-2": "funding-executor",
773+
"order-3": "",
774+
});
775+
} finally {
776+
await archiver.close();
777+
await forwarder.close();
778+
}
779+
});
780+
});
781+
664782
describe("withdrawal observation tracker", () => {
665783
function shouldArchive(
666784
tracker: WithdrawalObservationTracker,

0 commit comments

Comments
 (0)