fix: scope inventory fetch by submission time window to prevent OOM
All checks were successful
deploy / deploy (push) Successful in 48s
All checks were successful
deploy / deploy (push) Successful in 48s
Instead of fetching up to 50k inventory snapshots (~442MB), scope the query to only the time range of the current submission batch with a 15-minute buffer. For a typical 1-hour batch this drops from 50k rows to ~300 rows, well within the 1280Mi pod memory limit. The coalesced_at_idx on intent_inventory_snapshots covers the BETWEEN clause so this remains efficient. Proof: history-writer OOM kills from refreshQuoteOutcomes inventory fetch Assumptions: 15min buffer covers the attribution window for all submissions Still fake: heuristic gap outcomes may attribute trades imprecisely
This commit is contained in:
parent
aa8a868c14
commit
fb547f24d9
50 changed files with 4841 additions and 10 deletions
BIN
.DS_Store
vendored
Executable file
BIN
.DS_Store
vendored
Executable file
Binary file not shown.
BIN
._.DS_Store
Executable file
BIN
._.DS_Store
Executable file
Binary file not shown.
0
.codex
Normal file
0
.codex
Normal file
39
.githooks/commit-msg
Executable file
39
.githooks/commit-msg
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MSG_FILE="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$MSG_FILE" || ! -f "$MSG_FILE" ]]; then
|
||||||
|
echo "commit-msg hook: missing commit message file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
missing=0
|
||||||
|
|
||||||
|
check_line() {
|
||||||
|
local pattern="$1"
|
||||||
|
local label="$2"
|
||||||
|
if ! grep -q "^${pattern}" "$MSG_FILE"; then
|
||||||
|
echo "missing required commit metadata: ${label}" >&2
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_line "Proof:" "Proof:"
|
||||||
|
check_line "Assumptions:" "Assumptions:"
|
||||||
|
check_line "Still fake:" "Still fake:"
|
||||||
|
|
||||||
|
if [[ "$missing" -ne 0 ]]; then
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
|
||||||
|
Required non-merge commit message body lines:
|
||||||
|
Proof: <which approved proof or charter this serves>
|
||||||
|
Assumptions: <key assumptions or constraints>
|
||||||
|
Still fake: <what remains mocked, unverified, or incomplete>
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
# Implementation Turn: pre-credit funding visibility and operator alerts
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-02
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Make the already-live BTC/EURe loop operationally understandable between external funding and spendable credit, and make critical stale or failed states queryable as durable alert records instead of transient logs.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- [O003] Alerts for stale reference prices, stale inventory state, stuck funding actions, and failed executor submissions.
|
||||||
|
- [O004] Pre-credit funding visibility for slow chains: watch configured deposit addresses at chain level, track inbound transfers through mempool and on-chain confirmation before bridge credit, persist that state separately from spendable inventory, and alert operators when funding is seen, delayed, or stuck.
|
||||||
|
|
||||||
|
## Design rule
|
||||||
|
Keep the already-proven execution and inventory truth path intact:
|
||||||
|
- verifier and bridge credit remain the only spendable truth
|
||||||
|
- pre-credit visibility is additive observability
|
||||||
|
- alerts are additive operability
|
||||||
|
|
||||||
|
Also keep the future operator architecture clean:
|
||||||
|
- no browser-direct Kafka or Redpanda dependency
|
||||||
|
- future dashboard reads PostgreSQL for history and service HTTP state for live controls and freshness
|
||||||
|
- this turn should therefore emit stable records and machine-friendly current-state summaries rather than log-only clues
|
||||||
|
|
||||||
|
## Event backbone
|
||||||
|
Retain Kafka as the backbone. Add only the minimal new topics required:
|
||||||
|
|
||||||
|
- `ops.funding_observation`
|
||||||
|
- `ops.alert`
|
||||||
|
|
||||||
|
These topics are append-only evidence streams, not control planes.
|
||||||
|
|
||||||
|
## Durable store
|
||||||
|
Extend PostgreSQL with new append-only families:
|
||||||
|
- `funding_observations`
|
||||||
|
- `ops_alerts`
|
||||||
|
|
||||||
|
If a current-state materialization is needed, derive it from append-only records or keep it as a clearly named snapshot table. Do not replace the append-only record.
|
||||||
|
|
||||||
|
## Service changes
|
||||||
|
|
||||||
|
### 1. `liquidity-manager`
|
||||||
|
Extend the existing treasury owner instead of inventing a broad new funding stack.
|
||||||
|
|
||||||
|
New responsibilities:
|
||||||
|
- retain active funding handles by chain and asset
|
||||||
|
- poll configured chain observers for those handles
|
||||||
|
- emit `ops.funding_observation`
|
||||||
|
- correlate chain observations with bridge `recent_deposits`
|
||||||
|
- expose latest observations and credit-correlation state through `/state`
|
||||||
|
|
||||||
|
Expected state shape additions:
|
||||||
|
- `funding_observations_by_handle`
|
||||||
|
- `latest_funding_observation_at`
|
||||||
|
- `uncredited_funding_total_by_asset`
|
||||||
|
- `credit_correlation`
|
||||||
|
- `observer_health`
|
||||||
|
- `observer_age_ms` or equivalent freshness signal
|
||||||
|
|
||||||
|
Control additions:
|
||||||
|
- `POST /refresh-funding-observations`
|
||||||
|
- optional `POST /pause-funding-observer`
|
||||||
|
- optional `POST /resume-funding-observer`
|
||||||
|
|
||||||
|
Important implementation constraints:
|
||||||
|
- do not change withdrawal behavior
|
||||||
|
- do not reuse spendable inventory fields for pre-credit state
|
||||||
|
- keep BTC and EURe observation records in one shared schema
|
||||||
|
|
||||||
|
### 2. `inventory-sync`
|
||||||
|
Keep current spendable accounting intact.
|
||||||
|
|
||||||
|
Possible additions:
|
||||||
|
- read the latest funding observations
|
||||||
|
- expose a separate `pre_credit_inbound` or `funding_visibility` field in `/state`
|
||||||
|
|
||||||
|
Hard rule:
|
||||||
|
- `spendable`, `pending_inbound`, and strategy-facing credited truth must not become looser
|
||||||
|
|
||||||
|
### 3. `history-writer`
|
||||||
|
Consume and persist the new topics:
|
||||||
|
- `ops.funding_observation`
|
||||||
|
- `ops.alert`
|
||||||
|
|
||||||
|
Expose through `/state`:
|
||||||
|
- latest funding-observation write time
|
||||||
|
- latest alert write time
|
||||||
|
- counts or offsets for the new topics
|
||||||
|
|
||||||
|
Add query-friendly indexes for:
|
||||||
|
- `tx_hash`
|
||||||
|
- `funding_handle`
|
||||||
|
- `alert_code`
|
||||||
|
- `status`
|
||||||
|
- `asset_id`
|
||||||
|
- `chain`
|
||||||
|
- `ingested_at`
|
||||||
|
|
||||||
|
If this turn needs a current-state SQL materialization for future operator queries, keep it narrow and explicit, for example:
|
||||||
|
- latest funding observation per handle
|
||||||
|
- currently active alerts
|
||||||
|
|
||||||
|
Do not replace append-only records with mutable-only state.
|
||||||
|
|
||||||
|
### 4. `ops-sentinel` or equivalent alert evaluator
|
||||||
|
Add one small service only if needed to keep alert logic separate and testable.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
- consume:
|
||||||
|
- `ref.market_price`
|
||||||
|
- `state.intent_inventory`
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
- `ops.funding_observation`
|
||||||
|
- `exec.trade_result`
|
||||||
|
- evaluate policy windows for stale and stuck conditions
|
||||||
|
- emit `ops.alert` raise/clear transitions
|
||||||
|
- expose current alert state
|
||||||
|
|
||||||
|
Preferred alert model:
|
||||||
|
- stable `alert_code`
|
||||||
|
- `status`: `raised` or `cleared`
|
||||||
|
- `severity`
|
||||||
|
- `reason`
|
||||||
|
- `first_raised_at`
|
||||||
|
- `last_evaluated_at`
|
||||||
|
- correlation IDs when available
|
||||||
|
|
||||||
|
State shape should be future-dashboard-friendly:
|
||||||
|
- `active_alerts`
|
||||||
|
- `recent_transitions`
|
||||||
|
- `last_evaluated_at`
|
||||||
|
- `stale`
|
||||||
|
- `paused`
|
||||||
|
|
||||||
|
Minimal alert set for this turn:
|
||||||
|
- `reference_price_stale`
|
||||||
|
- `inventory_snapshot_stale`
|
||||||
|
- `funding_seen_unconfirmed`
|
||||||
|
- `funding_confirmed_credit_pending`
|
||||||
|
- `funding_stuck`
|
||||||
|
- `executor_submission_failed`
|
||||||
|
|
||||||
|
Do not add Slack, email, or paging integrations in this turn unless required to prove the path. Durable alert records plus HTTP state are sufficient.
|
||||||
|
|
||||||
|
## Chain observer plan
|
||||||
|
|
||||||
|
### BTC
|
||||||
|
Must be the first-class proof path.
|
||||||
|
|
||||||
|
Implementation expectations:
|
||||||
|
- configurable observer endpoint
|
||||||
|
- look up the configured BTC deposit address
|
||||||
|
- detect:
|
||||||
|
- mempool appearance when available
|
||||||
|
- confirmation count
|
||||||
|
- credited transition once bridge/verifier catches up
|
||||||
|
|
||||||
|
Assumption to keep explicit in code:
|
||||||
|
- a chain observer can disappear or lag independently of the bridge
|
||||||
|
|
||||||
|
### Gnosis / EURe
|
||||||
|
Nice-to-have within the same schema, but BTC is the proof-critical path.
|
||||||
|
|
||||||
|
If included this turn:
|
||||||
|
- watch the configured deposit address for EURe token transfers
|
||||||
|
- represent observation state in the same event model
|
||||||
|
|
||||||
|
## Record shapes
|
||||||
|
|
||||||
|
### `ops.funding_observation`
|
||||||
|
Required fields:
|
||||||
|
- `funding_observation_id`
|
||||||
|
- `account_id`
|
||||||
|
- `asset_id`
|
||||||
|
- `chain`
|
||||||
|
- `funding_handle`
|
||||||
|
- `source`
|
||||||
|
- `tx_hash`
|
||||||
|
- `status`
|
||||||
|
- `amount`
|
||||||
|
- `confirmations`
|
||||||
|
- `first_seen_at`
|
||||||
|
- `last_seen_at`
|
||||||
|
- `credited_at` when known
|
||||||
|
- `bridge_deposit_tx_hash` when correlated
|
||||||
|
|
||||||
|
### `ops.alert`
|
||||||
|
Required fields:
|
||||||
|
- `alert_event_id`
|
||||||
|
- `alert_code`
|
||||||
|
- `status`
|
||||||
|
- `severity`
|
||||||
|
- `reason`
|
||||||
|
- `service_scope`
|
||||||
|
- `pair` when relevant
|
||||||
|
- `asset_id` when relevant
|
||||||
|
- `tx_hash` when relevant
|
||||||
|
- `raised_at`
|
||||||
|
- `cleared_at`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
## Control surface expectations
|
||||||
|
|
||||||
|
### `liquidity-manager`
|
||||||
|
Must expose:
|
||||||
|
- active deposit handles
|
||||||
|
- latest pre-credit funding observations
|
||||||
|
- latest credit correlation
|
||||||
|
- whether the funding observer is healthy or paused
|
||||||
|
- explicit ages or timestamps so the future top bar can mark stale data without inferring it from logs
|
||||||
|
|
||||||
|
### alert evaluator
|
||||||
|
Must expose:
|
||||||
|
- current active alerts
|
||||||
|
- latest cleared alerts
|
||||||
|
- per-alert evaluation timestamps
|
||||||
|
- pause state
|
||||||
|
- enough stable fields that a future dashboard backend can render alert lists and severity without custom log parsing
|
||||||
|
|
||||||
|
### `history-writer`
|
||||||
|
Must expose the new topic offsets and write status for funding observations and alerts.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
Required automated coverage:
|
||||||
|
- BTC funding observation remains non-spendable before credit
|
||||||
|
- alert transitions raise then clear on recovered stale state
|
||||||
|
- funding observation correlates to a later credited deposit without losing the original tx hash
|
||||||
|
- executor failure produces an alert event
|
||||||
|
|
||||||
|
If a meaningful automated test cannot be written for a subpath, stop and record why instead of hand-waving.
|
||||||
|
|
||||||
|
## Validation plan
|
||||||
|
- Safe induced stale-price alert:
|
||||||
|
- pause `market-reference-ingest`
|
||||||
|
- wait past freshness window
|
||||||
|
- observe `reference_price_stale`
|
||||||
|
- resume and observe clear
|
||||||
|
- Safe induced stale-inventory alert:
|
||||||
|
- pause `inventory-sync`
|
||||||
|
- wait past freshness window
|
||||||
|
- observe `inventory_snapshot_stale`
|
||||||
|
- resume and observe clear
|
||||||
|
- Funding visibility proof:
|
||||||
|
- use a real deposit address
|
||||||
|
- observe pre-credit chain state before bridge credit where timing allows
|
||||||
|
- later observe credit correlation
|
||||||
|
- Executor failure alert proof:
|
||||||
|
- use a controlled non-destructive failure mode such as temporary relay endpoint override in a safe environment or a replayable failure fixture
|
||||||
|
- verify `executor_submission_failed`
|
||||||
|
|
||||||
|
## Future dashboard handoff
|
||||||
|
This turn should leave the next dashboard turn with these ready-made data sources:
|
||||||
|
- PostgreSQL history for funding observations and alerts
|
||||||
|
- service `/state` for current funding status, current alert status, balances, arming state, and freshness
|
||||||
|
- stable timestamps and reason fields that can drive:
|
||||||
|
- a top status bar
|
||||||
|
- a funds page
|
||||||
|
- a system alert list
|
||||||
|
|
||||||
|
The next dashboard turn should not need to invent interpretation logic that belongs in this turn's services.
|
||||||
|
|
||||||
|
## Out of scope on purpose
|
||||||
|
- No new trading strategy
|
||||||
|
- No historical backtest engine
|
||||||
|
- No broad observability stack
|
||||||
|
- No polished dashboard frontend
|
||||||
|
- No automated treasury refills
|
||||||
|
|
||||||
|
## Still fake at turn open
|
||||||
|
- Pre-credit funding visibility is still missing from the live cluster.
|
||||||
|
- Alert state is still mostly implicit in service logs and manual inspection.
|
||||||
|
- There is no durable operator-facing record yet for "funds are on the way but not spendable."
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# Implementation Proof: pre-credit funding visibility and operator alerts
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-02
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The next turn is complete only when `unrip` can show operators the gap between "funds sent" and "funds spendable" with durable evidence, and can surface actionable alert state for the live loop without requiring log-diving or manual SQL every time something stalls.
|
||||||
|
|
||||||
|
This turn does not expand the trade hot path. It makes the existing live system more explainable and more operable.
|
||||||
|
|
||||||
|
## Hypothesis
|
||||||
|
`unrip` becomes materially safer to operate once it can:
|
||||||
|
|
||||||
|
1. observe configured funding handles before NEAR Intents credit
|
||||||
|
2. persist chain-level funding observations separately from spendable inventory
|
||||||
|
3. link pre-credit observations to later bridge and verifier credit where possible
|
||||||
|
4. emit durable alert state for stale prices, stale inventory, stuck funding, and failed execution submissions
|
||||||
|
5. expose that state through the same small control surfaces and PostgreSQL audit trail as the rest of the system
|
||||||
|
|
||||||
|
If pre-credit funding remains invisible, or alert state still lives only in transient logs, the live loop is still too opaque for routine funded operation.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- [O003] Alerts for stale reference prices, stale inventory state, stuck funding actions, and failed executor submissions.
|
||||||
|
- [O004] Pre-credit funding visibility for slow chains: watch configured deposit addresses at chain level, track inbound transfers through mempool and on-chain confirmation before bridge credit, persist that state separately from spendable inventory, and alert operators when funding is seen, delayed, or stuck.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No new venue, pair, or strategy logic.
|
||||||
|
- No dashboard or polished UI.
|
||||||
|
- No automatic treasury actions or auto-refunding.
|
||||||
|
- No attempt to treat chain-level observations as spendable inventory.
|
||||||
|
- No change to the live execution arming model.
|
||||||
|
|
||||||
|
## Source-of-truth rule
|
||||||
|
Spendable inventory remains the existing truth:
|
||||||
|
- bridge and verifier credit determine spendable balances
|
||||||
|
- chain-level observations are visibility only
|
||||||
|
|
||||||
|
The new pre-credit path must never be allowed to make a direction tradable earlier than the verifier does.
|
||||||
|
|
||||||
|
## Future dashboard compatibility rule
|
||||||
|
This turn does not build the dashboard, but it must leave behind clean machine-facing surfaces for it.
|
||||||
|
|
||||||
|
- Future operator UI should read PostgreSQL plus service HTTP state, not Kafka directly from the browser.
|
||||||
|
- Funding-observation and alert records must therefore be query-friendly and stable enough for a future dashboard backend to consume without parsing logs.
|
||||||
|
- Current-state APIs introduced or extended in this turn must expose explicit freshness fields such as timestamps, ages, health, or stale status where practical.
|
||||||
|
- If a current-state materialization is needed, it must be clearly separated from append-only evidence so the UI can read current truth without losing historical auditability.
|
||||||
|
|
||||||
|
## Required runtime behavior
|
||||||
|
|
||||||
|
### Funding visibility
|
||||||
|
- The system must know the currently active funding handles for BTC and EURe.
|
||||||
|
- For configured chains, it must watch those handles before NEAR Intents credit appears.
|
||||||
|
- It must distinguish at least these states where applicable:
|
||||||
|
- `SEEN_UNCONFIRMED`
|
||||||
|
- `SEEN_CONFIRMED`
|
||||||
|
- `CREDIT_PENDING`
|
||||||
|
- `CREDITED`
|
||||||
|
- `FAILED_OR_STUCK`
|
||||||
|
- BTC is the must-prove chain because that is where live funding latency was operationally visible.
|
||||||
|
- Gnosis support may share the same event model even if its confirmation behavior is simpler.
|
||||||
|
|
||||||
|
### Alerts
|
||||||
|
- Alerts must be durable records, not only log lines.
|
||||||
|
- At minimum the system must raise and clear alert state for:
|
||||||
|
- stale reference price
|
||||||
|
- stale inventory snapshot
|
||||||
|
- funding seen but not credited within policy
|
||||||
|
- execution submission failure
|
||||||
|
- Alert transitions must be inspectable through HTTP state and PostgreSQL.
|
||||||
|
|
||||||
|
## Service expectations
|
||||||
|
|
||||||
|
### `liquidity-manager`
|
||||||
|
Must become the owner of chain-level funding observations because it already owns deposit handles and treasury state.
|
||||||
|
|
||||||
|
It must:
|
||||||
|
- refresh and retain active funding handles
|
||||||
|
- ingest chain-level funding observations
|
||||||
|
- reconcile them against bridge deposit state
|
||||||
|
- publish durable funding-observation records
|
||||||
|
- expose current per-handle funding state
|
||||||
|
|
||||||
|
It must not:
|
||||||
|
- mark funds spendable
|
||||||
|
- trade on pre-credit observations
|
||||||
|
|
||||||
|
### `inventory-sync`
|
||||||
|
May surface pre-credit funding context, but only under a clearly separate non-spendable field.
|
||||||
|
|
||||||
|
It must not:
|
||||||
|
- merge pre-credit observations into `spendable`
|
||||||
|
|
||||||
|
### `history-writer`
|
||||||
|
Must persist the new record families:
|
||||||
|
- funding observations
|
||||||
|
- alert events
|
||||||
|
- optionally current alert snapshots if the implementation separates events from state
|
||||||
|
|
||||||
|
### Alert evaluator
|
||||||
|
This may be a new small service or a tightly scoped extension of an existing one, but it must:
|
||||||
|
- evaluate staleness and stuck conditions from durable inputs
|
||||||
|
- emit durable alert events
|
||||||
|
- expose current alert state and the latest reasons
|
||||||
|
|
||||||
|
No broad orchestration or dashboard service should be introduced just to satisfy this proof.
|
||||||
|
|
||||||
|
## Required durable storage
|
||||||
|
PostgreSQL must store at least:
|
||||||
|
- funding observations before bridge credit
|
||||||
|
- alert events or alert snapshots
|
||||||
|
- enough timestamps and IDs to correlate:
|
||||||
|
- funding handle
|
||||||
|
- chain tx hash
|
||||||
|
- later bridge tx hash or deposit record
|
||||||
|
- resulting verifier credit snapshot when available
|
||||||
|
|
||||||
|
Kafka remains the event backbone.
|
||||||
|
|
||||||
|
## Required control surface
|
||||||
|
At minimum operators must be able to inspect:
|
||||||
|
- active funding handles
|
||||||
|
- latest pre-credit observations by handle
|
||||||
|
- confirmation depth or equivalent chain state when available
|
||||||
|
- whether a funding action is still pending credit
|
||||||
|
- current active alerts and their reasons
|
||||||
|
- when the observed state was last refreshed and whether it is stale
|
||||||
|
|
||||||
|
If a new alert service exists, it must expose:
|
||||||
|
- `GET /healthz`
|
||||||
|
- `GET /state`
|
||||||
|
- `POST /pause`
|
||||||
|
- `POST /resume`
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- The live cluster still runs the previously proven funded trade loop unchanged for spendable truth.
|
||||||
|
- At least one real funding handle is watched at chain level before bridge credit.
|
||||||
|
- For at least one real deposit path, the system records a pre-credit observation before or during confirmation and later records the credited state separately.
|
||||||
|
- PostgreSQL contains durable records for the pre-credit funding path and alert path.
|
||||||
|
- Operators can inspect current funding observations and current alerts through control APIs.
|
||||||
|
- The new observation and alert records are shaped so a future dashboard backend can read them from PostgreSQL and HTTP state without scraping logs or Kafka.
|
||||||
|
- A stale price or stale inventory condition can be induced safely and becomes a durable alert.
|
||||||
|
- A funding delay or manually injected stuck condition can be represented as a durable alert with explicit reason fields.
|
||||||
|
- A failed execution submission path is represented as an alert without inventing fake venue traffic.
|
||||||
|
- Tests cover:
|
||||||
|
- pre-credit observations staying non-spendable
|
||||||
|
- alert raise and clear transitions
|
||||||
|
- correlation of funding observation to later credit when identifiers are available
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- Funding observations exist only in logs and are not queryable later.
|
||||||
|
- Pre-credit observations leak into spendable inventory or strategy gating.
|
||||||
|
- Alerts cannot be queried as current state.
|
||||||
|
- The only proof of stuck funding is a human manually watching a block explorer.
|
||||||
|
- The implementation adds a dashboard shell without stronger runtime truth.
|
||||||
|
|
||||||
|
## Current real
|
||||||
|
- The first funded BTC/EURe live loop is already proven:
|
||||||
|
- real quote ingest
|
||||||
|
- real reference pricing
|
||||||
|
- real credited inventory
|
||||||
|
- real strategy decisions
|
||||||
|
- real `quote_response` submissions
|
||||||
|
- durable event chain in PostgreSQL
|
||||||
|
- Portfolio metrics are now durably computed and exposed, but alerting and pre-credit funding visibility are still incomplete.
|
||||||
|
|
@ -0,0 +1,401 @@
|
||||||
|
# Implementation Turn: operator dashboard foundation, funds desk, and operator controls
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-04
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Build a real operator dashboard service for the live BTC/EURe system so routine supervision no longer depends on ad hoc `curl`, `kubectl`, and SQL. The dashboard must read real durable and live state, make freshness obvious, and answer `are we making money` before it answers lower-level operator questions. It must also provide a narrow set of real operator controls without pretending that risky treasury actions are routine.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- [I010] Operator dashboard foundation: dedicated dashboard pod plus backend API that reads PostgreSQL and service `/state`, with a global live status bar, arm/disarm controls, stale-state display, and `Funds` as the default home page.
|
||||||
|
- [I011] Funds desk: current balances, EUR-equivalent value, deposit-handle reveal with retrieval timestamps, emergency stop plus withdraw-all control path, deposit/withdraw history, trade-driven asset-change history, and flexible withdrawal forms by asset and EUR equivalent.
|
||||||
|
- [I014] Strategy and system operator pages: active strategy settings, skip reasons, alert state, service health, topic lag, persistence health, and cluster-operator stats.
|
||||||
|
|
||||||
|
## Design rule
|
||||||
|
The dashboard is a reader and operator surface over existing truths. It must not invent a second truth model.
|
||||||
|
|
||||||
|
- PostgreSQL remains the durable store for historical records and portfolio metrics.
|
||||||
|
- Existing services remain the owners of live state and control actions.
|
||||||
|
- The dashboard backend aggregates those sources and exposes one browser-facing API.
|
||||||
|
- The frontend must not talk to Kafka, Redpanda, PostgreSQL, or internal app services directly.
|
||||||
|
- Frontend polling is prohibited. Live updates must flow over WebSockets from the dashboard backend.
|
||||||
|
|
||||||
|
## Profitability-first rule
|
||||||
|
The default dashboard experience should answer these operator questions in order:
|
||||||
|
- what is the portfolio worth in EUR right now
|
||||||
|
- are we up or down versus the original deposits
|
||||||
|
- are we beating simple hold of the original deposits
|
||||||
|
- how much of the result appears to come from market move versus trading
|
||||||
|
- are trades actually happening right now
|
||||||
|
|
||||||
|
Operational telemetry should support that answer, not displace it.
|
||||||
|
|
||||||
|
## Deployment shape
|
||||||
|
Add one new service:
|
||||||
|
- `operator-dashboard`
|
||||||
|
|
||||||
|
Preferred shape:
|
||||||
|
- one Node service in the existing image
|
||||||
|
- serves static frontend assets plus JSON API plus WebSocket endpoint
|
||||||
|
- cluster-local by default, operator-accessed by port-forward or explicitly approved exposure later
|
||||||
|
|
||||||
|
Expected config additions:
|
||||||
|
- dashboard host or port
|
||||||
|
- PostgreSQL connection string
|
||||||
|
- Kafka brokers and a dedicated dashboard consumer group
|
||||||
|
- internal base URLs for:
|
||||||
|
- `market-reference-ingest`
|
||||||
|
- `inventory-sync`
|
||||||
|
- `liquidity-manager`
|
||||||
|
- `history-writer`
|
||||||
|
- `ops-sentinel`
|
||||||
|
- `strategy-engine`
|
||||||
|
- `trade-executor`
|
||||||
|
- optionally `near-intents-ingest`
|
||||||
|
- optional namespace or Kubernetes API settings if cluster state is included
|
||||||
|
- auth mode and any future operator-auth placeholders needed to keep the backend auth contract stable
|
||||||
|
|
||||||
|
## Backend architecture
|
||||||
|
The dashboard backend should have four cooperating parts:
|
||||||
|
|
||||||
|
1. REST API for bootstrap, history, and control actions
|
||||||
|
2. WebSocket hub for live push updates to the browser
|
||||||
|
3. PostgreSQL query layer for durable history and metrics
|
||||||
|
4. Kafka consumer layer for live event ingestion without polling
|
||||||
|
|
||||||
|
High-level flow:
|
||||||
|
- browser loads the page and calls dashboard REST endpoints for initial bootstrap data
|
||||||
|
- browser opens one backend WebSocket connection
|
||||||
|
- backend fans live events from Kafka into normalized WebSocket messages
|
||||||
|
- backend uses PostgreSQL for historical lists, pagination, and durable baseline data
|
||||||
|
- backend uses internal service REST calls for current control state and to execute allowlisted actions
|
||||||
|
|
||||||
|
The browser should never need its own polling timers.
|
||||||
|
|
||||||
|
## Auth skeleton
|
||||||
|
Add backend auth and authorization from the start, even if the first provider is permissive.
|
||||||
|
|
||||||
|
Required shape:
|
||||||
|
- a request-level auth middleware for REST
|
||||||
|
- a connection-level auth gate for WebSockets
|
||||||
|
- a structured auth context on every request or socket, for example:
|
||||||
|
- `authenticated`
|
||||||
|
- `subject`
|
||||||
|
- `mode`
|
||||||
|
- `roles` or equivalent
|
||||||
|
|
||||||
|
Initial implementation may be something like:
|
||||||
|
- `mode = stub`
|
||||||
|
- `authenticated = true`
|
||||||
|
- `subject = local-operator`
|
||||||
|
|
||||||
|
But route handlers and socket handlers must already depend on auth context rather than bypass auth entirely.
|
||||||
|
|
||||||
|
## Backend responsibilities
|
||||||
|
|
||||||
|
### 0. Authenticated API and socket surface
|
||||||
|
The backend should expose:
|
||||||
|
- a bootstrap or session route that tells the frontend whether it is authenticated
|
||||||
|
- authenticated JSON API routes
|
||||||
|
- an authenticated WebSocket channel for live dashboard updates
|
||||||
|
|
||||||
|
The goal is that future real auth can replace the provider, not the route architecture.
|
||||||
|
|
||||||
|
### 1. Aggregate durable history from PostgreSQL
|
||||||
|
The backend should query PostgreSQL directly for:
|
||||||
|
- latest portfolio metric from `portfolio_metrics_snapshots`
|
||||||
|
- deposit-time baseline and simple-hold benchmark inputs from credited deposit records plus reference-price history, or from durable precomputed portfolio metrics if those already exist
|
||||||
|
- recent deposits and withdrawals from the latest inventory snapshot
|
||||||
|
- recent funding observations from `funding_observations`
|
||||||
|
- active or recent alerts from `ops_alerts`
|
||||||
|
- recent raw or normalized quotes from:
|
||||||
|
- `raw_near_intents_quotes`
|
||||||
|
- or `swap_demand_events`, whichever is cleaner for the operator surface
|
||||||
|
- recent trade commands, results, and linked decision metadata from:
|
||||||
|
- `execute_trade_commands`
|
||||||
|
- `trade_execution_results`
|
||||||
|
- `trade_decisions`
|
||||||
|
- latest market price and latest inventory snapshots as durable fallback truth
|
||||||
|
|
||||||
|
The backend should normalize these into dashboard-oriented response shapes rather than leaking raw SQL rows to the frontend.
|
||||||
|
If benchmark or attribution values are derived from incomplete data, the response shape should carry explicit caveats rather than silently presenting them as fully precise.
|
||||||
|
|
||||||
|
### 2. Aggregate live state from service control APIs
|
||||||
|
The backend should read `/state` and `/healthz` from the live services and normalize:
|
||||||
|
- freshness timestamps
|
||||||
|
- stale status
|
||||||
|
- pause state
|
||||||
|
- armed state
|
||||||
|
- last errors
|
||||||
|
- active alerts
|
||||||
|
- funding observer status
|
||||||
|
- writer offsets and database connectivity
|
||||||
|
|
||||||
|
The backend should make failures explicit per upstream service so one dead app does not blank the whole dashboard.
|
||||||
|
These reads should happen on demand for bootstrap and action-result flows, not through periodic polling loops.
|
||||||
|
|
||||||
|
### 3. Live event fan-out from Kafka to WebSockets
|
||||||
|
To satisfy the no-polling rule, the dashboard backend should consume Kafka directly and publish normalized WebSocket events.
|
||||||
|
|
||||||
|
Initial live topics should include at least:
|
||||||
|
- `raw.near_intents.quote` or `norm.swap_demand` for recent quote activity
|
||||||
|
- `ref.market_price` for price freshness and top-bar updates
|
||||||
|
- `state.intent_inventory` for balance freshness and valuation updates
|
||||||
|
- `ops.alert` for alert count and transition updates
|
||||||
|
- `exec.trade_result` for successful trade feed updates
|
||||||
|
|
||||||
|
The backend should maintain small in-memory current views such as:
|
||||||
|
- recent quote ring buffer limited to 10 items
|
||||||
|
- latest market price summary
|
||||||
|
- latest inventory summary
|
||||||
|
- active alert count or latest transitions
|
||||||
|
|
||||||
|
This keeps the browser live without polling while PostgreSQL remains the durable history source.
|
||||||
|
|
||||||
|
### 4. Optional read-only cluster state
|
||||||
|
For the system page, add a narrow read-only cluster view if practical:
|
||||||
|
- deployment readiness
|
||||||
|
- pod restart counts
|
||||||
|
- pod age
|
||||||
|
- PVC presence and mount health where useful
|
||||||
|
|
||||||
|
If this is included, use namespace-scoped read-only access only. Do not add broad cluster-admin behavior to satisfy a dashboard.
|
||||||
|
If live updates are needed here, prefer Kubernetes watch semantics or explicit refresh actions over polling loops.
|
||||||
|
|
||||||
|
## Frontend shape
|
||||||
|
Use the smallest frontend that can produce a real operator experience.
|
||||||
|
|
||||||
|
Preferred implementation:
|
||||||
|
- server-served static HTML, CSS, and JavaScript
|
||||||
|
- no dashboard-specific heavyweight frontend stack unless it materially reduces risk
|
||||||
|
- explicit operator refresh actions are acceptable
|
||||||
|
- polling is not acceptable
|
||||||
|
|
||||||
|
Expected browser behavior:
|
||||||
|
- one initial bootstrap fetch for page data
|
||||||
|
- one authenticated WebSocket connection for live updates
|
||||||
|
- no interval timers that refetch API routes
|
||||||
|
|
||||||
|
The UI should feel like a serious operator surface, not a placeholder admin page.
|
||||||
|
|
||||||
|
## Page plan
|
||||||
|
|
||||||
|
### Global status bar
|
||||||
|
The shared top bar should show:
|
||||||
|
- active pair
|
||||||
|
- latest EUR or EURe per BTC price
|
||||||
|
- market freshness and inventory freshness
|
||||||
|
- active alert count
|
||||||
|
- strategy armed state
|
||||||
|
- executor armed state
|
||||||
|
- total EUR portfolio value
|
||||||
|
|
||||||
|
The bar should color or label stale and degraded conditions explicitly.
|
||||||
|
The values in the bar should update from WebSocket events.
|
||||||
|
|
||||||
|
### Funds page
|
||||||
|
This is the default home page.
|
||||||
|
|
||||||
|
Sections to include:
|
||||||
|
- profitability summary at the top:
|
||||||
|
- current total portfolio value in EUR
|
||||||
|
- profit or loss versus deposit-time baseline
|
||||||
|
- profit or loss versus simple hold of the original deposits
|
||||||
|
- compact attribution between market move and trading contribution
|
||||||
|
- last successful trade time and total successful trade count
|
||||||
|
- explicit caveat text when fees or per-trade realized net profit are not fully tracked yet
|
||||||
|
- current balances by asset in nominal units
|
||||||
|
- EUR-equivalent valuation by asset and in total
|
||||||
|
- latest synced inventory timestamp and reconciliation status
|
||||||
|
- current funding handles and current funding observation state
|
||||||
|
- pre-credit funding visibility separated from spendable balances
|
||||||
|
- recent deposits and withdrawals
|
||||||
|
- recent trade-driven asset changes
|
||||||
|
- portfolio metric summary from the durable metric table
|
||||||
|
|
||||||
|
Control affordances on this page should prioritize safe actions already supported by the system:
|
||||||
|
- refresh funds or liquidity state
|
||||||
|
- pause or resume funding observer
|
||||||
|
- freeze or unfreeze withdrawals
|
||||||
|
- withdrawal estimate form by asset and amount
|
||||||
|
|
||||||
|
Actual live withdrawal submission is not part of this turn unless the user explicitly approves that risk during implementation.
|
||||||
|
|
||||||
|
### Activity section
|
||||||
|
Add a small operator activity surface, either as its own page or as a clearly named section.
|
||||||
|
|
||||||
|
It must include:
|
||||||
|
- recent incoming quotes: 10 most recent, live updated over WebSockets
|
||||||
|
- successful trades ledger: all successful trades, paginated from PostgreSQL
|
||||||
|
|
||||||
|
Keep this narrow:
|
||||||
|
- no bucket analytics
|
||||||
|
- no quote histograms
|
||||||
|
- no hindsight trade grading
|
||||||
|
|
||||||
|
Useful fields for recent quotes:
|
||||||
|
- observed time
|
||||||
|
- pair or asset direction
|
||||||
|
- request kind
|
||||||
|
- amount in and amount out when available
|
||||||
|
|
||||||
|
Useful fields for successful trades:
|
||||||
|
- time
|
||||||
|
- pair
|
||||||
|
- request kind
|
||||||
|
- submitted size
|
||||||
|
- result code
|
||||||
|
- linked decision edge when available
|
||||||
|
|
||||||
|
This activity surface should support the profitability view rather than compete with it. Recent trade count and last-trade recency should also feed the top-level profit summary.
|
||||||
|
|
||||||
|
### Strategy page
|
||||||
|
Sections to include:
|
||||||
|
- strategy armed and paused state
|
||||||
|
- executor armed, paused, and draining state
|
||||||
|
- threshold and notional settings
|
||||||
|
- recent decisions and skip reasons
|
||||||
|
- recent trade submission results
|
||||||
|
- alert state relevant to execution safety
|
||||||
|
|
||||||
|
If arm, disarm, pause, resume, or drain controls are shown here, the backend must classify them as risky or non-risky and require explicit confirmation for the risky ones.
|
||||||
|
|
||||||
|
### System page
|
||||||
|
Sections to include:
|
||||||
|
- service health summary across all runtime apps
|
||||||
|
- latest writer offsets and persistence health
|
||||||
|
- latest error surfaces by service
|
||||||
|
- current active alerts and recent transitions
|
||||||
|
- pod or deployment readiness and restart counts if cluster state is included
|
||||||
|
- any explicitly computed stale-state or degraded-state flags
|
||||||
|
|
||||||
|
This page should replace routine namespace health spot-checks for day-to-day operation, not full cluster administration.
|
||||||
|
|
||||||
|
## Control proxy plan
|
||||||
|
The backend should expose a small allowlisted control surface such as:
|
||||||
|
- `POST /api/control/:service/:action`
|
||||||
|
|
||||||
|
The backend should also expose WebSocket event shapes such as:
|
||||||
|
- `status_bar.updated`
|
||||||
|
- `quotes.recent`
|
||||||
|
- `alerts.updated`
|
||||||
|
- `inventory.updated`
|
||||||
|
- `trades.successful.appended`
|
||||||
|
|
||||||
|
The exact event names may differ, but they should be stable and explicit.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- only known service/action pairs are allowed
|
||||||
|
- the backend maps them to existing internal control routes
|
||||||
|
- the response must include the updated live state when practical
|
||||||
|
- risky actions must require an explicit confirmation payload
|
||||||
|
- unknown or blocked actions must fail loudly
|
||||||
|
|
||||||
|
Safe controls to prioritize first:
|
||||||
|
- refresh actions
|
||||||
|
- pause or resume on non-fund-moving services
|
||||||
|
- funding observer pause or resume
|
||||||
|
- withdrawals freeze or unfreeze
|
||||||
|
- withdrawal estimate
|
||||||
|
|
||||||
|
Risky controls that may be shown but not exercised without explicit approval:
|
||||||
|
- strategy arm or disarm
|
||||||
|
- executor arm, disarm, or drain
|
||||||
|
- any control that can move funds
|
||||||
|
|
||||||
|
## Service changes expected in this turn
|
||||||
|
The dashboard should reuse existing service truth where possible, but a few additive changes are acceptable if they remove ambiguity:
|
||||||
|
|
||||||
|
### `history-writer`
|
||||||
|
May need extra read helpers or SQL query functions, but should remain the owner of portfolio-metric durability.
|
||||||
|
|
||||||
|
### `liquidity-manager`
|
||||||
|
May need clearer deposit-handle timestamps, withdrawal-freeze state detail, or withdrawal estimate response shaping for dashboard consumption.
|
||||||
|
|
||||||
|
### `strategy-engine` and `trade-executor`
|
||||||
|
May need cleaner state fields or explicit risk metadata for dashboard control rendering.
|
||||||
|
|
||||||
|
### `ops-sentinel`
|
||||||
|
Should remain the owner of current alert state; the dashboard should not re-implement alert logic in the browser.
|
||||||
|
|
||||||
|
### `operator-dashboard`
|
||||||
|
This new service becomes the owner of:
|
||||||
|
- browser authentication context
|
||||||
|
- dashboard REST contracts
|
||||||
|
- dashboard WebSocket contracts
|
||||||
|
- live quote ring buffer and other in-memory dashboard views fed from Kafka
|
||||||
|
- durable pagination queries for trade history and other operator lists
|
||||||
|
|
||||||
|
If a field is already available from an existing `/state`, do not duplicate it elsewhere just for convenience.
|
||||||
|
|
||||||
|
## Query and presentation requirements
|
||||||
|
The dashboard backend should present stable API responses that already answer operator questions such as:
|
||||||
|
- what do we hold right now?
|
||||||
|
- what is it worth in EUR right now?
|
||||||
|
- are we making money versus the deposit baseline?
|
||||||
|
- are we beating or lagging simple hold of the original deposits?
|
||||||
|
- how much of the current result appears to come from market move versus trading?
|
||||||
|
- what deposits are credited versus merely observed?
|
||||||
|
- what alerts are active right now and why?
|
||||||
|
- are strategy and executor armed?
|
||||||
|
- what happened in the most recent trades?
|
||||||
|
- what are the 10 most recent incoming quotes right now?
|
||||||
|
- what are all successful trades so far, across pages?
|
||||||
|
- which service is stale, unhealthy, or paused?
|
||||||
|
|
||||||
|
Avoid pushing interpretation complexity into the frontend when the backend can answer it once.
|
||||||
|
|
||||||
|
## Validation plan
|
||||||
|
- Deploy the dashboard service to k3s.
|
||||||
|
- Verify that the default page loads and shows live balances matching `inventory-sync` and PostgreSQL.
|
||||||
|
- Verify that the total EUR valuation matches the current reference price and current spendable inventory.
|
||||||
|
- Verify that the profitability summary matches a manual deposit-baseline and simple-hold calculation from durable history, including any caveat text where data is incomplete.
|
||||||
|
- Verify that the browser receives live updates over WebSockets with no polling timers or polling API loops.
|
||||||
|
- Verify that recent incoming quotes update live in the dashboard and remain capped to the 10 most recent items.
|
||||||
|
- Verify that the successful-trades list paginates through all successful trades from PostgreSQL.
|
||||||
|
- Verify that funding visibility shown on the dashboard matches `liquidity-manager` and `inventory-sync` current state.
|
||||||
|
- Safely induce a stale-price or stale-inventory condition and confirm the status bar and relevant page reflect it without manual SQL.
|
||||||
|
- Execute at least one safe control action through the dashboard backend and verify the underlying service state changes.
|
||||||
|
- Verify that a dead or unreachable upstream service degrades only its section rather than blanking the entire dashboard.
|
||||||
|
- Confirm that risky controls are visibly confirmation-gated and do not fire by accident.
|
||||||
|
- Verify that auth context exists on both REST and WebSocket paths even when the auth provider is permissive.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
Required automated coverage:
|
||||||
|
- REST auth middleware and WebSocket auth gating
|
||||||
|
- backend aggregation of PostgreSQL rows plus live service state into dashboard responses
|
||||||
|
- valuation logic for nominal and EUR-equivalent balances
|
||||||
|
- deposit-baseline, simple-hold, and attribution summary calculations, including caveat behavior when some inputs are incomplete
|
||||||
|
- active-alert and freshness normalization
|
||||||
|
- Kafka-to-WebSocket fan-out for recent quotes and other live dashboard updates
|
||||||
|
- recent-quote ring buffer behavior
|
||||||
|
- successful-trade pagination queries
|
||||||
|
- control routing allowlist and rejection behavior for unsupported actions
|
||||||
|
- confirmation gating for risky actions
|
||||||
|
|
||||||
|
If a dashboard rendering layer is non-trivial, add at least smoke coverage for the key pages or payload contracts that feed them.
|
||||||
|
|
||||||
|
## Kubernetes and runtime work
|
||||||
|
Expected deployment additions:
|
||||||
|
- `operator-dashboard` deployment and service
|
||||||
|
- config wiring for internal service URLs
|
||||||
|
- config wiring for Kafka access and dashboard consumer group
|
||||||
|
- optional RBAC if the dashboard reads Kubernetes readiness state
|
||||||
|
|
||||||
|
Keep RBAC narrow and read-only. No new privileged operator pod should be introduced just to make the dashboard convenient.
|
||||||
|
|
||||||
|
## Out of scope on purpose
|
||||||
|
- No full quote analytics workbench page beyond the narrow recent-quotes and successful-trades views
|
||||||
|
- No benchmark or hindsight analytics page
|
||||||
|
- No strategy redesign
|
||||||
|
- No browser-facing auth or multi-user access model
|
||||||
|
- No public ingress hardening work
|
||||||
|
- No one-click live withdrawal submit path without explicit approval
|
||||||
|
|
||||||
|
## Still fake at turn open
|
||||||
|
- There is no single operator dashboard service yet.
|
||||||
|
- Operators still depend on manual service HTTP calls and SQL for routine checks.
|
||||||
|
- No browser-safe backend aggregation layer exists yet.
|
||||||
|
- No backend auth layer exists yet for operator UI traffic.
|
||||||
|
- No dashboard WebSocket path exists yet.
|
||||||
|
- Cluster-level health is still surfaced through scripts and `kubectl`, not an in-product operator page.
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
# Implementation Proof: operator dashboard foundation, funds desk, and operator controls
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-04
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The next turn is complete only when operators can answer from one real dashboard:
|
||||||
|
- are we making money
|
||||||
|
- are we trading
|
||||||
|
- can I safely control the system
|
||||||
|
- what exactly is broken if the answer is no
|
||||||
|
|
||||||
|
That answer must come from the live BTC/EURe system itself rather than from stitched-together `curl`, `kubectl`, and SQL by hand.
|
||||||
|
|
||||||
|
This is an operator-surface turn built on top of the already-proven data and execution loop. It should make the live system easier to supervise without loosening spendable truth or silently widening the trade hot path.
|
||||||
|
|
||||||
|
## Hypothesis
|
||||||
|
`unrip` becomes materially easier and safer to run once it has a dedicated dashboard service that:
|
||||||
|
|
||||||
|
1. reads PostgreSQL for durable history and current portfolio metrics
|
||||||
|
2. reads service `/state` and `/healthz` for live freshness and controls
|
||||||
|
3. shows operators the current EUR portfolio value, current profit view, current funds position, current alerts, and current control state in one place
|
||||||
|
4. exposes the routine non-destructive control actions that already exist
|
||||||
|
5. makes stale or unhealthy state obvious without requiring log-diving
|
||||||
|
|
||||||
|
If operators still have to reconstruct balances, alerts, arming state, and recent activity from multiple terminals, the system remains too operationally expensive for routine funded use.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- [I010] Operator dashboard foundation: dedicated dashboard pod plus backend API that reads PostgreSQL and service `/state`, with a global live status bar, arm/disarm controls, stale-state display, and `Funds` as the default home page.
|
||||||
|
- [I011] Funds desk: current balances, EUR-equivalent value, deposit-handle reveal with retrieval timestamps, emergency stop plus withdraw-all control path, deposit/withdraw history, trade-driven asset-change history, and flexible withdrawal forms by asset and EUR equivalent.
|
||||||
|
- [I014] Strategy and system operator pages: active strategy settings, skip reasons, alert state, service health, topic lag, persistence health, and cluster-operator stats.
|
||||||
|
|
||||||
|
## Turn-shaping rule
|
||||||
|
This turn is about truthful operator surfaces, not new trading logic.
|
||||||
|
|
||||||
|
- The browser must read one dashboard backend, not Kafka, not PostgreSQL directly, and not a fan-out of service control APIs.
|
||||||
|
- The dashboard backend may read PostgreSQL, service HTTP state, Kafka, and narrow read-only cluster metadata.
|
||||||
|
- If the dashboard shows a control, that control must either be real and wired or clearly absent. No fake buttons.
|
||||||
|
- Frontend polling is prohibited. Live dashboard updates must use WebSockets.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No new strategy logic, dynamic fee logic, or benchmark-aware thresholding.
|
||||||
|
- No full quote analytics workbench or hindsight trade analytics page beyond the narrow recent-quotes and successful-trades views named in this turn.
|
||||||
|
- No browser-direct Kafka, Redpanda, or PostgreSQL access.
|
||||||
|
- No public internet ingress, auth stack, or multi-user permission system in this turn.
|
||||||
|
- No live-funds-moving withdrawal submit path without explicit user approval during implementation.
|
||||||
|
- No dashboard widgets backed only by invented or hand-maintained data.
|
||||||
|
- No polling-based live refresh loops.
|
||||||
|
|
||||||
|
## Required runtime behavior
|
||||||
|
|
||||||
|
### Dashboard topology
|
||||||
|
- A dedicated `operator-dashboard` service must exist in cluster runtime.
|
||||||
|
- The dashboard frontend must be served by the dashboard service itself or by a tightly coupled companion in the same deployment shape.
|
||||||
|
- The dashboard backend must be the only browser-facing integration point.
|
||||||
|
- The browser must use one authenticated WebSocket connection to the dashboard backend for live updates.
|
||||||
|
|
||||||
|
### Authentication skeleton
|
||||||
|
The dashboard backend must have authentication and authorization hooks in place from the first turn even if the initial implementation is permissive.
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
- REST API routes must pass through backend auth middleware.
|
||||||
|
- WebSocket connection establishment must also pass through backend auth checks.
|
||||||
|
- The current implementation may resolve to an always-true local operator identity, but the code path must already return a structured auth context rather than bypass auth entirely.
|
||||||
|
- The frontend must learn auth state from the backend, not hard-code a local assumption.
|
||||||
|
|
||||||
|
### Profitability-first operator view
|
||||||
|
The dashboard must revolve around the question `are we making money`.
|
||||||
|
|
||||||
|
At minimum the default view must make it obvious:
|
||||||
|
- current total portfolio value in EUR terms
|
||||||
|
- profit or loss versus the deposit-time baseline
|
||||||
|
- profit or loss versus simply holding the original deposits
|
||||||
|
- a compact attribution split between market move and trading contribution using the truthful data available in this turn
|
||||||
|
- whether trading is currently active, including recent trade count and last successful trade time
|
||||||
|
|
||||||
|
If some part of the answer is still provisional because the system does not yet track every fee or per-trade realized settlement delta, the dashboard must say that explicitly rather than implying complete precision.
|
||||||
|
|
||||||
|
### Global status bar
|
||||||
|
The dashboard must show a stable top-level status strip with at least:
|
||||||
|
- active pair
|
||||||
|
- latest reference price and freshness
|
||||||
|
- latest inventory snapshot and freshness
|
||||||
|
- active alert count and highest visible severity
|
||||||
|
- strategy armed state
|
||||||
|
- executor armed state
|
||||||
|
- current total portfolio value in EUR terms
|
||||||
|
|
||||||
|
If one of these inputs is stale or unavailable, the bar must show that explicitly rather than quietly rendering old values.
|
||||||
|
The status bar should update from the backend WebSocket feed rather than browser polling.
|
||||||
|
|
||||||
|
### Funds home page
|
||||||
|
`Funds` must be the default landing page.
|
||||||
|
|
||||||
|
Its top section must answer `are we making money` before it answers lower-level operator questions.
|
||||||
|
|
||||||
|
It must show at least:
|
||||||
|
- current total portfolio value in EUR terms
|
||||||
|
- profit or loss versus the deposit-time baseline
|
||||||
|
- profit or loss versus simple hold of the original deposits
|
||||||
|
- current spendable balances by asset
|
||||||
|
- current EUR-equivalent value by asset and in total
|
||||||
|
- latest credited deposit inventory
|
||||||
|
- pre-credit funding visibility and current funding handles
|
||||||
|
- recent deposit and withdrawal records
|
||||||
|
- recent trade-driven asset changes
|
||||||
|
- the timestamps that tell the operator how fresh each section is
|
||||||
|
|
||||||
|
The dashboard may show funding observations, but it must not present pre-credit observations as spendable.
|
||||||
|
If the profit view excludes some fee classes or cannot yet provide per-trade realized net profit, that limitation must be visible in the page copy.
|
||||||
|
|
||||||
|
### Quote and trade activity
|
||||||
|
The dashboard must include a narrow operator-facing activity surface that is intentionally smaller than the future analytics workbench.
|
||||||
|
|
||||||
|
At minimum it must show:
|
||||||
|
- the 10 most recent incoming quotes
|
||||||
|
- live updates for that recent-quote list through the backend WebSocket feed
|
||||||
|
- a list of all successful trades with pagination
|
||||||
|
|
||||||
|
The recent-quote list may be minimal as long as it is real, current, and useful. For example:
|
||||||
|
- quote time
|
||||||
|
- pair or asset direction
|
||||||
|
- request kind
|
||||||
|
- input and output amounts when available
|
||||||
|
|
||||||
|
The successful-trades list must be durable-history-backed rather than in-memory-only.
|
||||||
|
|
||||||
|
### Strategy and operator surfaces
|
||||||
|
The dashboard must show:
|
||||||
|
- current strategy threshold and notional settings
|
||||||
|
- recent decisions and skip reasons
|
||||||
|
- current alert state and recent alert transitions
|
||||||
|
- service-by-service health and pause state
|
||||||
|
- writer offsets, persistence health, and relevant freshness timestamps
|
||||||
|
- read-only cluster/operator facts needed to understand whether the app pods are healthy
|
||||||
|
|
||||||
|
### Controls
|
||||||
|
The dashboard must expose routine controls through the backend, not by requiring the operator to hand-craft HTTP calls.
|
||||||
|
|
||||||
|
At minimum this turn must support some real control actions end to end, such as:
|
||||||
|
- service refresh where available
|
||||||
|
- pause or resume on safe services
|
||||||
|
- funding observer pause or resume
|
||||||
|
- withdrawals freeze or unfreeze
|
||||||
|
- withdrawal estimate requests
|
||||||
|
|
||||||
|
If arm/disarm or drain controls are shown for strategy or executor, they must be explicit about their risk class and confirmation requirements.
|
||||||
|
|
||||||
|
### Risk boundary for treasury actions
|
||||||
|
This turn may prepare operator-visible withdrawal estimation and confirmation surfaces, but it must not silently add a one-click live-funds path.
|
||||||
|
|
||||||
|
Any control that can move funds or materially alter live execution risk must remain:
|
||||||
|
- explicitly identified as risky
|
||||||
|
- confirmation-gated
|
||||||
|
- subject to separate user approval before live use
|
||||||
|
|
||||||
|
## Required durable and live sources
|
||||||
|
The dashboard backend must use:
|
||||||
|
- PostgreSQL for durable history and portfolio metrics
|
||||||
|
- service `/state` or `/healthz` for live state and freshness
|
||||||
|
- Kafka on the backend side for live event fan-out to WebSocket clients where appropriate
|
||||||
|
- optionally a narrow read-only cluster source for pod or deployment readiness
|
||||||
|
|
||||||
|
Kafka remains the event backbone underneath the system, but the browser must not depend on Kafka directly.
|
||||||
|
The backend may consume Kafka specifically to avoid polling and to drive live quote, price, inventory, alert, and trade updates into the dashboard WebSocket stream.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- The live cluster still runs the previously proven BTC/EURe loop with unchanged spendable truth.
|
||||||
|
- A real dashboard service is deployed and reachable by port-forward or equivalent operator access.
|
||||||
|
- `Funds` is the default page and makes the current profit view explicit, including current EUR value, deposit-baseline comparison, hold comparison, and current balances from real sources.
|
||||||
|
- Strategy or system views show live operator state from real sources, including alerts and service health.
|
||||||
|
- The dashboard exposes a recent-quotes section that updates live over WebSockets with no polling.
|
||||||
|
- The dashboard exposes a durable successful-trades list with pagination.
|
||||||
|
- Backend auth is present for both REST and WebSocket paths, even if the initial auth provider is permissive.
|
||||||
|
- At least one safe control action is executed through the dashboard backend and reflected in the underlying service state.
|
||||||
|
- A stale or paused condition induced safely is visible in the dashboard without manual SQL or log parsing.
|
||||||
|
- The dashboard clearly distinguishes durable history from live current state.
|
||||||
|
- Tests cover backend aggregation and control routing logic.
|
||||||
|
- Remaining risky or intentionally deferred controls are named plainly.
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- The dashboard is only a static shell over mocked or copied data.
|
||||||
|
- The browser has to call five different service APIs directly to work.
|
||||||
|
- Balances or alerts shown in the dashboard disagree with PostgreSQL or live service state.
|
||||||
|
- Freshness is hidden, forcing the operator to guess whether a value is current.
|
||||||
|
- Live updates depend on frontend polling instead of WebSockets.
|
||||||
|
- The dashboard has no backend auth path and would need auth bolted on later.
|
||||||
|
- The turn adds risky treasury controls without explicit approval and confirmation design.
|
||||||
|
- Profitability remains buried in generic metric cards or requires the operator to reconstruct the answer manually.
|
||||||
|
- The dashboard ships without enough runtime truth to replace routine manual `curl` and SQL work.
|
||||||
|
|
||||||
|
## Current real
|
||||||
|
- The first funded BTC/EURe live loop is already proven and archived.
|
||||||
|
- Pre-credit BTC funding visibility, durable alert records, and rollout-safe armed-state persistence are now live.
|
||||||
|
- PostgreSQL contains the durable trade, inventory, funding, alert, and portfolio-metric history needed for a dashboard backend.
|
||||||
|
- Operators still depend on manual terminal workflows to inspect that truth.
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
# Implementation Turn: CoW Protocol intent-based venue integration
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-07
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Integrate CoW Protocol as a second execution venue in a way that makes the architecture more real, not more decorative.
|
||||||
|
|
||||||
|
The system must be able to request a CoW quote, build the exact order amounts CoW expects, sign the order with the correct chain-specific domain, submit it to the CoW orderbook, durably track later order states, and surface that truth to operators without inventing a parallel data model or widening live-funds risk by accident.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- [I019] CoW Protocol intent-based venue integration: quote, sign, submit, and durably track CoW orders through the shared Kafka and PostgreSQL truth pipeline without widening live-funds risk beyond explicit approval.
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
- Keep Kafka as the backbone and PostgreSQL as the durable store.
|
||||||
|
- Treat CoW as a venue adapter behind the existing architecture, not as a separate mini-application ecosystem.
|
||||||
|
- Do not silently change the already-live NEAR spendable inventory truth.
|
||||||
|
- Prefer one real chain and one real pair over scaffolding for many chains or advanced CoW features.
|
||||||
|
- Do not build solver, autopilot, conditional-order, hook, or CoW AMM support in this turn.
|
||||||
|
|
||||||
|
## Assumptions to lock before coding
|
||||||
|
- Initial chain: Gnosis Chain.
|
||||||
|
- Initial signing path: EIP-712 with an EOA signer. If the user later wants Safe or another smart-contract wallet, that is a separate signing-path expansion.
|
||||||
|
- Initial pair: `EURe/WBTC` on Gnosis.
|
||||||
|
- Initial token addresses:
|
||||||
|
- `EURe`: `0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430`
|
||||||
|
- `WBTC`: `0x8e5bBbb09Ed1ebdE8674Cda39A0c169401db4252`
|
||||||
|
- Planning validation already confirmed that CoW's live Gnosis quote API returns a valid `EURe -> WBTC` quote for a sample sell order.
|
||||||
|
- Full passed-proof validation requires an explicitly approved isolated CoW trader identity with funded and approved ERC-20 inventory. Without it, coding can proceed, but the turn cannot claim a passed live-trading proof.
|
||||||
|
|
||||||
|
## Minimal service changes for this turn
|
||||||
|
- `trade-executor` must be refactored from a NEAR-only executor into an executor core with venue adapters.
|
||||||
|
- `history-writer` and the PostgreSQL layer must learn the new CoW raw quote and order-lifecycle records.
|
||||||
|
- `operator-dashboard` must expose CoW venue state, open orders, and last outcomes from the backend.
|
||||||
|
- `strategy-engine` should not be turned into a general multi-venue router in this turn. If a command-source tweak is needed to target CoW, keep it narrow and explicit.
|
||||||
|
- `inventory-sync` and `liquidity-manager` must not absorb CoW treasury or approval flows in this turn. At most they may expose read-only isolated CoW wallet facts if needed for operator clarity.
|
||||||
|
|
||||||
|
## Why CoW changes the execution model
|
||||||
|
The current execution path is close to:
|
||||||
|
- decision
|
||||||
|
- command
|
||||||
|
- immediate result
|
||||||
|
|
||||||
|
CoW requires:
|
||||||
|
- quote request
|
||||||
|
- fee-aware order construction
|
||||||
|
- signature generation
|
||||||
|
- order submission returning an order UID
|
||||||
|
- later monitoring until the order becomes `Filled`, `Partially-filled`, `Expired`, or `Cancelled`
|
||||||
|
|
||||||
|
The first coding objective is therefore not a new screen or a new script. It is upgrading the execution model so a stateful orderbook venue fits the shared truth pipeline.
|
||||||
|
|
||||||
|
## Architecture changes
|
||||||
|
|
||||||
|
### 1. Add a CoW venue package
|
||||||
|
Create a new venue package under `src/venues/cow-protocol/` with focused modules:
|
||||||
|
- `chains.mjs`
|
||||||
|
- chain metadata such as chain ID, orderbook base URL, settlement contract, and human label
|
||||||
|
- `client.mjs`
|
||||||
|
- thin REST client for `POST /api/v1/quote`, `POST /api/v1/orders`, `GET /api/v1/orders/{uid}`, `GET /api/v1/trades`, and `DELETE /api/v1/orders/{uid}`
|
||||||
|
- `amounts.mjs`
|
||||||
|
- quote-to-order amount construction using the documented CoW fee semantics
|
||||||
|
- explicitly encode the difference between sell and buy order handling
|
||||||
|
- `signing.mjs`
|
||||||
|
- CoW order digest creation and EIP-712 signing
|
||||||
|
- use the documented `Gnosis Protocol` `v2` domain shape and chain-specific settlement contract
|
||||||
|
- `normalization.mjs`
|
||||||
|
- map CoW quote, order, trade, and cancellation responses into repo-owned event shapes
|
||||||
|
- `errors.mjs`
|
||||||
|
- normalize HTTP, validation, and remote API failures into durable reason codes
|
||||||
|
|
||||||
|
Prefer the official CoW SDK or contracts helpers for digest and order-signing math where that reduces risk, but keep the repo-owned normalization and persistence boundaries explicit.
|
||||||
|
|
||||||
|
### 2. Extend config for CoW
|
||||||
|
Add explicit config entries in `src/lib/config.mjs` and manifests for:
|
||||||
|
- CoW enabled flag
|
||||||
|
- CoW chain name and chain ID
|
||||||
|
- CoW orderbook base URL
|
||||||
|
- CoW settlement contract address for the selected chain
|
||||||
|
- CoW trader address
|
||||||
|
- CoW signer private key or signing-provider hook
|
||||||
|
- CoW preferred sell and buy token addresses and decimals for the approved pair
|
||||||
|
- CoW order validity default
|
||||||
|
- CoW monitoring interval for open orders
|
||||||
|
- CoW API key slot if partner-authenticated access is later used
|
||||||
|
|
||||||
|
Do not store secrets in repo. Only add config plumbing.
|
||||||
|
|
||||||
|
### 3. Refactor `trade-executor` into executor core plus adapters
|
||||||
|
The executor should become:
|
||||||
|
- a venue-agnostic command consumer
|
||||||
|
- a shared idempotency and state owner
|
||||||
|
- a venue adapter dispatcher
|
||||||
|
|
||||||
|
Adapter plan:
|
||||||
|
- extract current NEAR execution code into a `near-intents` adapter without changing behavior
|
||||||
|
- add a `cow-protocol` adapter implementing:
|
||||||
|
- command validation
|
||||||
|
- balance and allowance preflight checks
|
||||||
|
- CoW quote request
|
||||||
|
- amount construction from the quote
|
||||||
|
- order digest generation
|
||||||
|
- signature creation
|
||||||
|
- order submission
|
||||||
|
- submission-time event emission
|
||||||
|
- open-order monitoring and later state updates
|
||||||
|
- cancellation support if enabled
|
||||||
|
|
||||||
|
The executor core should no longer assume that an adapter returns only one terminal result.
|
||||||
|
|
||||||
|
### 4. Introduce canonical order-lifecycle events
|
||||||
|
The current `exec.trade_result` topic is not enough for CoW.
|
||||||
|
|
||||||
|
Add one new canonical topic for non-terminal and terminal order-state updates:
|
||||||
|
- `exec.order_update`
|
||||||
|
|
||||||
|
This topic should carry at least:
|
||||||
|
- `command_id`
|
||||||
|
- `venue`
|
||||||
|
- `venue_order_id`
|
||||||
|
- `status`
|
||||||
|
- `observed_at`
|
||||||
|
- `filled_sell_amount`
|
||||||
|
- `filled_buy_amount`
|
||||||
|
- `settlement_tx_hash`
|
||||||
|
- `trade_ids`
|
||||||
|
- `reason_code`
|
||||||
|
- `reason_text`
|
||||||
|
- `is_terminal`
|
||||||
|
|
||||||
|
Keep `exec.trade_result` as the terminal summary topic for compatibility, but make it derived from the richer order-update model where needed.
|
||||||
|
|
||||||
|
Near Intents may continue to emit one immediate terminal update through this new topic as a trivial case. CoW will use multiple updates over time.
|
||||||
|
|
||||||
|
### 5. Persist CoW raw and normalized truth in PostgreSQL
|
||||||
|
Add new durable tables and query helpers in `src/lib/postgres.mjs` and `src/core/history-records.mjs`.
|
||||||
|
|
||||||
|
Required new durable stores:
|
||||||
|
- `raw_cow_quotes`
|
||||||
|
- quote request inputs and raw response payload
|
||||||
|
- `trade_order_events`
|
||||||
|
- canonical order lifecycle records across venues
|
||||||
|
- `cow_orderbook_orders`
|
||||||
|
- latest known CoW order snapshot keyed by order UID
|
||||||
|
- `cow_orderbook_trades`
|
||||||
|
- trade records returned by CoW trade history when available
|
||||||
|
|
||||||
|
Required linkage:
|
||||||
|
- `command_id` must join submission, order UID, later state updates, and terminal result
|
||||||
|
- order UID must join order snapshot and trade records
|
||||||
|
- every later state update must be replayable from PostgreSQL without requiring in-memory executor state
|
||||||
|
|
||||||
|
### 6. Upgrade history writing and replay
|
||||||
|
Extend `history-writer` routing so the new CoW topics and records are first-class:
|
||||||
|
- `raw.cow.quote` -> `raw_cow_quotes`
|
||||||
|
- `exec.order_update` -> `trade_order_events`
|
||||||
|
- optional CoW-specific normalized topics if needed for operator surfaces
|
||||||
|
|
||||||
|
Replay requirement:
|
||||||
|
- on restart, the CoW monitor must be able to recover open orders from durable state and resume monitoring
|
||||||
|
- local state files may exist for convenience, but PostgreSQL must remain the durable source of truth
|
||||||
|
|
||||||
|
### 7. Add a CoW open-order monitor
|
||||||
|
CoW order submission is not enough. The repo needs a venue monitor loop.
|
||||||
|
|
||||||
|
Implementation shape:
|
||||||
|
- `trade-executor` maintains a small set of currently open CoW orders
|
||||||
|
- on startup, it reloads open CoW orders from PostgreSQL
|
||||||
|
- on each monitor tick, it fetches latest order state from `GET /api/v1/orders/{uid}`
|
||||||
|
- if the order is partially or fully filled, also fetch trade details from `GET /api/v1/trades`
|
||||||
|
- it emits `exec.order_update` for every meaningful state transition
|
||||||
|
- it emits a terminal `exec.trade_result` when the order reaches a terminal state
|
||||||
|
|
||||||
|
Operator rules:
|
||||||
|
- open-order age and last refresh time must be visible
|
||||||
|
- monitoring failures must produce durable alerts or explicit venue-state errors
|
||||||
|
|
||||||
|
### 8. Handle CoW-specific preflight checks
|
||||||
|
Before submitting a CoW order, the adapter must validate:
|
||||||
|
- configured chain and orderbook URL are consistent
|
||||||
|
- signer address matches configured trader address
|
||||||
|
- token addresses and decimals match the selected pair config
|
||||||
|
- wallet balance is sufficient for the order
|
||||||
|
- allowance is sufficient for the settlement contract or relayer path expected by CoW
|
||||||
|
- order validity window is sane
|
||||||
|
|
||||||
|
Failures here should become structured submission rejections, not opaque thrown errors.
|
||||||
|
|
||||||
|
### 9. Keep inventory truth separate
|
||||||
|
This turn must not mutate the current shared spendable inventory model for the NEAR live loop.
|
||||||
|
|
||||||
|
Implementation rules:
|
||||||
|
- CoW balances are venue-local facts, not part of current global spendable truth by default
|
||||||
|
- do not merge CoW balances into the funded BTC/EURe dashboard PnL cards
|
||||||
|
- if the dashboard shows CoW wallet balances, label them as isolated venue inventory
|
||||||
|
- do not treat pre-credit or observed cross-chain funds as CoW-spendable
|
||||||
|
- if a later turn wants unified cross-venue treasury truth, that is separate work
|
||||||
|
|
||||||
|
### 10. Command-source strategy for this turn
|
||||||
|
Do not expand `strategy-engine` into a venue arbiter.
|
||||||
|
|
||||||
|
Use one of these narrow command sources:
|
||||||
|
- a controlled manual command injection path for validation
|
||||||
|
- a small venue-targeted control route on the executor
|
||||||
|
- a minimal strategy extension that emits `venue = cow-protocol` only for the approved validation pair
|
||||||
|
|
||||||
|
Whichever path is chosen, keep it explicit and reversible. The point of this turn is the venue plumbing and lifecycle truth, not smart order routing.
|
||||||
|
|
||||||
|
### 11. Dashboard and operator surfaces
|
||||||
|
Use the existing authenticated dashboard backend and WebSocket surface.
|
||||||
|
|
||||||
|
Add operator visibility for:
|
||||||
|
- CoW venue configuration summary
|
||||||
|
- current CoW trader address and chain
|
||||||
|
- last successful quote time
|
||||||
|
- last submitted order UID
|
||||||
|
- open order count
|
||||||
|
- per-order status with timestamps
|
||||||
|
- last terminal outcome and reason text
|
||||||
|
- last settlement transaction hash when available
|
||||||
|
- monitoring freshness and last error
|
||||||
|
|
||||||
|
Safe controls that may be real in this turn:
|
||||||
|
- refresh CoW venue state
|
||||||
|
- pause or resume CoW monitoring
|
||||||
|
- request cancellation for an open CoW order
|
||||||
|
|
||||||
|
Controls that must stay absent without explicit approval:
|
||||||
|
- fund CoW wallet
|
||||||
|
- approve token spend
|
||||||
|
- bridge assets to CoW chain
|
||||||
|
- any generic "trade now" button in the browser
|
||||||
|
|
||||||
|
### 12. Alerting and health
|
||||||
|
Extend alerting only where needed:
|
||||||
|
- stale CoW order monitoring
|
||||||
|
- repeated quote failure
|
||||||
|
- repeated submission failure
|
||||||
|
- open order older than expected validity window
|
||||||
|
- cancellation requested but order still open after grace period
|
||||||
|
|
||||||
|
Do not build a new alert subsystem. Feed venue issues into the existing alert path.
|
||||||
|
|
||||||
|
## Concrete implementation order
|
||||||
|
|
||||||
|
### Phase 1. Lock the venue contract
|
||||||
|
- confirm selected chain and pair
|
||||||
|
- confirm CoW quoteability for the pair at the intended notional
|
||||||
|
- confirm settlement contract and orderbook base URL
|
||||||
|
- confirm signing scheme and wallet model
|
||||||
|
|
||||||
|
### Phase 2. Add CoW libraries and tests
|
||||||
|
- implement amount construction from quotes
|
||||||
|
- implement order digest and signature helpers
|
||||||
|
- add unit tests from documented sell and buy fee semantics
|
||||||
|
- add unit tests for domain separator selection and digest generation
|
||||||
|
|
||||||
|
### Phase 3. Refactor executor core
|
||||||
|
- extract current NEAR execution into an adapter
|
||||||
|
- add adapter dispatch by `venue`
|
||||||
|
- add canonical `exec.order_update` event handling
|
||||||
|
- keep existing NEAR behavior passing
|
||||||
|
|
||||||
|
### Phase 4. Add CoW submission path
|
||||||
|
- quote
|
||||||
|
- preflight
|
||||||
|
- sign
|
||||||
|
- submit
|
||||||
|
- persist raw quote and submission acknowledgement
|
||||||
|
- emit order update and terminal failure events as needed
|
||||||
|
|
||||||
|
### Phase 5. Add durable monitoring
|
||||||
|
- persist open CoW orders
|
||||||
|
- restore them on restart
|
||||||
|
- poll order and trade endpoints
|
||||||
|
- emit durable state transitions and terminal results
|
||||||
|
|
||||||
|
### Phase 6. Add operator visibility
|
||||||
|
- extend dashboard backend aggregation
|
||||||
|
- extend UI pages only enough to expose CoW order state truthfully
|
||||||
|
- no fake controls
|
||||||
|
|
||||||
|
### Phase 7. Run live validation
|
||||||
|
- request a real quote
|
||||||
|
- if approved wallet is available, submit one real order
|
||||||
|
- observe at least one later status change
|
||||||
|
- persist evidence in PostgreSQL
|
||||||
|
- verify dashboard visibility
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
- unit tests for quote amount transformation for sell orders
|
||||||
|
- unit tests for quote amount transformation for buy orders if buy orders are supported in this turn
|
||||||
|
- unit tests for EIP-712 domain generation by chain
|
||||||
|
- unit tests for order digest generation against known CoW examples or local fixtures
|
||||||
|
- unit tests for lifecycle reducer:
|
||||||
|
- submitted -> open
|
||||||
|
- open -> partially-filled
|
||||||
|
- open -> filled
|
||||||
|
- open -> expired
|
||||||
|
- open -> cancelled
|
||||||
|
- integration tests with a mocked CoW orderbook server for quote, order submission, order fetch, trade fetch, and cancellation
|
||||||
|
- PostgreSQL persistence tests for new tables and history routing
|
||||||
|
- regression tests proving the existing NEAR executor still emits correct results after adapter extraction
|
||||||
|
|
||||||
|
No CoW bug fix is complete without a regression test covering the relevant fee, digest, lifecycle, or persistence case.
|
||||||
|
|
||||||
|
## Validation checklist against the proof
|
||||||
|
- real quote acquired and stored
|
||||||
|
- correct amounts signed from CoW quote semantics
|
||||||
|
- correct domain and signature path used
|
||||||
|
- order UID returned and linked to command ID
|
||||||
|
- later order state observed and stored
|
||||||
|
- operator surface shows venue state and order outcome
|
||||||
|
- NEAR live path remains unchanged in spendable truth and arm safety
|
||||||
|
|
||||||
|
## Failure modes to plan for
|
||||||
|
- unsupported token or `NoLiquidity` from CoW quote
|
||||||
|
- bad token or unsupported token rejection
|
||||||
|
- wrong chain ID or settlement contract causing invalid signatures
|
||||||
|
- insufficient allowance or balance
|
||||||
|
- order accepted but never transitions before expiry
|
||||||
|
- partial fill without terminal fill in the same process lifetime
|
||||||
|
- duplicate monitoring after restart
|
||||||
|
- cancellation race where an order fills while cancellation is in flight
|
||||||
|
- orderbook API timeout or rate limiting
|
||||||
|
|
||||||
|
Each of these needs an explicit durable reason code or operator-visible error path.
|
||||||
|
|
||||||
|
## What remains deliberately out of scope after this turn
|
||||||
|
- unified cross-venue treasury truth
|
||||||
|
- fee-complete realized PnL for CoW fills
|
||||||
|
- automatic venue selection between NEAR Intents and CoW
|
||||||
|
- Safe or generic `ERC-1271` support
|
||||||
|
- advanced CoW order types
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
# Implementation Proof: CoW Protocol intent-based venue integration
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-07
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
This turn proves that `unrip` can absorb a second venue with materially different execution semantics without breaking the shared truth pipeline.
|
||||||
|
|
||||||
|
The concrete target is one real CoW Protocol trader flow for one approved chain and pair:
|
||||||
|
- request a CoW `/quote`
|
||||||
|
- construct the exact order amounts that CoW expects after fee and slippage handling
|
||||||
|
- sign the order with the correct chain-specific EIP-712 domain
|
||||||
|
- submit the signed order to the CoW orderbook
|
||||||
|
- durably track the resulting order lifecycle and any associated trade or settlement state
|
||||||
|
- expose the resulting truth to operators without hand-assembled SQL, log scraping, or one-off scripts
|
||||||
|
|
||||||
|
## Why this is a meaningful architecture test
|
||||||
|
The current proven venue path is NEAR Intents, where the hot path is close to immediate quote-response execution.
|
||||||
|
|
||||||
|
CoW is different in ways that matter:
|
||||||
|
- `unrip` acts as a trader submitting intents, not as a solver
|
||||||
|
- submission returns an order UID, not an immediate final settlement
|
||||||
|
- orders can remain `Open`, become `Partially-filled`, `Filled`, `Expired`, or `Cancelled`
|
||||||
|
- the signed order depends on chain-specific domain separation and fee semantics from the CoW orderbook quote
|
||||||
|
|
||||||
|
If the repo can integrate this flow without bypassing Kafka, PostgreSQL, and the existing operator surfaces, the architecture is likely good enough for more venue work.
|
||||||
|
|
||||||
|
## Hypothesis
|
||||||
|
`unrip` becomes a more trustworthy multi-venue system if it can integrate CoW Protocol's intent flow as a first-class execution venue while keeping the same core rules:
|
||||||
|
- Kafka remains the event backbone
|
||||||
|
- PostgreSQL remains the durable truth store
|
||||||
|
- operator visibility comes from durable and live sources, not from ad hoc inspection
|
||||||
|
- venue-specific behavior lives behind explicit adapters instead of leaking through the whole system
|
||||||
|
|
||||||
|
The turn passes only if CoW order submission and lifecycle truth become real repository-owned behavior rather than docs, SDK experiments, or local throwaway scripts.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- [I019] CoW Protocol intent-based venue integration: quote, sign, submit, and durably track CoW orders through the shared Kafka and PostgreSQL truth pipeline without widening live-funds risk beyond explicit approval.
|
||||||
|
- One approved CoW-supported chain for the first integration. Initial assumption: Gnosis Chain because the current treasury already reasons about EURe on chain `100`.
|
||||||
|
- One approved CoW-supported pair for the first integration: `EURe/WBTC` on Gnosis using `EURe` token `0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430` and `WBTC` token `0x8e5bBbb09Ed1ebdE8674Cda39A0c169401db4252`.
|
||||||
|
- One supported signing path for the first pass. Initial assumption: EIP-712 EOA signing. `ERC-1271`, `PreSign`, hooks, and conditional orders are out of scope unless explicitly pulled in during the turn.
|
||||||
|
- One truthful operator surface for CoW order state, submission failures, and open-order status, implemented through the existing dashboard backend and WebSocket model.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
- CoW orderbook access is available over the documented REST API for the selected chain.
|
||||||
|
- The implementation will use the documented CoW amount and signing semantics, including chain-specific EIP-712 domain separation against the settlement contract.
|
||||||
|
- A live CoW quote for `EURe -> WBTC` on Gnosis was verified during planning on 2026-04-07, so pair quoteability is no longer only a planning assumption.
|
||||||
|
- A dedicated CoW trader identity can be provided during implementation if the user wants a fully real posted order. Without that identity and its approved inventory, the turn can make the quote, signing, persistence, and monitoring paths real but cannot claim a passed live-trading proof.
|
||||||
|
- Existing NEAR verifier and bridge credit remain the only spendable truth for the already-live BTC/EURe loop. CoW balances are separate and must not be silently merged into the current spendable inventory model.
|
||||||
|
|
||||||
|
## Turn-shaping rules
|
||||||
|
- This is a second-venue proof, not a broad multi-venue router project.
|
||||||
|
- Integrate CoW as a trader on the intent and orderbook side only. Do not take on solver, autopilot, CoW AMM, JIT order, or watch-tower work in this turn.
|
||||||
|
- The browser must still talk only to repo-owned backend services. No browser-direct CoW API usage.
|
||||||
|
- Durable truth must distinguish at least these stages:
|
||||||
|
- quote request or response
|
||||||
|
- signed order intent
|
||||||
|
- orderbook submission acknowledgement
|
||||||
|
- later order status transitions
|
||||||
|
- terminal outcome and any trade or settlement linkage
|
||||||
|
- No live-funds-moving CoW funding, approval, or replenishment path may be added without explicit user approval during implementation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No CoW solver implementation or solver-competition participation.
|
||||||
|
- No conditional orders, TWAP, hooks, milkman, or other advanced CoW order types.
|
||||||
|
- No generalized smart order router across NEAR Intents and CoW.
|
||||||
|
- No automatic cross-chain treasury balancing between the existing NEAR inventory and a CoW wallet.
|
||||||
|
- No silent expansion of portfolio PnL to include CoW balances before a separate spendable-truth decision.
|
||||||
|
- No UI-only venue showcase without a real repository-owned quote, sign, submit, and lifecycle path underneath it.
|
||||||
|
|
||||||
|
## Required runtime behavior
|
||||||
|
|
||||||
|
### Canonical execution path
|
||||||
|
For one approved chain and pair, `unrip` must be able to:
|
||||||
|
- construct a real CoW `/quote` request from repo-owned command inputs
|
||||||
|
- capture the raw quote response durably
|
||||||
|
- transform the quote response into the exact order amounts that CoW expects for the selected order kind
|
||||||
|
- compute the correct CoW order digest using the chain-specific EIP-712 domain
|
||||||
|
- sign the order using the approved signing scheme
|
||||||
|
- submit the signed order to `POST /api/v1/orders`
|
||||||
|
- store the returned order UID durably and link it to the canonical command ID
|
||||||
|
|
||||||
|
### Lifecycle truth
|
||||||
|
After submission, the system must durably observe and expose later CoW order states from the orderbook or trade endpoints.
|
||||||
|
|
||||||
|
At minimum it must be able to represent and persist:
|
||||||
|
- `Open`
|
||||||
|
- `Partially-filled`
|
||||||
|
- `Filled`
|
||||||
|
- `Expired`
|
||||||
|
- `Cancelled`
|
||||||
|
- submission rejection or later monitoring failure with explicit reason text
|
||||||
|
|
||||||
|
It is not sufficient to log the UID and stop.
|
||||||
|
|
||||||
|
### Execution model upgrade
|
||||||
|
The system must stop assuming that every venue command produces one immediate terminal execution result.
|
||||||
|
|
||||||
|
This turn must introduce a truthful way to represent:
|
||||||
|
- submission-time acknowledgement
|
||||||
|
- non-terminal order states
|
||||||
|
- terminal order completion or cancellation
|
||||||
|
- settlement identifiers such as trade IDs or transaction hashes when available
|
||||||
|
|
||||||
|
### Inventory and risk boundary
|
||||||
|
CoW execution must use only explicitly configured CoW wallet state.
|
||||||
|
|
||||||
|
Required rules:
|
||||||
|
- current NEAR spendable inventory truth stays unchanged
|
||||||
|
- pre-credit observations still do not count as spendable
|
||||||
|
- CoW wallet balances must be treated as separate venue-local state
|
||||||
|
- if venue-local funds are not yet approved for live use, the system must say so plainly instead of implying they are active inventory
|
||||||
|
|
||||||
|
### Operator surface
|
||||||
|
Operators must be able to answer:
|
||||||
|
- is the CoW path configured for the expected chain and wallet
|
||||||
|
- what was the last CoW quote request and response
|
||||||
|
- what orders are currently open
|
||||||
|
- what was the last terminal CoW outcome
|
||||||
|
- whether the venue path is paused, healthy, or failing
|
||||||
|
- whether a cancellation was requested and what happened next
|
||||||
|
|
||||||
|
Any control shown for CoW must be real and wired, or absent.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- The finished dashboard turn is archived and the live turn files govern the CoW turn.
|
||||||
|
- A real CoW venue adapter exists in the repo and uses the documented CoW quote, signing, and orderbook APIs.
|
||||||
|
- Kafka remains the backbone for CoW quote, order, and lifecycle fan-out.
|
||||||
|
- PostgreSQL durably stores CoW raw quote data plus order lifecycle truth.
|
||||||
|
- A canonical command can produce a CoW quote request, signed order, order submission, and durable linked order UID.
|
||||||
|
- The system observes at least one later order-state transition from the CoW orderbook after submission.
|
||||||
|
- Operators can inspect current CoW state from the dashboard or equivalent repo-owned backend surface.
|
||||||
|
- Safe control actions for the CoW path exist where needed, such as refresh or cancel, or are explicitly absent.
|
||||||
|
- The current NEAR live loop remains armed-state-safe and unchanged in spendable truth.
|
||||||
|
- Tests cover CoW amount construction, signing-domain selection, lifecycle reduction, and persistence mapping.
|
||||||
|
|
||||||
|
For this turn to close with status `passed`, one real CoW order must be submitted from an explicitly approved isolated trader identity and observed through at least one later status transition. If that approval or identity is not provided during implementation, the turn may end `paused` with the remaining blocker named plainly.
|
||||||
|
|
||||||
|
## Validation evidence required
|
||||||
|
- direct evidence of a successful CoW `/quote` response for the selected chain and pair
|
||||||
|
- automated test evidence that signed amounts match CoW quote semantics for both sell-side or buy-side paths used
|
||||||
|
- direct evidence that the repo computes the correct EIP-712 digest for the selected chain and settlement contract domain
|
||||||
|
- direct evidence of order submission acknowledgement and returned order UID
|
||||||
|
- direct evidence of later order status retrieval from CoW orderbook or trades APIs
|
||||||
|
- PostgreSQL evidence that the quote, order UID, and later lifecycle state were persisted
|
||||||
|
- operator-surface evidence that the CoW state is visible without manual one-off inspection
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- CoW support exists only as SDK experimentation or scripts outside the main services.
|
||||||
|
- The system special-cases CoW by bypassing Kafka or PostgreSQL.
|
||||||
|
- The signed order ignores CoW fee semantics and signs the wrong amounts.
|
||||||
|
- The chain ID or settlement contract domain is wrong, making signatures invalid or unsafe.
|
||||||
|
- The repo stores only the initial order submission and not the later lifecycle.
|
||||||
|
- Open CoW exposure is invisible to operators.
|
||||||
|
- CoW funds are implied to be spendable without explicit wallet and approval boundaries.
|
||||||
|
- The implementation quietly widens live-funds risk by adding funding, approval, or treasury flows without user approval.
|
||||||
|
|
||||||
|
## Current real before this turn
|
||||||
|
- The BTC/EURe NEAR Intents loop is already proven and archived.
|
||||||
|
- Pre-credit BTC funding visibility, durable alerts, armed-state persistence, and a real operator dashboard are already proven and archived.
|
||||||
|
- Kafka and PostgreSQL already serve as the live and durable backbone for market, inventory, decision, and execution truth.
|
||||||
|
- The current execution model is still biased toward immediate terminal results and does not yet prove a stateful orderbook venue lifecycle.
|
||||||
|
|
||||||
|
## Deliberately not built by this proof
|
||||||
|
- CoW solver participation
|
||||||
|
- broad multi-venue routing logic
|
||||||
|
- treasury funding and approval UX for CoW wallets
|
||||||
|
- fee-complete realized PnL for CoW fills
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Implementation Turn: NEAR Intents request creation and EURe-to-BTC taker flow
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-12
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Build a repo-owned path to create our own NEAR Intents EURe-to-BTC swap request from credited internal inventory, submit it only behind explicit gates, and show truthful request-to-settlement evidence in the dashboard.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- [I017] Repo-owned NEAR Intents request creation and EURe-to-BTC taker flow: create, sign, submit, and settle our own swap requests from credited inventory with truthful dashboard evidence. tags=intents,taker,execution,inventory,settlement
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
- Treat request creation as a new taker lifecycle, not as an incoming quote-response decision.
|
||||||
|
- Preflight must be safe and side-effect-free.
|
||||||
|
- Live submission must be explicit, idempotent, and durably recorded before the relay call.
|
||||||
|
- Use only spendable credited EURe.
|
||||||
|
- Keep all ids copyable: request id, idempotency key, intent hash or relay request id, submission id.
|
||||||
|
- Submitted or relay-accepted does not mean completed.
|
||||||
|
- Completed requires inventory delta attribution.
|
||||||
|
|
||||||
|
## Problem statement
|
||||||
|
The current system watches the NEAR Intents market and responds to other users' quotes. That is not enough to intentionally convert our EURe inventory to BTC. The operator wants a repo-owned way to create our own request on the intents market and eventually move EURe into BTC, with the same truth standard as the incoming quote lifecycle.
|
||||||
|
|
||||||
|
The existing code can sign verifier token_diff payloads for quote responses and can submit quote_response over the solver relay websocket. It does not yet know how to publish a taker request, quote request, or swap intent that asks the market to fill our EURe-to-BTC order.
|
||||||
|
|
||||||
|
## Backend changes
|
||||||
|
|
||||||
|
### 1. Verify request protocol
|
||||||
|
- Identify the supported NEAR Intents method for creating our own request using primary code/docs or live non-mutating behavior.
|
||||||
|
- Determine whether the method uses the existing solver relay websocket, a JSON-RPC HTTP endpoint, or a verifier contract call.
|
||||||
|
- Determine exact payload fields for EURe-to-BTC exact-in requests.
|
||||||
|
- Verify signed intent semantics: outgoing EURe, incoming BTC, deadline, nonce, verifier contract, signer.
|
||||||
|
- Record a hard blocker if the protocol cannot be verified.
|
||||||
|
|
||||||
|
### 2. Add request model and schemas
|
||||||
|
- Add a canonical repo-owned request lifecycle model.
|
||||||
|
- Add event schemas for at least intent_request_preflight, intent_request_submit_command, intent_request_submission_result, and intent_request_outcome.
|
||||||
|
- Persist request id, idempotency key, source/destination assets, requested spend, min receive, reference price, slippage, signer account, verifier contract, deadline, nonce, and created/submitted timestamps.
|
||||||
|
- Keep this model separate from incoming swap_demand, trade_decision, execute_trade, and trade_result events.
|
||||||
|
|
||||||
|
### 3. Build preflight service logic
|
||||||
|
- Load latest spendable EURe inventory from current inventory state or durable snapshot.
|
||||||
|
- Compute request amount from all spendable EURe minus an explicit reserve if configured, while allowing a smaller operator-specified amount for testing.
|
||||||
|
- Compute expected BTC from reference price.
|
||||||
|
- Compute minimum BTC receive from explicit slippage basis points.
|
||||||
|
- Reject if reference price is stale, inventory is stale, amount is zero, amount exceeds spendable EURe, or signer is not registered.
|
||||||
|
- Return a draft request object without signing or submitting.
|
||||||
|
|
||||||
|
### 4. Build signing/envelope helpers
|
||||||
|
- Add a helper for taker request signing/envelope construction once the verified protocol is known.
|
||||||
|
- Reuse buildIntentNonce only if nonce semantics match the request protocol.
|
||||||
|
- Add deterministic signer tests for the exact signed payload.
|
||||||
|
- Keep quote-response signing tests unchanged.
|
||||||
|
|
||||||
|
### 5. Build gated submit path
|
||||||
|
- Add a narrow request-executor or extend trade-executor only if responsibilities stay clear.
|
||||||
|
- Require explicit submit command with preflight id or idempotency key.
|
||||||
|
- Mark request as submit_requested before the relay/API call.
|
||||||
|
- Submit with timeout and reconnect-safe client behavior.
|
||||||
|
- Persist result as submitted, accepted_by_relay, failed, or protocol-specific status.
|
||||||
|
- Enforce idempotency so duplicate commands cannot submit twice.
|
||||||
|
|
||||||
|
### 6. Settlement attribution
|
||||||
|
- Extend outcome attribution to handle repo-created requests.
|
||||||
|
- Match settlement from inventory deltas: EURe decrease and BTC increase consistent with submitted request terms.
|
||||||
|
- Mark unresolved as awaiting_settlement before deadline/grace expires.
|
||||||
|
- Mark not_filled only after deadline/grace and fresh inventory evidence.
|
||||||
|
- Mark completed only with durable inventory movement evidence.
|
||||||
|
|
||||||
|
### 7. Dashboard and controls
|
||||||
|
- Add a Funds or Strategy panel for Create BTC request with preflight amount, slippage/minimum BTC receive, dry-run/preflight action, and live submit only after a valid preflight.
|
||||||
|
- Add a repo-created requests table with request id, created/submitted time, source spend, min receive, state, decisive reason, relay/API response, settlement result, and expandable lifecycle.
|
||||||
|
- Make request ids and returned intent/relay ids copyable.
|
||||||
|
- Label live fund movement clearly; no green success label without settlement.
|
||||||
|
|
||||||
|
## Data and persistence
|
||||||
|
- Prefer adding narrow tables or durable event routes for request preflight, submit commands, submission results, and outcomes.
|
||||||
|
- Preserve raw relay/API responses alongside normalized lifecycle state.
|
||||||
|
- Include enough fields to replay the request and reason about slippage, deadline, and settlement.
|
||||||
|
- Do not reuse incoming quote ids as request ids.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
- Missing request API evidence: stop with blocker.
|
||||||
|
- Signer not registered: block before signing/submission.
|
||||||
|
- EURe inventory stale or unavailable: block.
|
||||||
|
- Requested amount exceeds spendable EURe: block.
|
||||||
|
- Reference price stale: block.
|
||||||
|
- Slippage missing: block.
|
||||||
|
- Duplicate idempotency key: return original result, do not submit again.
|
||||||
|
- Relay timeout: failed or unknown state with durable reason, not completed.
|
||||||
|
- Relay accepted but no inventory movement: awaiting outcome, then not filled after evidence window.
|
||||||
|
- Partial fill, if supported: represent explicitly; otherwise block or mark ambiguous until semantics are known.
|
||||||
|
|
||||||
|
## Concrete implementation order
|
||||||
|
|
||||||
|
### Phase 1. Protocol verification and model
|
||||||
|
- Inspect primary NEAR Intents request API evidence and current repo clients.
|
||||||
|
- Decide endpoint/method and payload shape.
|
||||||
|
- Add request lifecycle constants and schemas.
|
||||||
|
- Add tests for semantic invariants independent of live API.
|
||||||
|
|
||||||
|
### Phase 2. Preflight path
|
||||||
|
- Implement pure preflight helper for EURe-to-BTC exact-in requests.
|
||||||
|
- Wire inventory, reference price, signer registration, amount, slippage, and deadline checks.
|
||||||
|
- Add tests for success, insufficient inventory, stale price, stale inventory, zero amount, and missing signer.
|
||||||
|
|
||||||
|
### Phase 3. Signing and submit client
|
||||||
|
- Implement taker request envelope/signing helper.
|
||||||
|
- Implement relay/API client method with timeout and error normalization.
|
||||||
|
- Add deterministic signing tests and mocked client submission tests.
|
||||||
|
|
||||||
|
### Phase 4. Durable execution path
|
||||||
|
- Add request submit command/result event routes and persistence.
|
||||||
|
- Add idempotency guard.
|
||||||
|
- Add submission failure and duplicate-submit tests.
|
||||||
|
|
||||||
|
### Phase 5. Outcome truth
|
||||||
|
- Extend request outcome attribution using inventory deltas.
|
||||||
|
- Add tests for submitted-not-completed, accepted-not-completed, completed with exact settlement, not-filled after deadline, and ambiguous movement.
|
||||||
|
|
||||||
|
### Phase 6. Operator UI
|
||||||
|
- Add preflight/submit controls and request lifecycle table.
|
||||||
|
- Add copy affordances for request and relay ids.
|
||||||
|
- Add dashboard tests proving no submitted-only request is labeled successful.
|
||||||
|
|
||||||
|
### Phase 7. Deploy and validate
|
||||||
|
- Run targeted tests plus full npm test.
|
||||||
|
- Build dashboard bundle.
|
||||||
|
- Commit with required proof body.
|
||||||
|
- Push to forgejo/main.
|
||||||
|
- Validate rollout image, service state, dashboard bootstrap, and live dry-run/preflight.
|
||||||
|
- If operator explicitly submits live, validate durable request row, submission result, and later settlement or no-fill.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
- Request preflight unit tests.
|
||||||
|
- Request signing/envelope tests.
|
||||||
|
- Request client timeout/failure tests.
|
||||||
|
- Idempotency tests.
|
||||||
|
- Persistence routing tests.
|
||||||
|
- Outcome attribution tests.
|
||||||
|
- Dashboard semantic tests.
|
||||||
|
- Negative tests: submitted != completed; relay accepted != completed; pending EURe cannot be spent; dry-run cannot submit; duplicate submit does not call relay twice; request lifecycle cannot be confused with incoming quote response lifecycle.
|
||||||
|
|
||||||
|
## Validation checklist against the proof
|
||||||
|
- /state or dashboard shows request preflight capability and current spendable EURe.
|
||||||
|
- A dry-run EURe-to-BTC request can be produced without side effects.
|
||||||
|
- A live submit path exists only behind explicit operator action.
|
||||||
|
- Durable request lifecycle rows exist and are visible with copyable ids.
|
||||||
|
- Settlement outcome remains truthful after submission.
|
||||||
|
- All services deploy from repo push without manual cluster reconciliation.
|
||||||
|
|
||||||
|
## Known fakes allowed at start of this turn
|
||||||
|
- Request API method is not yet verified.
|
||||||
|
- Venue-native terminal fill events remain unavailable unless the request API exposes them.
|
||||||
|
- Fee-complete realized PnL remains unavailable.
|
||||||
|
- Automatic portfolio optimization remains unavailable.
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Implementation Proof: NEAR Intents request creation and EURe-to-BTC taker flow
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-04-12
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The repository must be able to create our own NEAR Intents EURe-to-BTC swap request from credited internal inventory, submit it through repo-owned code, and explain the full request lifecycle from preflight through settlement truth.
|
||||||
|
|
||||||
|
For each repo-created request the operator must be able to answer:
|
||||||
|
- which asset and amount we intended to spend
|
||||||
|
- which asset and minimum amount we expected to receive
|
||||||
|
- which signer, verifier contract, nonce, deadline, slippage, and idempotency key were used
|
||||||
|
- whether the request was drafted, blocked, submitted, accepted by relay, failed, awaiting settlement, not filled, or completed
|
||||||
|
- whether completed means durable inventory movement, not relay acceptance
|
||||||
|
- how much BTC was received and how much EURe was spent, if settlement is proven
|
||||||
|
|
||||||
|
## Why this is the next architecture test
|
||||||
|
The existing system can observe incoming NEAR Intents quote requests and respond as a solver or maker. That path has not produced fills at 1.49%, 0.99%, or 0.49% in recent live validation, and it cannot intentionally convert our EURe inventory into BTC.
|
||||||
|
|
||||||
|
The next false path would be manually moving funds or adding a button that submits opaque live requests without durable preflight, request, and settlement evidence. This turn is successful only if the system can create requests as a first-class, auditable repo-owned flow instead of treating live fund movement as an off-system action.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- [I017] Repo-owned NEAR Intents request creation and EURe-to-BTC taker flow: create, sign, submit, and settle our own swap requests from credited inventory with truthful dashboard evidence. tags=intents,taker,execution,inventory,settlement
|
||||||
|
- Active venue only: NEAR Intents.
|
||||||
|
- Active direction: credited internal EURe to BTC.
|
||||||
|
- Use credited spendable internal inventory only. Pending funding remains non-spendable.
|
||||||
|
- Build the request path with dry-run/preflight first, then gated live submission.
|
||||||
|
- Actual live submission requires explicit operator control and must be recorded durably.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
- NEAR Intents exposes a supported request or quote publication method through the solver relay or another documented endpoint callable with our existing API key and verifier signer.
|
||||||
|
- The correct taker intent can be represented as a signed verifier token_diff payload or a documented request envelope whose semantics can be proven before live submission.
|
||||||
|
- Internal inventory snapshots are fresh enough to enforce spendable EURe and settlement deltas.
|
||||||
|
- BTC minimum receive can be derived from reference price plus explicit slippage or minimum-out policy.
|
||||||
|
- The user's intent is to build the capability to move EURe into BTC; live all-EURe execution remains behind an explicit final operator action in the repo-owned UI or API.
|
||||||
|
|
||||||
|
## Turn-shaping rules
|
||||||
|
- Do not manually move funds.
|
||||||
|
- Do not submit an all-EURe live request until repo-owned preflight, durable request logging, and settlement attribution are implemented and the operator explicitly triggers that live request.
|
||||||
|
- Do not use pending or uncredited EURe.
|
||||||
|
- Do not label relay acceptance as completed settlement.
|
||||||
|
- Do not hide slippage, minimum receive, deadline, nonce, signer, or request id.
|
||||||
|
- If the NEAR Intents request API cannot be verified from primary evidence or live dry-run behavior, stop and record the blocker instead of inventing a protocol.
|
||||||
|
- Keep maker quote-response logic intact unless the new taker flow proves it should be paused or separated.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No new venue.
|
||||||
|
- No external wallet or manual bridge action.
|
||||||
|
- No automatic recurring rebalancer.
|
||||||
|
- No realized net PnL claim without fee and cost attribution.
|
||||||
|
- No tax or accounting treatment.
|
||||||
|
- No broad multi-asset request builder beyond EURe-to-BTC.
|
||||||
|
|
||||||
|
## Required operator behavior
|
||||||
|
|
||||||
|
### Request preflight truth
|
||||||
|
Before submission, the operator must see:
|
||||||
|
- spendable EURe available
|
||||||
|
- requested EURe spend amount
|
||||||
|
- BTC reference price
|
||||||
|
- slippage or minimum-out rule
|
||||||
|
- expected BTC receive and minimum BTC receive
|
||||||
|
- deadline and nonce policy
|
||||||
|
- exact signer account and verifier contract
|
||||||
|
- whether this is dry-run only or live-submit capable
|
||||||
|
|
||||||
|
### Request lifecycle truth
|
||||||
|
For each repo-created request, the dashboard must show:
|
||||||
|
- request id, copyable
|
||||||
|
- idempotency key, copyable
|
||||||
|
- intent hash or relay request id if returned, copyable
|
||||||
|
- created_at, submitted_at, resolved_at if known
|
||||||
|
- state: draft, blocked, submitted, accepted_by_relay, failed, awaiting_settlement, not_filled, completed
|
||||||
|
- decisive reason for blocks, failures, or no-fill
|
||||||
|
- settlement evidence if completed
|
||||||
|
|
||||||
|
### Settlement truth
|
||||||
|
Completed requires durable asset movement evidence:
|
||||||
|
- EURe spend delta and BTC receive delta from inventory snapshots
|
||||||
|
- attribution method and caveat if heuristic
|
||||||
|
- submitted-only and relay-accepted rows remain separate from completed
|
||||||
|
- portfolio-vs-hold movement remains separate from realized trade PnL
|
||||||
|
|
||||||
|
## Semantic invariants
|
||||||
|
The implementation and tests must enforce at least:
|
||||||
|
- request submitted != completed
|
||||||
|
- relay accepted != settled inventory movement
|
||||||
|
- pending EURe cannot be spent
|
||||||
|
- insufficient spendable EURe blocks before signing or submission
|
||||||
|
- slippage and minimum-out must be explicit and persisted
|
||||||
|
- duplicate idempotency key cannot produce duplicate live submissions
|
||||||
|
- dry-run request cannot accidentally submit
|
||||||
|
- completed request rows require durable settlement evidence
|
||||||
|
- request-creation flow is distinct from incoming quote-response flow
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- The repo contains a verified NEAR Intents request-creation client or a recorded blocker proving the venue path is unavailable.
|
||||||
|
- A request preflight API computes amount, min-out, deadline, nonce policy, signer, verifier, and inventory checks from durable/current data.
|
||||||
|
- A gated live submission API exists only after preflight and records durable request, submission, and result events.
|
||||||
|
- The dashboard shows repo-created request rows separately from incoming quote-response rows, with copyable ids and lifecycle reasons.
|
||||||
|
- Settlement attribution handles repo-created requests without treating relay acceptance as completion.
|
||||||
|
- Regression tests cover request signing/envelope semantics, inventory blocks, dry-run safety, idempotency, submission failure, submitted-not-completed, and completed-only-with-settlement.
|
||||||
|
- The result is deployed through the repo workflow and validated against live service state.
|
||||||
|
|
||||||
|
## Validation evidence required
|
||||||
|
- Unit tests for request preflight and minimum-out/slippage math.
|
||||||
|
- Unit tests for signed request or request envelope construction using deterministic signer fixtures.
|
||||||
|
- Unit tests proving insufficient EURe blocks before signing/submission.
|
||||||
|
- Unit tests proving submitted/accepted requests do not count as completed without inventory deltas.
|
||||||
|
- Live dry-run/preflight evidence for current credited EURe inventory.
|
||||||
|
- If live submission is explicitly triggered, durable DB evidence for request, submission result, and later settlement or no-fill.
|
||||||
|
- Dashboard bootstrap evidence showing the new request lifecycle row with copyable ids and truthful state.
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- Live funds are moved outside repo-owned code.
|
||||||
|
- A request is submitted without durable preflight and request records.
|
||||||
|
- The UI claims BTC was bought from relay acceptance alone.
|
||||||
|
- A dry-run path can submit.
|
||||||
|
- Pending funding is treated as spendable.
|
||||||
|
- The implementation cannot prove the NEAR Intents request API shape and still proceeds.
|
||||||
|
|
||||||
|
## Current real before this turn
|
||||||
|
- The repo has verifier signing for quote-response token_diff payloads.
|
||||||
|
- The repo has a solver relay websocket client and quote-response submission path.
|
||||||
|
- Credited internal BTC/EURe inventory is synced durably.
|
||||||
|
- Quote, decision, command, submission result, and outcome attribution records exist for incoming quote-response flow.
|
||||||
|
- The dashboard distinguishes submitted, not-filled, completed, rejected, and blocked rows for incoming quote responses.
|
||||||
|
|
||||||
|
## Deliberately not built by this proof
|
||||||
|
- General portfolio optimizer.
|
||||||
|
- Automatic all-in rebalancing loop.
|
||||||
|
- Fee-complete realized PnL.
|
||||||
|
- Venue-native terminal fill ingestion unless the request API provides it directly.
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
# Implementation Turn: DB-backed asset registry and multi-pair strategy configuration
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-12
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Move supported assets, enabled pairs, pair filters, and per-pair edge/trading limits out of environment variables and into durable Postgres state, with a re-importable NEAR Intents asset catalog and fail-closed live trading behavior.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- none selected; this turn was opened directly from the approved operator request on 2026-05-12.
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
- DB state is the source of truth for assets, enabled pairs, edge, limits, and pair modes.
|
||||||
|
- Env remains only for infra/secrets/service wiring.
|
||||||
|
- Importing an asset never enables trading by itself.
|
||||||
|
- Pair config updates are versioned. Historical decisions must remain explainable.
|
||||||
|
- Services fail closed if DB config is missing, stale, invalid, or internally inconsistent.
|
||||||
|
- Current nBTC/EURe behavior must survive the migration before any additional pair trades.
|
||||||
|
|
||||||
|
## External source
|
||||||
|
The supported asset catalog comes from the NEAR Intents 1Click tokens endpoint:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET https://1click.chaindefuser.com/v0/tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
The docs describe this endpoint as the source for supported token `assetId` values. A live check on 2026-05-12 returned an array of 163 token records and included:
|
||||||
|
- `nep141:nbtc.bridge.near`
|
||||||
|
- `nep141:btc.omft.near`
|
||||||
|
- `nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near`
|
||||||
|
|
||||||
|
## Backend changes
|
||||||
|
|
||||||
|
### 1. Add config schema
|
||||||
|
Add tables through the tracked schema path:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
supported_asset_import_runs
|
||||||
|
trading_assets
|
||||||
|
trading_pairs
|
||||||
|
pair_strategy_configs
|
||||||
|
pair_price_routes
|
||||||
|
pair_config_audit_log
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimum fields:
|
||||||
|
- `supported_asset_import_runs`: run id, source URL, fetched_at, status, token_count, added_count, updated_count, retired_count, raw response hash, error.
|
||||||
|
- `trading_assets`: asset id, venue, symbol, label, decimals, blockchain, contract address, latest price, price updated at, supported, retired_at, enabled_for_inventory, raw payload, updated_at.
|
||||||
|
- `trading_pairs`: pair id, venue, asset_in, asset_out, mode, enabled, status, created_at, updated_at.
|
||||||
|
- `pair_strategy_configs`: config id, pair id, version, active, edge_bps, max_notional, min_notional, slippage_bps, min_deadline_ms, price_max_age_ms, inventory_max_age_ms, created_at, created_by, reason.
|
||||||
|
- `pair_price_routes`: route id, pair id, source, base asset id, quote asset id, route config JSON, max age, enabled.
|
||||||
|
- `pair_config_audit_log`: audit id, entity type, entity id, action, old value, new value, changed_at, changed_by, reason.
|
||||||
|
|
||||||
|
### 2. Build supported-token importer
|
||||||
|
- Add a pure normalizer for 1Click token records.
|
||||||
|
- Add an idempotent importer that fetches `/v0/tokens`, validates shape, upserts `trading_assets`, and writes `supported_asset_import_runs`.
|
||||||
|
- Mark previously supported assets as `supported=false` and `retired_at=<run time>` when missing from the latest import.
|
||||||
|
- Keep raw payloads and source metadata.
|
||||||
|
- Add an operator-safe command/API to rerun the importer.
|
||||||
|
- Add tests for success, duplicate import, token update, asset retirement, malformed response, and network failure.
|
||||||
|
|
||||||
|
### 3. Seed current production truth
|
||||||
|
- Seed current nBTC, legacy OMFT BTC, and EURe assets if absent.
|
||||||
|
- Seed both directed nBTC/EURe pair rows needed by maker quote response.
|
||||||
|
- Seed the active strategy config at 49 bps.
|
||||||
|
- Seed current max notional/deadline/freshness settings from existing production defaults.
|
||||||
|
- Keep legacy OMFT BTC inventory-visible but not active for maker trading unless explicitly enabled.
|
||||||
|
|
||||||
|
### 4. Runtime DB config loader
|
||||||
|
- Add a loader module that returns:
|
||||||
|
- asset registry map
|
||||||
|
- enabled pair set
|
||||||
|
- pair strategy config by pair
|
||||||
|
- tracked asset ids
|
||||||
|
- supported/retired flags
|
||||||
|
- Cache briefly but refresh without restart.
|
||||||
|
- Expose config load state in service `/state`.
|
||||||
|
- Fail closed when DB is unreachable or config is invalid.
|
||||||
|
- Store config version metadata in emitted decisions and commands.
|
||||||
|
|
||||||
|
### 5. Replace env-backed pair filtering
|
||||||
|
- Remove runtime dependency on `NEAR_INTENTS_PAIR_FILTER`.
|
||||||
|
- `near-intents-ingest` loads enabled observed pairs from DB.
|
||||||
|
- Ingest should preserve raw quote data but only normalize/publish pairs approved for observation/trading.
|
||||||
|
- Pair updates must apply without redeploy.
|
||||||
|
- Keep a safe disabled state when no pairs are configured.
|
||||||
|
|
||||||
|
### 6. Replace env-backed strategy edge
|
||||||
|
- Remove runtime dependency on `STRATEGY_GROSS_THRESHOLD_PCT`.
|
||||||
|
- Strategy selects the active `pair_strategy_configs` row for the incoming quote pair.
|
||||||
|
- Decisions include `pair_id`, `pair_config_id`, `pair_config_version`, `edge_bps`, and active limits.
|
||||||
|
- Unsupported/disabled/unpriced pairs emit explicit rejected decisions, not silent drops where practical.
|
||||||
|
- Existing nBTC/EURe math remains the first compatibility target.
|
||||||
|
|
||||||
|
### 7. Generalize asset and pair math
|
||||||
|
- Replace `tradingBtc`/`tradingEure` assumptions in strategy with pair assets from DB.
|
||||||
|
- Use DB decimals for amount conversion.
|
||||||
|
- Preserve current BTC/EURe reference-price support first.
|
||||||
|
- Block pairs whose price route is missing or stale.
|
||||||
|
- Keep pair direction explicit instead of inferring BTC/EURe-only names.
|
||||||
|
|
||||||
|
### 8. Price route model
|
||||||
|
- Current market reference is BTC/EUR-specific.
|
||||||
|
- Add DB price routes before enabling non-BTC/EURe trading.
|
||||||
|
- For this turn, support the current nBTC/EURe route and make other pairs blocked with `price_route_missing`.
|
||||||
|
- Persist enough price-route metadata so later sources can be added without another env-based pair model.
|
||||||
|
|
||||||
|
### 9. Inventory and funding visibility
|
||||||
|
- Inventory sync uses DB `trading_assets` for tracked assets.
|
||||||
|
- Dashboard shows balances for known/tracked assets including retired assets.
|
||||||
|
- Funding/deposit observations label assets from DB metadata.
|
||||||
|
- Assets may be `supported=false` but still inventory-visible.
|
||||||
|
|
||||||
|
### 10. Dashboard controls
|
||||||
|
- Add an asset catalog section:
|
||||||
|
- import status
|
||||||
|
- import button/control
|
||||||
|
- counts for supported/retired/known assets
|
||||||
|
- searchable asset table
|
||||||
|
- Add a pair config section:
|
||||||
|
- pair rows with mode/status
|
||||||
|
- active strategy config and edge bps
|
||||||
|
- price route status
|
||||||
|
- controls to enable observe-only and update edge
|
||||||
|
- Live trading enablement must remain explicit and separated from import.
|
||||||
|
- Show pair config version in quote lifecycle rows.
|
||||||
|
|
||||||
|
### 11. Alerts and health
|
||||||
|
- Runtime health uses DB active pair set instead of a single `activePair`.
|
||||||
|
- Alerts must include pair id where applicable.
|
||||||
|
- Missing DB config or invalid active pair config becomes a critical blocked state for trading services.
|
||||||
|
- NEAR upstream incident relevance should be scoped to DB tracked assets/chains.
|
||||||
|
|
||||||
|
### 12. Deployment config cleanup
|
||||||
|
- Remove pair/asset/edge env vars from `deploy/k8s/base/unrip.yaml` after DB seed and runtime loading are in place.
|
||||||
|
- Keep infra/secrets env vars.
|
||||||
|
- Ensure a repo push deploys the whole change without manual database editing.
|
||||||
|
|
||||||
|
## Concrete implementation order
|
||||||
|
|
||||||
|
### Phase 1. Schema and seed
|
||||||
|
- Add DB schema and helper functions.
|
||||||
|
- Add seed routine for current nBTC/EURe production config.
|
||||||
|
- Add tests proving seed is idempotent.
|
||||||
|
|
||||||
|
### Phase 2. Asset importer
|
||||||
|
- Implement 1Click token fetch/normalize/import.
|
||||||
|
- Add CLI/control path for re-import.
|
||||||
|
- Add tests for import and retirement semantics.
|
||||||
|
|
||||||
|
### Phase 3. Runtime config loader
|
||||||
|
- Implement DB loader and fail-closed validation.
|
||||||
|
- Convert asset registry consumers that only need labels/decimals.
|
||||||
|
- Add tests for missing/invalid config.
|
||||||
|
|
||||||
|
### Phase 4. Ingest migration
|
||||||
|
- Replace env/file pair filter with DB pair set.
|
||||||
|
- Preserve current pair behavior.
|
||||||
|
- Add tests for pair update and no-config disabled behavior.
|
||||||
|
|
||||||
|
### Phase 5. Strategy migration
|
||||||
|
- Load pair strategy config from DB.
|
||||||
|
- Remove global env edge use from runtime.
|
||||||
|
- Persist pair config version in decisions/commands.
|
||||||
|
- Add current-pair compatibility and edge-version tests.
|
||||||
|
|
||||||
|
### Phase 6. Dashboard and controls
|
||||||
|
- Add asset import and pair config surfaces.
|
||||||
|
- Add controls for re-import and observe-only/pair edge update.
|
||||||
|
- Add tests proving dashboard data comes from DB.
|
||||||
|
|
||||||
|
### Phase 7. Deployment cleanup and validation
|
||||||
|
- Remove asset/pair/edge env vars from deployment config.
|
||||||
|
- Run targeted tests and full test suite.
|
||||||
|
- Build dashboard bundle.
|
||||||
|
- Deploy through repo workflow.
|
||||||
|
- Validate live importer, current pair config, quote ingest, decisions, and dashboard bootstrap.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
- Asset import normalizer tests.
|
||||||
|
- Import idempotency tests.
|
||||||
|
- Retired asset tests.
|
||||||
|
- Schema/seed idempotency tests.
|
||||||
|
- Runtime config loader tests.
|
||||||
|
- Fail-closed missing DB config tests.
|
||||||
|
- Ingest DB pair filter tests.
|
||||||
|
- Strategy per-pair edge tests.
|
||||||
|
- Strategy config versioning tests.
|
||||||
|
- Dashboard asset/pair config tests.
|
||||||
|
- Deployment config static test proving pair/edge env vars are absent.
|
||||||
|
|
||||||
|
## Validation checklist against the proof
|
||||||
|
- DB contains current assets and current nBTC/EURe pair.
|
||||||
|
- Supported-token importer can rerun and record a new import run.
|
||||||
|
- Current active pair and 49 bps edge load from DB.
|
||||||
|
- NEAR Intents ingest no longer depends on `NEAR_INTENTS_PAIR_FILTER`.
|
||||||
|
- Strategy no longer depends on `STRATEGY_GROSS_THRESHOLD_PCT`.
|
||||||
|
- Dashboard shows asset import status and pair strategy config.
|
||||||
|
- New imported assets are not trade-enabled by default.
|
||||||
|
- Current pair still emits decisions and quote responses when armed.
|
||||||
|
- All services deploy from repo push.
|
||||||
|
|
||||||
|
## Known fakes allowed at start of this turn
|
||||||
|
- Only current BTC/EURe pricing route is real.
|
||||||
|
- Additional imported pairs may be observe-only or blocked until price routes exist.
|
||||||
|
- Venue-native terminal fills remain unavailable unless existing paths already provide them.
|
||||||
|
- Fee-complete realized PnL remains unavailable.
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Implementation Proof: DB-backed asset registry and multi-pair strategy configuration
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-12
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The repository must stop treating tradeable assets, active pairs, pair filters, and edge thresholds as environment configuration. Those values must live in Postgres, be visible and auditable in the dashboard, and be reloadable without a deploy.
|
||||||
|
|
||||||
|
The system must be able to import the current NEAR Intents supported-token catalog from the 1Click API, persist it durably, and then enable operator-approved pairs from that catalog with per-pair strategy settings.
|
||||||
|
|
||||||
|
## Why this is the next architecture test
|
||||||
|
The previous one-pair model hardcoded BTC/EURe assumptions across config, ingest filtering, pricing, strategy, inventory, dashboard labels, alerts, and taker request creation. That worked for proving one narrow loop, but it blocks the next practical requirement: adding pairs and adjusting edge without editing deployment env vars.
|
||||||
|
|
||||||
|
This turn succeeds only if "what can be observed" and "what may trade" are durable runtime data. A new asset or pair must be introduced by DB state and operator controls, not by changing Kubernetes config.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Add DB-backed asset registry sourced from NEAR Intents / 1Click supported tokens.
|
||||||
|
- Add idempotent importer for `GET https://1click.chaindefuser.com/v0/tokens`.
|
||||||
|
- Preserve each import run and raw token payload so assets can be re-imported as support changes.
|
||||||
|
- Add DB-backed enabled pairs with explicit venue, source asset, destination asset, mode, and status.
|
||||||
|
- Add DB-backed per-pair strategy config: edge bps, max notional, freshness requirements, slippage/default request limits where applicable.
|
||||||
|
- Replace env-backed pair filter and edge selection in runtime services.
|
||||||
|
- Seed the current production truth from repo-controlled bootstrap code:
|
||||||
|
- `nep141:nbtc.bridge.near`
|
||||||
|
- `nep141:btc.omft.near`
|
||||||
|
- `nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near`
|
||||||
|
- current nBTC/EURe pair
|
||||||
|
- current 49 bps edge
|
||||||
|
- Preserve current BTC/EURe behavior after migration unless the DB pair config explicitly disables it.
|
||||||
|
- Make unsupported or unpriced pairs fail closed: observe-only or rejected, never live trading.
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
- Supported asset: an asset currently returned by the NEAR Intents 1Click tokens endpoint.
|
||||||
|
- Known asset: any asset ever imported or manually registered in the DB.
|
||||||
|
- Enabled pair: a directed pair the operator has approved for quote ingestion and/or trading.
|
||||||
|
- Pair mode: `observe_only`, `maker`, `taker`, or `both`.
|
||||||
|
- Pair strategy version: the immutable config snapshot used by a decision, command, or request.
|
||||||
|
- Retired asset: a known asset not present in the latest import, retained for history and inventory visibility.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
- The 1Click tokens endpoint is the primary machine-readable source for currently supported NEAR Intents assets.
|
||||||
|
- The endpoint can change over time, so importer output must be append/audit friendly.
|
||||||
|
- Env vars remain valid only for infra and secrets: database URL, Kafka/RPC URLs, API keys, signer private key, service ports.
|
||||||
|
- Pair and edge config must not be set by env vars in production.
|
||||||
|
- Broad "any pair" support still requires a fresh reference price route and spendable inventory for that pair before trading can be armed.
|
||||||
|
|
||||||
|
## Turn-shaping rules
|
||||||
|
- Do not enable live trading for newly imported pairs by default.
|
||||||
|
- Do not delete assets when they disappear from the live token list; mark them unsupported/retired and keep historical records.
|
||||||
|
- Do not remove existing BTC/EURe history compatibility.
|
||||||
|
- Do not widen live-funds risk without explicit operator enablement.
|
||||||
|
- Do not treat a token being supported by 1Click as proof that we have inventory, pricing, liquidity, or safe execution for it.
|
||||||
|
- Do not introduce env vars for pair selection, edge thresholds, or active trading assets.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No automatic pair discovery to live trading.
|
||||||
|
- No generalized portfolio optimizer.
|
||||||
|
- No new venue beyond NEAR Intents.
|
||||||
|
- No realized net PnL or fee-complete accounting.
|
||||||
|
- No automatic recurring rebalancer.
|
||||||
|
- No live taker submission expansion unless it uses the new pair config and remains explicitly gated.
|
||||||
|
- No guarantee that every supported-token pair has solver liquidity.
|
||||||
|
|
||||||
|
## Required operator behavior
|
||||||
|
|
||||||
|
### Asset catalog truth
|
||||||
|
The operator must be able to see:
|
||||||
|
- latest supported-token import status
|
||||||
|
- import source URL and fetched_at timestamp
|
||||||
|
- token count
|
||||||
|
- added, updated, unchanged, and retired counts
|
||||||
|
- asset id, symbol, decimals, blockchain, contract address, price, priceUpdatedAt
|
||||||
|
- raw payload for imported tokens
|
||||||
|
- whether an asset is supported, known, enabled for inventory tracking, or retired
|
||||||
|
|
||||||
|
### Pair configuration truth
|
||||||
|
The operator must be able to see:
|
||||||
|
- enabled directed pairs and their modes
|
||||||
|
- pair status: disabled, observe_only, maker, taker, both
|
||||||
|
- asset metadata for each side
|
||||||
|
- edge bps and max notional for the active strategy version
|
||||||
|
- price route availability and freshness policy
|
||||||
|
- last decision/config version using the pair
|
||||||
|
- whether the pair can trade or is blocked, with a decisive reason
|
||||||
|
|
||||||
|
### Trading safety truth
|
||||||
|
Every decision, command, quote response, and repo-created request must persist:
|
||||||
|
- pair id
|
||||||
|
- pair config version
|
||||||
|
- edge bps used
|
||||||
|
- max notional used
|
||||||
|
- asset decimals and reference price id used
|
||||||
|
- reason if the pair was unsupported, disabled, unpriced, stale, or inventory-blocked
|
||||||
|
|
||||||
|
## Semantic invariants
|
||||||
|
The implementation and tests must enforce:
|
||||||
|
- pair/edge/env vars are not required for production trading behavior
|
||||||
|
- DB config absence fails closed
|
||||||
|
- imported support does not imply trading enablement
|
||||||
|
- retired assets remain visible for old balances and history
|
||||||
|
- each trade decision stores the exact pair config version used
|
||||||
|
- a pair cannot trade without a fresh reference price route
|
||||||
|
- a pair cannot trade without known asset decimals for both sides
|
||||||
|
- duplicate asset imports are idempotent
|
||||||
|
- updating edge creates a new strategy config version instead of mutating historical decision meaning
|
||||||
|
- dashboard labels come from the DB asset registry, not hardcoded BTC/EURe config
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- DB schema and bootstrap seed current production assets/pair/edge.
|
||||||
|
- Supported-token importer fetches 1Click tokens, upserts assets, records an import run, and marks missing assets retired without deleting them.
|
||||||
|
- Runtime config loader provides assets, enabled pairs, and strategy config to ingest, strategy, dashboard, alerts, and outcome attribution.
|
||||||
|
- NEAR Intents ingest uses DB-backed enabled pairs instead of `NEAR_INTENTS_PAIR_FILTER`.
|
||||||
|
- Strategy uses per-pair DB edge config instead of `STRATEGY_GROSS_THRESHOLD_PCT`.
|
||||||
|
- Current nBTC/EURe maker behavior remains functional after migration.
|
||||||
|
- Dashboard exposes asset import status and pair strategy config.
|
||||||
|
- Deployment config no longer carries trading asset/pair/edge env vars.
|
||||||
|
- Tests prove fail-closed behavior, import idempotency, config versioning, and current-pair compatibility.
|
||||||
|
- The result is deployed through repo workflow and validated against live service state.
|
||||||
|
|
||||||
|
## Validation evidence required
|
||||||
|
- Unit tests for supported-token import normalization and idempotent upsert.
|
||||||
|
- Unit tests for retired asset handling.
|
||||||
|
- Unit tests for DB pair config loading and fail-closed missing-config behavior.
|
||||||
|
- Unit tests proving edge changes version strategy config and historical decisions keep the old version.
|
||||||
|
- Strategy tests for current nBTC/EURe pair using DB config.
|
||||||
|
- Ingest tests proving pair filtering comes from DB pair set.
|
||||||
|
- Dashboard tests proving labels/config come from DB asset registry.
|
||||||
|
- Live importer evidence showing current token count and presence of nBTC, legacy OMFT BTC, and EURe.
|
||||||
|
- Live dashboard/bootstrap evidence showing current pair and 49 bps edge loaded from DB.
|
||||||
|
- Deployment evidence showing pair/edge env vars removed from repo-owned production config.
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- A service still needs pair or edge env vars to trade the current pair.
|
||||||
|
- A newly imported asset becomes trade-enabled by default.
|
||||||
|
- Missing DB config causes uncontrolled all-pair ingestion or trading.
|
||||||
|
- Retired assets vanish from historical dashboard/inventory views.
|
||||||
|
- Decisions do not persist pair config version.
|
||||||
|
- The importer cannot be rerun safely.
|
||||||
|
- The dashboard or alerts still assume one BTC/EURe active pair after migration.
|
||||||
|
|
||||||
|
## Current real before this turn
|
||||||
|
- The repo can ingest NEAR Intents quote flow for one configured pair.
|
||||||
|
- The repo can maintain internal inventory for the configured BTC/EURe assets.
|
||||||
|
- The repo can publish maker quote responses and attribute outcomes for the current pair.
|
||||||
|
- The dashboard can show current funds, quote lifecycle, and service state.
|
||||||
|
- The live 1Click tokens endpoint currently returns token records including nBTC, legacy BTC OMFT, and EURe.
|
||||||
|
|
||||||
|
## Deliberately not built by this proof
|
||||||
|
- Fully automated pair profitability discovery.
|
||||||
|
- Liquidity-depth probing for every possible pair.
|
||||||
|
- Cross-venue routing.
|
||||||
|
- Fee-complete realized PnL.
|
||||||
|
- Autonomous live trading enablement for imported assets.
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
# Implementation Turn: Pair-native trade semantics and multi-asset outcome truth
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-18
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Replace hidden BTC/EURe assumptions in shared runtime paths with pair-native asset semantics so maker decisions, taker requests, settlement attribution, valuation visibility, dashboard labels, and alerts work from DB asset/pair config without silently ignoring non-BTC/EURe assets.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- none selected; this turn was opened directly from the operator request on 2026-05-18.
|
||||||
|
|
||||||
|
## Design Rules
|
||||||
|
- DB asset, pair, strategy config, and price route rows are the canonical runtime model.
|
||||||
|
- Existing nBTC/EURe behavior and 49 bps edge must survive every refactor.
|
||||||
|
- nBTC/Ethereum USDC must be a first regression pair because it exposes the current hidden EURe assumptions.
|
||||||
|
- Legacy fields may remain for old stored history and API compatibility, but new runtime logic must use generic pair-native fields.
|
||||||
|
- Missing route, stale price, missing decimals, missing inventory, or missing DB config fails closed.
|
||||||
|
- Tracked assets without valuation remain visible with explicit reasons.
|
||||||
|
- No new live trading is enabled by import alone.
|
||||||
|
- No manual deployment or cluster repair is part of this turn.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
The DB-backed configuration turn made assets and pairs durable, but several runtime paths still behave as if there is one preferred BTC/EURe trading path:
|
||||||
|
|
||||||
|
- taker preflight parses `amount_eure`, requires `eure_per_btc`, and computes BTC receive from EURe
|
||||||
|
- strategy stores and displays `eure_notional` even when the quote notional is USDC
|
||||||
|
- outcome attribution takes `btcAsset` and `eureAsset` and ignores other pair assets
|
||||||
|
- portfolio valuation special-cases BTC, EURe, and USDC instead of making unvalued assets explicit
|
||||||
|
- dashboard request and lifecycle copy still says EURe-to-BTC
|
||||||
|
- alerts and health still describe stale truth around one `activePair`
|
||||||
|
|
||||||
|
The implementation should turn the current DB pair model into the actual shared runtime model, not just a dashboard/config layer.
|
||||||
|
|
||||||
|
## Backend Changes
|
||||||
|
|
||||||
|
### 1. Canonical pair-native trade terms
|
||||||
|
- Add or consolidate helpers for resolving a directed pair from DB config:
|
||||||
|
- by `pair_id`
|
||||||
|
- by `asset_in` and `asset_out`
|
||||||
|
- by default taker pair when the operator has not specified a pair
|
||||||
|
- Define normalized runtime terms used by strategy, request preflight, commands, and UI:
|
||||||
|
- `pair_id`, `venue`
|
||||||
|
- `source_asset_id`, `destination_asset_id`
|
||||||
|
- `source_symbol`, `destination_symbol`
|
||||||
|
- `source_decimals`, `destination_decimals`
|
||||||
|
- `source_amount_units`, `destination_amount_units`
|
||||||
|
- `notional`, `notional_asset_id`, `notional_symbol`
|
||||||
|
- `max_notional`, `min_notional`
|
||||||
|
- `price_route_id`, `reference_price_id`
|
||||||
|
- Keep compatibility alias mapping:
|
||||||
|
- `amount_eure` -> `source_amount` only for old request callers
|
||||||
|
- `eure_notional` may be emitted temporarily only as a deprecated alias when the notional asset is EURe
|
||||||
|
- `max_notional_eure` may be read from old rows but should not be the primary field
|
||||||
|
- Add tests that new decision/request builders expose generic fields for both nBTC/EURe and nBTC/USDC.
|
||||||
|
|
||||||
|
### 2. Generic route-rate adapter
|
||||||
|
- Update market price events for current adapters to include generic route fields:
|
||||||
|
- `base_asset_id`
|
||||||
|
- `quote_asset_id`
|
||||||
|
- `quote_per_base`
|
||||||
|
- `base_per_quote`
|
||||||
|
- `price_route_id`
|
||||||
|
- `reference_pair`
|
||||||
|
- Preserve old fields such as `eure_per_btc` and `usdc_per_btc` for stored-row compatibility while new strategy/request code reads generic fields first.
|
||||||
|
- Replace strategy direction names like `btc_to_eure` and `usdc_to_btc` in new logic with route-relative direction:
|
||||||
|
- `base_to_quote`
|
||||||
|
- `quote_to_base`
|
||||||
|
- Preserve display labels that include actual DB symbols.
|
||||||
|
- Add tests for BTC/EUR and BTC/USDC adapter payloads and route-relative direction classification.
|
||||||
|
|
||||||
|
### 3. Strategy migration
|
||||||
|
- Rename internal variables away from `maxNotionalEure` and `eure_notional` where they are not specifically EURe.
|
||||||
|
- Compute fair output/input from the generic route-rate adapter.
|
||||||
|
- Store new decision fields:
|
||||||
|
- `notional`
|
||||||
|
- `notional_asset_id`
|
||||||
|
- `notional_symbol`
|
||||||
|
- `max_notional`
|
||||||
|
- `price_route_id`
|
||||||
|
- `reference_price_id`
|
||||||
|
- asset decimals
|
||||||
|
- Store command fields with enough pair-native data for later attribution:
|
||||||
|
- source and destination asset ids
|
||||||
|
- proposed source/destination amounts
|
||||||
|
- expected inventory delta or enough terms to derive it
|
||||||
|
- Keep current pair config version and edge bps persistence unchanged.
|
||||||
|
- Add regression tests:
|
||||||
|
- nBTC/EURe exact-in and exact-out still produce expected decisions
|
||||||
|
- nBTC/USDC decision persists USDC notional as `notional_symbol=USDC`, not EUR
|
||||||
|
- unsupported route fails closed with `price_route_missing` or `unsupported_price_route`
|
||||||
|
|
||||||
|
### 4. Pair-native taker preflight
|
||||||
|
- Change request preflight input handling:
|
||||||
|
- accept `pair_id`
|
||||||
|
- accept `asset_in` and `asset_out` as an alternative
|
||||||
|
- accept `source_amount` or `amount`
|
||||||
|
- accept `amount_eure` only as a legacy alias when no generic amount is supplied
|
||||||
|
- Resolve the pair from DB config and require taker mode or both mode.
|
||||||
|
- Use DB asset decimals to parse the source amount.
|
||||||
|
- Load the latest price for the pair's route, not just latest BTC/EUR price.
|
||||||
|
- Compute expected destination amount from generic route fields:
|
||||||
|
- source is route base -> destination = source * quote_per_base
|
||||||
|
- source is route quote -> destination = source * base_per_quote
|
||||||
|
- Apply slippage to produce minimum destination amount.
|
||||||
|
- Persist preflight fields generically:
|
||||||
|
- pair id/config/version/edge
|
||||||
|
- source/destination assets and units
|
||||||
|
- route id/reference price id
|
||||||
|
- expected/min destination units
|
||||||
|
- slippage/deadline/signer/verifier
|
||||||
|
- Keep live submit gated behind existing executor controls.
|
||||||
|
- Add tests:
|
||||||
|
- nBTC/EURe legacy alias still works
|
||||||
|
- nBTC/USDC pair preflight does not require `amount_eure` or `eure_per_btc`
|
||||||
|
- route missing blocks before solver quote
|
||||||
|
- stale route blocks before solver quote
|
||||||
|
- insufficient source inventory emits source-asset-specific reason
|
||||||
|
|
||||||
|
### 5. Pair-native outcome attribution
|
||||||
|
- Refactor quote outcome attribution so the active delta assets are derived from expected command deltas.
|
||||||
|
- Refactor request outcome attribution so delta assets are derived from preflight source/destination terms.
|
||||||
|
- Remove `btcAsset`/`eureAsset` parameters from outcome refresh APIs or make them compatibility fallbacks only.
|
||||||
|
- Persist attribution deltas by actual asset id and use DB metadata only for formatting.
|
||||||
|
- Replace hardcoded humanized text such as "EURe decrease and BTC increase" with generic source/destination wording.
|
||||||
|
- Add tests:
|
||||||
|
- maker BTC/USDC command completes when inventory shows BTC decrease and USDC increase, or the correct inverse depending on command side
|
||||||
|
- taker USDC/BTC request completes from USDC decrease and BTC increase
|
||||||
|
- unrelated third-asset movements do not create false completion
|
||||||
|
- ambiguous matching remains ambiguous
|
||||||
|
- old BTC/EURe rows still normalize for dashboard history
|
||||||
|
|
||||||
|
### 6. Portfolio and valuation visibility
|
||||||
|
- Make portfolio inputs carry all tracked assets from DB.
|
||||||
|
- Build valuation assets from available price routes instead of symbol-only checks where practical.
|
||||||
|
- For this turn, support:
|
||||||
|
- BTC wrappers valued from BTC/EUR route
|
||||||
|
- EURe as EUR cash-equivalent
|
||||||
|
- USDC valued from BTC/EUR plus BTC/USDC route
|
||||||
|
- For any other tracked asset, include an explicit unvalued entry:
|
||||||
|
- `valuation_status=unvalued`
|
||||||
|
- `valuation_reason=valuation_route_missing`
|
||||||
|
- Ensure totals include only valued assets and expose unvalued assets separately.
|
||||||
|
- Add tests:
|
||||||
|
- tracked USDC remains valued
|
||||||
|
- a tracked non-BTC/non-EURe/non-USDC asset appears with `valuation_route_missing`
|
||||||
|
- no tracked asset disappears from balance rows
|
||||||
|
|
||||||
|
### 7. Dashboard migration
|
||||||
|
- Request creation UI:
|
||||||
|
- add pair selection from DB taker-enabled or both-mode pairs
|
||||||
|
- label amount as "Spend <symbol>"
|
||||||
|
- submit `pair_id` and `source_amount`
|
||||||
|
- remove EURe-to-BTC headers and copy
|
||||||
|
- Strategy/lifecycle UI:
|
||||||
|
- display generic notional with `notional_symbol`
|
||||||
|
- show pair route and config version from DB
|
||||||
|
- show settlement deltas from attributed asset metadata
|
||||||
|
- keep old rows readable when only `eure_notional` exists
|
||||||
|
- Funds/portfolio UI:
|
||||||
|
- show every tracked asset
|
||||||
|
- show valuation source or unvalued reason
|
||||||
|
- show deposit addresses by chain/assets without implying only BTC/EURe
|
||||||
|
- Status bar:
|
||||||
|
- replace single "Reference BTC/EUR" tile with route-aware reference status or latest key route labels
|
||||||
|
- avoid one active-pair wording when multiple pairs are enabled
|
||||||
|
- Add dashboard tests for request form payloads, generic notional labels, unvalued asset rows, and no hardcoded EURe-to-BTC request title.
|
||||||
|
|
||||||
|
### 8. Alerts and runtime health
|
||||||
|
- Replace single `activePair` alert context with active pair and price-route sets from DB config where service behavior is pair-scoped.
|
||||||
|
- Reference price stale alerts should identify the route or pair whose price is stale.
|
||||||
|
- Runtime health should not mark all trading truth healthy because the old active pair has a price while another enabled tradeable pair has no fresh route.
|
||||||
|
- Keep service-level alerts for DB unavailable, ingest disconnected, and inventory stale.
|
||||||
|
- Add tests for:
|
||||||
|
- stale route alert scoped to the affected pair
|
||||||
|
- active pair set shown in service state
|
||||||
|
- strategy/executor armed critical truth check still fires when any enabled trading pair has critical stale truth
|
||||||
|
|
||||||
|
### 9. Static cleanup and ops compatibility
|
||||||
|
- Update or retire ops scripts that still read removed pair/asset env vars, especially `scripts/ops/watch_live_mm.py`.
|
||||||
|
- Static tests should prevent reintroducing runtime dependencies on `TRADING_BTC_ASSET_ID`, `TRADING_EURE_ASSET_ID`, `NEAR_INTENTS_PAIR_FILTER`, or `STRATEGY_GROSS_THRESHOLD_PCT`.
|
||||||
|
- Keep deployment config free of trading asset/pair/edge env vars.
|
||||||
|
|
||||||
|
## Data and Persistence
|
||||||
|
- Prefer additive payload fields over destructive schema rewrites.
|
||||||
|
- Existing stored rows with `eure_notional` remain readable.
|
||||||
|
- New rows should use generic fields first.
|
||||||
|
- If a DB schema change is necessary, add it through the tracked schema path and make it idempotent.
|
||||||
|
- Preserve raw event payloads and normalized records for replay.
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
- Pair exists but is observe-only: preflight and trading block with `pair_not_taker_enabled` or `pair_not_maker_enabled`.
|
||||||
|
- Pair has no price route: block with `price_route_missing`.
|
||||||
|
- Price route exists but no fresh price event: block with `stale_reference_price` or `reference_price_unavailable`.
|
||||||
|
- Asset decimals missing: block with `asset_decimals_missing`.
|
||||||
|
- Source inventory unavailable or stale: block before solver quote or command.
|
||||||
|
- Source spend exceeds inventory: block with a source-symbol-specific reason.
|
||||||
|
- Valuation route missing: asset remains visible as unvalued; no fake zero-value assumption.
|
||||||
|
- Old rows with BTC/EURe-only fields: dashboard remains readable through compatibility normalizers.
|
||||||
|
- Multiple enabled pairs: status and alerts show pair/route scope instead of one global active pair.
|
||||||
|
|
||||||
|
## Concrete Implementation Order
|
||||||
|
|
||||||
|
### Phase 1. Canonical helpers and route-rate model
|
||||||
|
- Add pair/route resolution helpers.
|
||||||
|
- Add generic route-rate extraction with compatibility fallback for current price payloads.
|
||||||
|
- Add tests for BTC/EUR and BTC/USDC route math.
|
||||||
|
|
||||||
|
### Phase 2. Strategy generic fields
|
||||||
|
- Update strategy decision and command payloads to use generic notional and route-relative direction.
|
||||||
|
- Preserve current nBTC/EURe behavior.
|
||||||
|
- Add nBTC/USDC strategy regression tests.
|
||||||
|
|
||||||
|
### Phase 3. Taker preflight
|
||||||
|
- Update request controller and request helpers to accept pair-native input.
|
||||||
|
- Compute min destination amount from generic route fields.
|
||||||
|
- Persist generic preflight fields.
|
||||||
|
- Add preflight regression tests.
|
||||||
|
|
||||||
|
### Phase 4. Outcome attribution
|
||||||
|
- Refactor maker and taker outcome attribution around expected deltas.
|
||||||
|
- Remove fixed BTC/EURe active-asset assumptions from refresh paths.
|
||||||
|
- Add BTC/USDC attribution tests and compatibility tests.
|
||||||
|
|
||||||
|
### Phase 5. Portfolio valuation visibility
|
||||||
|
- Add valued/unvalued tracked-asset model.
|
||||||
|
- Preserve current BTC/EURe/USDC valuation.
|
||||||
|
- Add tests for explicit unvalued tracked assets.
|
||||||
|
|
||||||
|
### Phase 6. Dashboard and status surfaces
|
||||||
|
- Update request form, lifecycle tables, strategy tables, funds rows, and status bar.
|
||||||
|
- Add dashboard tests proving labels come from DB assets and no EURe-to-BTC-only request UI remains.
|
||||||
|
- Build dashboard bundle.
|
||||||
|
|
||||||
|
### Phase 7. Alerts, ops scripts, deployment validation
|
||||||
|
- Make runtime health and alerts pair/route-aware.
|
||||||
|
- Update stale ops scripts or mark them inactive with tests.
|
||||||
|
- Run static deployment tests.
|
||||||
|
- Run full test suite.
|
||||||
|
- Deploy through repo workflow.
|
||||||
|
- Validate live dashboard bootstrap, service state, pair config, and current pair quote behavior.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Generic route-rate unit tests.
|
||||||
|
- Strategy nBTC/EURe compatibility tests.
|
||||||
|
- Strategy nBTC/USDC generic notional tests.
|
||||||
|
- Taker preflight pair-native tests.
|
||||||
|
- Taker preflight fail-closed tests.
|
||||||
|
- Outcome attribution generic expected-delta tests.
|
||||||
|
- Portfolio valued/unvalued tracked-asset tests.
|
||||||
|
- Dashboard request/lifecycle/funds label tests.
|
||||||
|
- Alert/runtime-health pair-set tests.
|
||||||
|
- Static deployment/config tests.
|
||||||
|
- Full `npm test`.
|
||||||
|
- Dashboard build.
|
||||||
|
|
||||||
|
## Validation Checklist Against The Proof
|
||||||
|
- nBTC/EURe current pair still loads from DB and uses 49 bps unless operator changed DB config.
|
||||||
|
- nBTC/USDC pair decisions use `notional_symbol=USDC` and do not display as EUR notional.
|
||||||
|
- Request preflight accepts `pair_id` plus `source_amount`.
|
||||||
|
- Request preflight for a non-EURe pair does not require `amount_eure`.
|
||||||
|
- Outcome attribution can complete a BTC/USDC row from matching inventory deltas.
|
||||||
|
- Dashboard has no EURe-to-BTC-only request title or settlement copy in active UI.
|
||||||
|
- Every tracked asset appears in balances with either value or unvalued reason.
|
||||||
|
- Alerts and state expose active pair/route scope.
|
||||||
|
- Deployment config still contains no trading asset/pair/edge env vars.
|
||||||
|
- Services deploy from repo push only.
|
||||||
|
|
||||||
|
## Known Fakes Allowed At Start Of This Turn
|
||||||
|
- Only BTC/EUR and BTC/USDC reference route adapters are real.
|
||||||
|
- Arbitrary non-BTC asset valuation remains unavailable unless a supported route is configured.
|
||||||
|
- NEAR Intents remains the only live venue.
|
||||||
|
- Venue-native terminal fills remain unavailable unless existing relay/status paths already provide them.
|
||||||
|
- Fee-complete realized PnL remains unavailable.
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Implementation Proof: Pair-native trade semantics and multi-asset outcome truth
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-18
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The repository must stop using BTC/EURe as the hidden shape of shared trading behavior.
|
||||||
|
|
||||||
|
Runtime services may still support only the currently implemented price-route adapters, but decisions, commands, taker request preflights, settlement attribution, dashboard surfaces, valuation visibility, and alerts must use DB asset and pair metadata as their canonical model. A configured nBTC/USDC pair must not be silently ignored, mislabeled as EUR notional, or made impossible to explain because a shared path expects EURe and BTC.
|
||||||
|
|
||||||
|
## Why this is the next architecture test
|
||||||
|
The previous turn moved asset catalogs, pair modes, edges, and limits into Postgres and added dashboard controls. That made it possible to configure more than the original nBTC/EURe pair, but it also exposed narrower shared paths:
|
||||||
|
|
||||||
|
- repo-created request preflight still speaks `amount_eure` and `eure_per_btc`
|
||||||
|
- outcome attribution still builds inventory deltas from one BTC asset and one EURe asset
|
||||||
|
- portfolio valuation now handles tracked USDC, but only as a special case
|
||||||
|
- dashboard copy, notional labels, and lifecycle text still assume BTC/EURe
|
||||||
|
- alert and health code still centers on a single active pair
|
||||||
|
|
||||||
|
This turn succeeds only if the canonical event and operator surfaces are pair-native enough that adding an enabled pair from the DB does not collapse back to the original BTC/EURe path.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Replace canonical `amount_eure`, `eure_notional`, and `max_notional_eure` usage in active runtime paths with pair-native fields:
|
||||||
|
- `source_amount`, `source_amount_units`, `source_asset_id`
|
||||||
|
- `destination_amount`, `destination_amount_units`, `destination_asset_id`
|
||||||
|
- `notional`, `notional_asset_id`, `notional_symbol`
|
||||||
|
- `max_notional`, `min_notional`
|
||||||
|
- Keep legacy aliases only where needed for old stored rows or backward-compatible API bodies; new logic must not depend on them.
|
||||||
|
- Make strategy price-route evaluation generic over `base_asset_id`, `quote_asset_id`, `quote_per_base`, and `base_per_quote` while preserving current BTC/EUR and BTC/USDC adapters.
|
||||||
|
- Make taker request preflight pair-native:
|
||||||
|
- accept `pair_id` or explicit `asset_in`/`asset_out`
|
||||||
|
- accept `source_amount` or `amount`
|
||||||
|
- resolve asset decimals, edge, limits, freshness policy, and route from DB pair config
|
||||||
|
- compute expected and minimum destination amount from the configured route
|
||||||
|
- fail closed when the pair, route, price, signer, inventory, or decimals are missing
|
||||||
|
- Make settlement attribution derive the required asset set and expected deltas from commands or preflights, not from fixed BTC/EURe parameters.
|
||||||
|
- Make dashboard labels, request forms, lifecycle rows, notional displays, and settlement text use DB asset symbols and pair config.
|
||||||
|
- Make portfolio and funds views list every tracked asset with either a computed valuation or an explicit unvalued reason.
|
||||||
|
- Make alert and health summaries use active pair/route sets rather than a single hardcoded `activePair` where the service is pair-scoped.
|
||||||
|
- Preserve existing nBTC/EURe behavior and the current 49 bps edge through migration.
|
||||||
|
- Preserve nBTC/Ethereum USDC maker trade capability when the DB pair has strategy config, inventory, and a fresh BTC/USDC price route.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
- Postgres remains the source of truth for assets, pairs, pair modes, strategy configs, and price routes.
|
||||||
|
- NEAR Intents remains the only live execution venue in this turn, but canonical records must carry venue and pair metadata so they are not NEAR/BTC/EURe shaped internally.
|
||||||
|
- Supported price-route adapters remain limited to current implemented sources: BTC/EUR and BTC/USDC reference routes.
|
||||||
|
- Broad arbitrary-asset pricing requires a later price graph or route-source turn.
|
||||||
|
- Live fund movement remains behind existing arming and explicit operator controls.
|
||||||
|
- Imported supported assets remain observe-only or inactive unless explicitly enabled by operator pair config.
|
||||||
|
|
||||||
|
## Turn-shaping rules
|
||||||
|
- Do not introduce new env vars for active assets, pairs, edges, or valuation behavior.
|
||||||
|
- Do not make every imported asset tradeable or valued by default.
|
||||||
|
- Do not silently drop tracked assets from portfolio, funding, dashboard, or attribution views because they lack a valuation route.
|
||||||
|
- Do not claim realized PnL or completed settlement without durable evidence.
|
||||||
|
- Do not widen live-funds risk while replacing field names and runtime semantics.
|
||||||
|
- Do not remove old-row compatibility unless the migration and tests prove no operator-visible history is lost.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No new execution venue.
|
||||||
|
- No arbitrary multi-hop price graph.
|
||||||
|
- No automatic pair discovery or automatic live trading enablement.
|
||||||
|
- No solver-liquidity guarantee for every supported token pair.
|
||||||
|
- No fee-complete realized PnL.
|
||||||
|
- No autonomous rebalancer or optimizer.
|
||||||
|
- No manual production fixes outside repo workflow.
|
||||||
|
|
||||||
|
## Required Operator Behavior
|
||||||
|
|
||||||
|
### Pair-native request truth
|
||||||
|
The operator must be able to create a preflight for a directed taker pair using DB pair config. The UI/API must show:
|
||||||
|
- selected pair id and mode
|
||||||
|
- source asset, destination asset, symbols, decimals, and venue
|
||||||
|
- source amount and source units
|
||||||
|
- configured price route, route freshness, and reference price id
|
||||||
|
- expected destination amount and minimum destination amount
|
||||||
|
- slippage bps, deadline, signer, verifier contract, and inventory check
|
||||||
|
- pair config id, pair config version, and edge bps used
|
||||||
|
- decisive blocked reason when the pair cannot safely create a request
|
||||||
|
|
||||||
|
### Pair-native decision and command truth
|
||||||
|
Every new decision and command must persist:
|
||||||
|
- pair id and venue
|
||||||
|
- pair config id and version
|
||||||
|
- edge bps
|
||||||
|
- source and destination asset ids
|
||||||
|
- asset decimals used
|
||||||
|
- price route id and reference price id
|
||||||
|
- direction relative to the configured route, not BTC/EURe-specific labels
|
||||||
|
- generic notional value and notional asset
|
||||||
|
- legacy notional aliases only as compatibility fields where needed
|
||||||
|
|
||||||
|
### Pair-native settlement truth
|
||||||
|
Completed settlement must be based on the expected source decrease and destination increase for the actual pair. The dashboard must show asset deltas using DB labels, for example `-100 USDC, +0.0012 BTC`, not hardcoded EURe decrease and BTC increase text.
|
||||||
|
|
||||||
|
### Portfolio and valuation truth
|
||||||
|
The dashboard must list every tracked asset. If an asset can be valued through the currently supported route set, it shows the value and valuation source. If not, it shows an explicit unvalued reason such as `valuation_route_missing` rather than disappearing from totals without explanation.
|
||||||
|
|
||||||
|
## Semantic Invariants
|
||||||
|
- Active runtime behavior must not require `tradingBtc` and `tradingEure` as the only asset roles.
|
||||||
|
- New decisions and commands must carry generic `notional`, `notional_asset_id`, and `max_notional`.
|
||||||
|
- Taker preflight must work from `pair_id` plus `source_amount`; `amount_eure` may be accepted only as a legacy alias.
|
||||||
|
- Outcome attribution must include assets from the expected pair deltas, including USDC, instead of a fixed BTC/EURe active-asset set.
|
||||||
|
- Dashboard request and lifecycle labels must come from DB asset metadata.
|
||||||
|
- Existing nBTC/EURe maker behavior must remain compatible.
|
||||||
|
- nBTC/USDC maker decisions must keep working from the DB BTC/USDC price route.
|
||||||
|
- Missing price route, stale price, unknown decimals, or missing inventory must fail closed.
|
||||||
|
- Tracked but unvalued assets must remain visible.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- Strategy uses a generic route-rate model for currently supported BTC/EUR and BTC/USDC routes.
|
||||||
|
- Decisions and commands persist generic notional fields and pair-native asset metadata.
|
||||||
|
- Taker preflight accepts pair-native inputs and no longer requires EURe/BTC-specific price fields.
|
||||||
|
- Settlement attribution derives inventory delta assets from commands/preflights and handles at least a BTC/USDC regression case.
|
||||||
|
- Portfolio/dashboard surfaces expose every tracked asset with value or explicit unvalued reason.
|
||||||
|
- Dashboard request creation, lifecycle, pair config, status, and settlement text no longer assume EURe-to-BTC.
|
||||||
|
- Runtime health and alerts are pair/route-aware enough not to describe the whole system as one active BTC/EURe pair.
|
||||||
|
- Deployment config remains free of trading asset/pair/edge env vars.
|
||||||
|
- Tests cover the generic path and current-pair compatibility.
|
||||||
|
- The result is deployed through repo workflow and validated against live service state.
|
||||||
|
|
||||||
|
## Validation Evidence Required
|
||||||
|
- Unit tests for generic exact-in route math using BTC/EUR and BTC/USDC fixtures.
|
||||||
|
- Strategy tests proving nBTC/EURe compatibility and nBTC/USDC generic notional fields.
|
||||||
|
- Taker preflight tests for pair-native request creation, route missing, stale route, insufficient source inventory, and legacy `amount_eure` alias handling.
|
||||||
|
- Outcome attribution tests proving a BTC/USDC command/request can be completed from inventory deltas.
|
||||||
|
- Portfolio tests proving tracked unvalued assets remain visible with `valuation_route_missing`.
|
||||||
|
- Dashboard tests proving request labels, lifecycle text, notional labels, and balance rows come from asset metadata.
|
||||||
|
- Alert/runtime-health tests proving pair-scoped stale route and active pair set handling.
|
||||||
|
- Static deployment test proving pair/asset/edge env vars remain absent.
|
||||||
|
- Live dashboard/bootstrap evidence after deploy showing nBTC/EURe and nBTC/USDC pair config without EURe-only request labeling.
|
||||||
|
|
||||||
|
## Failure Conditions
|
||||||
|
- A non-EURe pair cannot be preflighted because request code requires `amount_eure` or `eure_per_btc`.
|
||||||
|
- A BTC/USDC submission cannot be attributed because only BTC/EURe inventory deltas are considered.
|
||||||
|
- Dashboard labels a USDC notional as EUR or hides a tracked USDC balance.
|
||||||
|
- A tracked asset is omitted from funds/portfolio because it lacks valuation.
|
||||||
|
- A service still needs asset, pair, or edge env vars for trading behavior.
|
||||||
|
- Missing DB pair config, price route, or decimals leads to trading instead of a blocked state.
|
||||||
|
- nBTC/EURe behavior or 49 bps compatibility regresses.
|
||||||
|
|
||||||
|
## Current Real Before This Turn
|
||||||
|
- DB-backed assets, import runs, pair modes, strategy configs, and pair controls exist.
|
||||||
|
- The 1Click token catalog import has been run and contains 163 known assets.
|
||||||
|
- Current nBTC/EURe behavior and 49 bps DB config have been preserved.
|
||||||
|
- nBTC/Ethereum USDC can be configured with a BTC/USDC reference route, but several shared paths still use BTC/EURe assumptions.
|
||||||
|
- Dashboard can show known assets, tracked balances, pair config, and deposit addresses.
|
||||||
|
|
||||||
|
## Deliberately Not Built By This Proof
|
||||||
|
- New venue integration beyond NEAR Intents.
|
||||||
|
- General price graph for arbitrary supported assets.
|
||||||
|
- Fee-complete realized PnL.
|
||||||
|
- Venue-native terminal fill ingestion beyond existing evidence paths.
|
||||||
|
- Autonomous portfolio optimization or rebalancing.
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
# Implementation Turn: Maker response latency and quote competitiveness
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-18
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Measure and reduce maker quote-response latency after pair-native routing so nBTC/USDC responses are only sent when they are competitively viable and every missed response has durable timing evidence.
|
||||||
|
|
||||||
|
## Selected backlog items
|
||||||
|
- none selected; this turn was opened directly from the operator request on 2026-05-18.
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
- DB asset, pair, route, and strategy config remain canonical.
|
||||||
|
- No active pair, edge, notional, latency, or response-policy env vars.
|
||||||
|
- Existing nBTC/EURe behavior and 49 bps compatibility must survive.
|
||||||
|
- nBTC/USDC is the key regression pair.
|
||||||
|
- Missing pair config, missing strategy config, missing timing prerequisites, stale price, stale inventory, or invalid policy must fail closed or skip response explicitly.
|
||||||
|
- New response policy may reduce responses; it must not loosen edge, notional, inventory, arming, pair enablement, or signer checks.
|
||||||
|
- Relay acceptance remains submission evidence only.
|
||||||
|
- Deploy only through repo workflow.
|
||||||
|
|
||||||
|
## Problem statement
|
||||||
|
The previous turn removed BTC/EURe assumptions and then instrumented and optimized the executor hot path. Live evidence after verifier salt caching shows local salt lookup is no longer the normal bottleneck:
|
||||||
|
|
||||||
|
- cache-hit failed submissions are often around 45 ms p50 total
|
||||||
|
- cache-hit accepted responses are often around 86 ms p50 total
|
||||||
|
- salt timing is near 0 ms on cache hits
|
||||||
|
- relay errors such as `quote not found or already finished` still dominate
|
||||||
|
|
||||||
|
The system now needs durable timing and competitiveness truth across the full maker path. Without it, the operator cannot tell whether nBTC/USDC fails because incoming quotes are already old, because relay round trip is still too slow, because certain request kinds/notional sizes are unwinnable, or because accepted responses simply do not settle.
|
||||||
|
|
||||||
|
## Backend changes
|
||||||
|
|
||||||
|
### 1. End-to-end maker timing model
|
||||||
|
- Define a normalized maker timing object carried by quotes, decisions, commands, results, and lifecycle rows where available:
|
||||||
|
- `quote_observed_at`
|
||||||
|
- `quote_received_at`
|
||||||
|
- `quote_normalized_at`
|
||||||
|
- `quote_published_at`
|
||||||
|
- `strategy_received_at`
|
||||||
|
- `strategy_decided_at`
|
||||||
|
- `command_published_at`
|
||||||
|
- `executor_received_at`
|
||||||
|
- `relay_result_at`
|
||||||
|
- `outcome_observed_at`
|
||||||
|
- `quote_to_decision_ms`
|
||||||
|
- `decision_to_command_ms`
|
||||||
|
- `command_to_executor_ms`
|
||||||
|
- `executor_to_relay_result_ms`
|
||||||
|
- `quote_to_relay_result_ms`
|
||||||
|
- `quote_to_outcome_ms`
|
||||||
|
- Use in-process monotonic timers where one process owns both timestamps.
|
||||||
|
- Use wall-clock timestamps for cross-service timing and mark impossible/negative values as unavailable instead of silently reporting misleading latency.
|
||||||
|
- Preserve the existing `executor_timing` payload and extend it rather than replacing it.
|
||||||
|
- Keep old rows readable when new timing fields are absent.
|
||||||
|
|
||||||
|
### 2. Quote ingress and normalization timestamps
|
||||||
|
- Inspect the NEAR Intents ingest path and normalized swap-demand event builder.
|
||||||
|
- Add timestamps at the earliest reliable point where the raw quote frame is received.
|
||||||
|
- Carry quote receive/publish timestamps into normalized quote payloads.
|
||||||
|
- Ensure history writer persists the timing fields without requiring a schema rewrite.
|
||||||
|
- Add tests proving raw/normalized timing survives quote normalization and durable history insertion.
|
||||||
|
|
||||||
|
### 3. Strategy and command timing propagation
|
||||||
|
- Update strategy decision payloads to carry quote timing metadata and strategy receipt/decision timestamps.
|
||||||
|
- Update execute command payloads to carry:
|
||||||
|
- quote timing metadata
|
||||||
|
- strategy timing metadata
|
||||||
|
- command publish timestamp
|
||||||
|
- pair id, route id, request kind, notional symbol, and notional bucket inputs needed for aggregation
|
||||||
|
- Ensure strategy rejection decisions also persist quote age and policy context.
|
||||||
|
- Add tests for nBTC/EURe compatibility and nBTC/USDC timing propagation.
|
||||||
|
|
||||||
|
### 4. Executor timing extension
|
||||||
|
- Extend executor result payloads with:
|
||||||
|
- executor received timestamp
|
||||||
|
- quote age at executor receipt
|
||||||
|
- quote age at relay result
|
||||||
|
- salt source and salt age from the existing verifier salt cache
|
||||||
|
- relay result timing and relay error text
|
||||||
|
- Keep executor duplicate detection and stale-command rejection distinct from strategy/policy skips.
|
||||||
|
- Add tests proving executor timing is present on submitted, failed, stale, and disarmed paths where a result is emitted.
|
||||||
|
|
||||||
|
### 5. Relay failure classification
|
||||||
|
- Normalize relay submission failures into durable categories without losing the original error:
|
||||||
|
- `quote_not_found_or_finished`
|
||||||
|
- `relay_timeout`
|
||||||
|
- `relay_disconnected`
|
||||||
|
- `signing_failed`
|
||||||
|
- `salt_unavailable`
|
||||||
|
- `unknown_submission_failure`
|
||||||
|
- Keep `result_code=submission_failed` compatibility where needed, but add a more specific `failure_category`.
|
||||||
|
- Add tests for known relay error strings and old-row compatibility.
|
||||||
|
|
||||||
|
### 6. Competitiveness aggregation
|
||||||
|
- Add shared aggregation helpers for recent maker response competitiveness:
|
||||||
|
- counts by pair, direction, request kind, result code, and failure category
|
||||||
|
- latency percentiles by stage
|
||||||
|
- quote-age buckets at decision, executor receipt, and relay result
|
||||||
|
- accepted response rate by age bucket
|
||||||
|
- not-filled/completed/submitted outcome rate by age bucket where outcome data exists
|
||||||
|
- notional buckets per notional asset
|
||||||
|
- Prefer deriving summaries from durable DB rows for dashboard bootstrap.
|
||||||
|
- Keep live WebSocket updates incremental enough that the dashboard does not require full refresh for every quote.
|
||||||
|
- Add unit tests for aggregation fixtures covering nBTC/EURe and nBTC/USDC.
|
||||||
|
|
||||||
|
### 7. DB-backed response-age policy
|
||||||
|
- After the timing fields and summaries exist, add a conservative pair-scoped maker response-age policy if evidence supports it.
|
||||||
|
- Store policy in DB-owned strategy config or an adjacent DB config table, not env vars.
|
||||||
|
- Candidate fields:
|
||||||
|
- `maker_max_quote_age_ms`
|
||||||
|
- `maker_max_quote_age_enabled`
|
||||||
|
- `maker_latency_policy_reason`
|
||||||
|
- config version and updated timestamp
|
||||||
|
- Default behavior must preserve current nBTC/EURe unless a stricter policy is explicitly configured.
|
||||||
|
- For nBTC/USDC, configure only a skip policy that reduces stale/unwinnable submissions; do not reduce edge or increase notional.
|
||||||
|
- When policy skips a quote, persist a strategy rejection/block with:
|
||||||
|
- `decision=blocked` or existing compatible rejection vocabulary
|
||||||
|
- `decision_reason=maker_quote_too_old`
|
||||||
|
- measured quote age
|
||||||
|
- configured max age
|
||||||
|
- pair config id/version
|
||||||
|
- route id/reference price id
|
||||||
|
- Add tests proving:
|
||||||
|
- old quotes are skipped before command emission
|
||||||
|
- fresh quotes continue through the existing checks
|
||||||
|
- missing/invalid policy does not create a global responder
|
||||||
|
- policy is pair-scoped
|
||||||
|
- skip rows appear in lifecycle without being labeled relay failures
|
||||||
|
|
||||||
|
### 8. Dashboard and operator surfaces
|
||||||
|
- Add a maker competitiveness section to the Strategy page or System page, whichever matches existing layout best after inspection.
|
||||||
|
- Show:
|
||||||
|
- pair/direction/request-kind filters
|
||||||
|
- accepted vs failed relay responses
|
||||||
|
- `quote_not_found_or_finished` counts
|
||||||
|
- p50/p90/p99 quote-to-relay result timing
|
||||||
|
- salt source distribution
|
||||||
|
- age-bucket outcome table
|
||||||
|
- latest exact relay errors with quote ids
|
||||||
|
- policy skip counts and reasons
|
||||||
|
- Extend existing lifecycle details to show the full timing waterfall for a selected quote.
|
||||||
|
- Avoid hardcoded EURe/BTC labels; use pair-native symbols already exposed by the previous turn.
|
||||||
|
- Add dashboard static and data tests.
|
||||||
|
|
||||||
|
### 9. Runtime health and alerts
|
||||||
|
- Add a pair-scoped warning when a configured maker pair has high recent stale/already-finished response rate after enough sample volume.
|
||||||
|
- Keep alerts informational/warning by default; do not auto-pause or auto-disarm unless an existing critical safety invariant is broken.
|
||||||
|
- Ensure ops-sentinel/service health summaries include enough timing state to diagnose stale response pressure by pair.
|
||||||
|
- Add tests for pair-scoped high-failure summaries and no global active-pair regression.
|
||||||
|
|
||||||
|
### 10. Deployment and live validation
|
||||||
|
- Run targeted tests for timing propagation, failure classification, aggregation, policy skip behavior, dashboard surfaces, and config/static deployment invariants.
|
||||||
|
- Run full `npm test`.
|
||||||
|
- Build the operator dashboard bundle.
|
||||||
|
- Deploy only through repo workflow.
|
||||||
|
- After deploy, collect live evidence:
|
||||||
|
- timing distribution before/after policy if a policy was enabled
|
||||||
|
- nBTC/USDC response counts by result category
|
||||||
|
- policy skip counts
|
||||||
|
- relay accepted response rate by age bucket
|
||||||
|
- outcome status distribution for accepted responses
|
||||||
|
|
||||||
|
## Data and persistence
|
||||||
|
- Prefer additive JSON payload fields and derived summaries over destructive schema changes.
|
||||||
|
- If a schema change is needed for policy config, make it idempotent and preserve old rows.
|
||||||
|
- Keep raw and normalized event payloads replayable.
|
||||||
|
- Store original relay error messages alongside normalized categories.
|
||||||
|
- Keep timing fields nullable for old history and unavailable timestamps.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
- Quote has no observed timestamp: fail closed for age-based policy or mark timing unavailable; do not invent zero age.
|
||||||
|
- Cross-service clock skew yields negative duration: expose timing as unavailable with a reason.
|
||||||
|
- Strategy rejects before executor: lifecycle shows strategy/policy skip, not relay failure.
|
||||||
|
- Executor duplicate skip: does not count as a fresh relay response.
|
||||||
|
- Salt cache stale/unavailable: signing fails closed with `salt_unavailable` or existing compatible failure plus category.
|
||||||
|
- Relay disconnected or timeout: category is distinct from `quote_not_found_or_finished`.
|
||||||
|
- Pair is observe-only or disabled: no response policy makes it tradeable.
|
||||||
|
- Multiple maker-enabled pairs: summaries and alerts remain pair-scoped.
|
||||||
|
- Old BTC/EURe rows without timing remain readable.
|
||||||
|
|
||||||
|
## Concrete implementation order
|
||||||
|
|
||||||
|
### Phase 1. Codebase inspection and timing contracts
|
||||||
|
- Inspect current NEAR Intents ingest, normalized quote, strategy decision, command, executor result, history writer, dashboard lifecycle, and ops-sentinel paths.
|
||||||
|
- Write the canonical timing-field contract and helper functions.
|
||||||
|
- Add low-level tests for duration calculation and clock-skew handling.
|
||||||
|
|
||||||
|
### Phase 2. Timing propagation
|
||||||
|
- Add quote ingress timestamps.
|
||||||
|
- Propagate timing through strategy decisions and commands.
|
||||||
|
- Extend executor timing with quote age fields.
|
||||||
|
- Add propagation tests and old-row compatibility tests.
|
||||||
|
|
||||||
|
### Phase 3. Failure classification and aggregation
|
||||||
|
- Normalize relay failure categories.
|
||||||
|
- Build durable/live competitiveness summary helpers.
|
||||||
|
- Add nBTC/EURe and nBTC/USDC aggregation tests.
|
||||||
|
|
||||||
|
### Phase 4. Operator UI
|
||||||
|
- Add timing waterfall to lifecycle details.
|
||||||
|
- Add pair-native competitiveness summary with filters and age buckets.
|
||||||
|
- Add dashboard tests and build the bundle.
|
||||||
|
|
||||||
|
### Phase 5. Policy gate
|
||||||
|
- Add DB-backed pair-scoped `maker_max_quote_age_ms` policy only after summaries exist.
|
||||||
|
- Wire policy into strategy before command emission.
|
||||||
|
- Persist visible skip reasons.
|
||||||
|
- Add fail-closed and pair-scoped policy tests.
|
||||||
|
|
||||||
|
### Phase 6. Alerts and validation
|
||||||
|
- Add pair-scoped high stale/already-finished response warning.
|
||||||
|
- Run targeted tests, full `npm test`, and dashboard build.
|
||||||
|
- Deploy through repo workflow.
|
||||||
|
- Validate live timing and response distributions after rollout.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
- Timing helper unit tests.
|
||||||
|
- Quote normalization timing tests.
|
||||||
|
- Strategy timing propagation tests.
|
||||||
|
- Executor timing extension tests.
|
||||||
|
- Relay failure classification tests.
|
||||||
|
- Competitiveness aggregation tests for nBTC/EURe and nBTC/USDC.
|
||||||
|
- Response-age policy tests:
|
||||||
|
- old quote skipped
|
||||||
|
- fresh quote allowed
|
||||||
|
- missing policy does not loosen behavior
|
||||||
|
- invalid policy fails closed
|
||||||
|
- pair-scoped behavior
|
||||||
|
- Dashboard lifecycle and competitiveness UI tests.
|
||||||
|
- Runtime-health/alert pair-scope tests.
|
||||||
|
- Static deployment tests proving no new pair/latency env vars.
|
||||||
|
- Full `npm test`.
|
||||||
|
- Dashboard build.
|
||||||
|
|
||||||
|
## Validation checklist against the proof
|
||||||
|
- A selected nBTC/USDC quote can be explained from quote ingress to relay result with durable timing fields.
|
||||||
|
- `quote not found or already finished` is categorized and shown with exact error text.
|
||||||
|
- Dashboard summarizes accepted/failed/skipped responses by pair, direction, request kind, and age bucket.
|
||||||
|
- Policy skips, if enabled, are visible as strategy/policy skips and not relay failures.
|
||||||
|
- nBTC/EURe behavior and 49 bps compatibility remain intact.
|
||||||
|
- No new live trading is enabled by import or env vars.
|
||||||
|
- Repo workflow deploys the result without manual cluster reconciliation.
|
||||||
|
|
||||||
|
## Known fakes allowed at start of this turn
|
||||||
|
- Fee-complete realized PnL remains unavailable.
|
||||||
|
- Venue-native terminal fill ids remain unavailable unless existing relay/status evidence exposes them.
|
||||||
|
- Arbitrary low-latency infrastructure, multi-region placement, and multi-executor racing are not built.
|
||||||
|
- Quote competitiveness is initially observational until the timing data supports a concrete skip policy.
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
# Implementation Proof: Maker response latency and quote competitiveness
|
||||||
|
|
||||||
|
Status: open
|
||||||
|
Opened: 2026-05-18
|
||||||
|
|
||||||
|
## Target outcome
|
||||||
|
The maker path must explain why nBTC/USDC quote responses are accepted, rejected by the relay, or never become settled trades.
|
||||||
|
|
||||||
|
After the pair-native turn, the local executor hot path is fast enough that blind optimization is no longer justified. This turn succeeds when the repository records an end-to-end timing waterfall for each responded quote, exposes pair/request-kind competitiveness metrics to the operator, and uses DB-backed response policy to skip quotes that are already too old or otherwise empirically unwinnable.
|
||||||
|
|
||||||
|
## Why this is the next architecture test
|
||||||
|
Live evidence after verifier salt caching showed cache-hit executor results around:
|
||||||
|
|
||||||
|
- `submission_failed`: p50 total near 45 ms, p90 near 55 ms, p50 salt near 0 ms
|
||||||
|
- `quote_response_ok`: p50 total near 86 ms, p90 near 141 ms, p50 salt near 0 ms
|
||||||
|
- occasional blocking salt refreshes still exist, but most local salt latency is gone
|
||||||
|
|
||||||
|
Despite that, nBTC/USDC still produces many relay errors such as `quote not found or already finished`, and accepted relay responses are not reliably becoming durable settlement evidence.
|
||||||
|
|
||||||
|
The next question is not "can the code answer quotes at all"; it is whether each response is competitive at the moment we answer it, and whether the bot should skip quotes whose measured age and route characteristics make them unlikely to fill.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Add durable, pair-native maker timing fields from quote ingress through strategy, command, executor signing, relay response, and outcome attribution.
|
||||||
|
- Preserve existing executor timing fields and extend them with quote-age and stage timing that can be aggregated across services.
|
||||||
|
- Classify relay responses and failures by:
|
||||||
|
- pair id
|
||||||
|
- source/destination assets
|
||||||
|
- request kind
|
||||||
|
- notional asset and notional bucket
|
||||||
|
- route id/reference price id
|
||||||
|
- quote age at decision, command, executor receipt, and relay response
|
||||||
|
- relay result code and exact relay error message
|
||||||
|
- eventual outcome status when available
|
||||||
|
- Add operator-visible competitiveness metrics for nBTC/EURe and nBTC/USDC:
|
||||||
|
- response counts
|
||||||
|
- accepted response rate
|
||||||
|
- stale/already-finished response rate
|
||||||
|
- p50/p90/p99 stage latency
|
||||||
|
- quote age bucket outcomes
|
||||||
|
- pair/direction/request-kind breakdowns
|
||||||
|
- Add a DB-backed maker response-age policy only if evidence from the new timing data supports it.
|
||||||
|
- Keep response-age policy pair-scoped and fail closed:
|
||||||
|
- missing pair config must not create a default live responder
|
||||||
|
- missing or invalid max-age config must not loosen behavior
|
||||||
|
- age cutoffs may skip responses but must not reduce edge or raise notional limits
|
||||||
|
- Preserve the existing nBTC/EURe behavior unless the operator explicitly configures a stricter pair policy.
|
||||||
|
- Keep nBTC/USDC as the key regression pair.
|
||||||
|
- Deploy only through the repo workflow.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
- NEAR Intents quote availability can expire or be consumed independently of our local execution speed.
|
||||||
|
- The relay error `quote not found or already finished` is ambiguous; it may mean quote expiry, competing solver completion, or a relay-side state transition.
|
||||||
|
- Wall-clock timestamps across pods are good enough for operator timing ranges, but in-process monotonic timings remain the source of truth for sub-step latency.
|
||||||
|
- The active proof may reduce responses to empirically stale quotes, but it must not increase live-funds exposure, notional size, edge looseness, or automatic pair enablement.
|
||||||
|
- Postgres remains the canonical runtime source for pair and strategy config.
|
||||||
|
|
||||||
|
## Turn-shaping rules
|
||||||
|
- Do not add new env vars for latency policy, pair selection, or trade sizing.
|
||||||
|
- Do not relax edge thresholds, notional limits, inventory checks, arming, or pair enablement.
|
||||||
|
- Do not create new live trading by import or by default policy rows.
|
||||||
|
- Do not treat relay acceptance as a completed trade.
|
||||||
|
- Do not claim quote competitiveness from sampled logs alone; persist the fields needed to recompute it.
|
||||||
|
- Do not do manual deployment or cluster repair outside the repo workflow.
|
||||||
|
- If a policy change skips responses, the dashboard must show the skip reason explicitly.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
- No new execution venue.
|
||||||
|
- No geographic relocation, paid low-latency infrastructure, or permanent infra spend.
|
||||||
|
- No multi-replica executor race strategy.
|
||||||
|
- No response flooding or duplicate submissions.
|
||||||
|
- No automatic edge reduction to win more quotes.
|
||||||
|
- No fee-complete realized PnL.
|
||||||
|
- No venue-native terminal fill integration beyond existing evidence paths.
|
||||||
|
|
||||||
|
## Required operator behavior
|
||||||
|
|
||||||
|
### Quote timing truth
|
||||||
|
For a quote that reaches strategy and executor, the operator must be able to see:
|
||||||
|
|
||||||
|
- when the quote was received from the relay websocket
|
||||||
|
- when it was normalized and persisted
|
||||||
|
- when strategy decided
|
||||||
|
- when the execute command was emitted
|
||||||
|
- when executor received it
|
||||||
|
- salt source and salt age
|
||||||
|
- signing time
|
||||||
|
- relay response time
|
||||||
|
- total quote-to-relay-result time
|
||||||
|
- final relay result or error message
|
||||||
|
- eventual outcome status when available
|
||||||
|
|
||||||
|
### Competitiveness truth
|
||||||
|
The operator must be able to answer:
|
||||||
|
|
||||||
|
- which pair/direction/request-kind is producing response failures
|
||||||
|
- whether failures cluster by quote age, notional bucket, relay latency, or route
|
||||||
|
- whether accepted responses have materially different timing than failed responses
|
||||||
|
- whether nBTC/USDC is losing because local path latency is too high, because relay responses are late, or because incoming quotes are already stale
|
||||||
|
|
||||||
|
### Response policy truth
|
||||||
|
If the system skips a maker response because the quote is too old or otherwise blocked by response policy, the decision must be durable and visible as a strategy rejection/block reason such as `maker_quote_too_old`, including the measured age, configured max age, pair id, and config version.
|
||||||
|
|
||||||
|
## Semantic invariants
|
||||||
|
- Pair-native fields remain canonical; no BTC/EURe-only latency or policy path is reintroduced.
|
||||||
|
- Missing DB config, missing pair config, missing timing prerequisites, or invalid policy values fail closed.
|
||||||
|
- A skipped response is not a failed relay submission; lifecycle views must keep strategy/policy skips distinct from relay failures.
|
||||||
|
- Relay acceptance remains submission evidence only, not settlement or realized profit.
|
||||||
|
- Response policy must be pair-scoped and versioned with DB strategy config or another DB-owned config row.
|
||||||
|
- Existing nBTC/EURe compatibility and 49 bps edge behavior must survive.
|
||||||
|
- nBTC/USDC must remain observable even when the policy skips responses.
|
||||||
|
|
||||||
|
## Definition of done
|
||||||
|
- New maker timing fields are persisted on quote, decision, command, result, and/or derived lifecycle records without relying on logs.
|
||||||
|
- Dashboard and API surfaces expose pair-scoped latency and competitiveness summaries.
|
||||||
|
- Relay errors such as `quote not found or already finished` are visible with timing context.
|
||||||
|
- A DB-backed response-age policy exists if supported by the measured data, and skipped responses are durable, visible, and tested.
|
||||||
|
- The policy cannot loosen live trading behavior or invent pair defaults.
|
||||||
|
- Tests cover timing propagation, dashboard summaries, policy skip behavior, old-row compatibility, and nBTC/USDC regressions.
|
||||||
|
- Full `npm test` and dashboard build pass.
|
||||||
|
- The result is deployed through the repo workflow and validated against live post-deploy timing data.
|
||||||
|
|
||||||
|
## Validation evidence required
|
||||||
|
- Unit tests for timing-field propagation from normalized quote to strategy decision, command, executor result, and lifecycle row.
|
||||||
|
- Tests proving relay failure messages remain visible with quote age and stage timing.
|
||||||
|
- Tests proving nBTC/USDC rows aggregate by pair, direction, request kind, and result code.
|
||||||
|
- Tests proving response-age policy skips old quotes without calling the executor or relay.
|
||||||
|
- Tests proving missing/invalid DB policy fails closed or preserves existing behavior according to the explicit config contract.
|
||||||
|
- Dashboard tests proving the competitiveness view shows timing buckets and skip reasons without hardcoded EURe/BTC labels.
|
||||||
|
- Static deployment/config tests proving no latency or pair policy env vars are added.
|
||||||
|
- Full `npm test`.
|
||||||
|
- Dashboard bundle build.
|
||||||
|
- Live evidence after deploy comparing at least pre-policy timing distribution and post-policy skip/accept/failure distribution.
|
||||||
|
|
||||||
|
## Failure conditions
|
||||||
|
- nBTC/USDC quote failures still cannot be explained beyond a relay error string.
|
||||||
|
- Timing metrics exist only in logs and cannot be recomputed from durable rows.
|
||||||
|
- The dashboard merges strategy/policy skips with relay submission failures.
|
||||||
|
- A latency policy applies globally when it should be pair-scoped.
|
||||||
|
- Missing DB config causes live responses instead of blocked behavior.
|
||||||
|
- The implementation relaxes edge, notional, inventory, arming, or pair enablement.
|
||||||
|
- A service requires new active pair or latency env vars to behave correctly.
|
||||||
|
- Existing nBTC/EURe behavior regresses.
|
||||||
|
|
||||||
|
## Current real before this turn
|
||||||
|
- Pair-native asset, pair, strategy, route, preflight, attribution, valuation, dashboard, alert, and ops paths have been implemented and deployed.
|
||||||
|
- nBTC/USDC maker decisions and result rows are visible from DB pair and route config.
|
||||||
|
- Executor timing is already persisted with salt, signing, relay, and total timings.
|
||||||
|
- Verifier salt caching has removed normal per-command salt RPC latency from the hot path.
|
||||||
|
- Live cache-hit executor totals are often below 100 ms, but `quote not found or already finished` remains common.
|
||||||
|
- Durable settlement truth still depends on observed inventory/outcome evidence, not relay acceptance alone.
|
||||||
|
|
||||||
|
## Deliberately not built by this proof
|
||||||
|
- Fee-complete realized PnL.
|
||||||
|
- Venue-native terminal fill ids if the current relay/status paths do not expose them.
|
||||||
|
- Arbitrary multi-venue or cross-region execution.
|
||||||
|
- General solver-competition modeling beyond observed quote/response/outcome data.
|
||||||
54
check-matches-small.mjs
Normal file
54
check-matches-small.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
// Wait a bit for connection
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
console.log("Fetching submissions...");
|
||||||
|
const expectedDeltasRows = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
COALESCE(observed_at, ingested_at) as submitted_at,
|
||||||
|
payload->>'expected_inventory_delta' as expected_inventory_delta
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 2000
|
||||||
|
`);
|
||||||
|
const submissions = expectedDeltasRows.rows.map(r => ({
|
||||||
|
quote_id: r.quote_id,
|
||||||
|
submitted_at: r.submitted_at,
|
||||||
|
expected_inventory_delta: typeof r.expected_inventory_delta === 'string' && r.expected_inventory_delta ? JSON.parse(r.expected_inventory_delta) : r.expected_inventory_delta
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("Fetching inventory...", submissions.length);
|
||||||
|
const inventoryRows = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
event_id as inventory_id,
|
||||||
|
observed_at,
|
||||||
|
payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 5000
|
||||||
|
`);
|
||||||
|
const inventorySnapshots = inventoryRows.rows.map(r => ({
|
||||||
|
inventory_id: r.inventory_id,
|
||||||
|
observed_at: r.observed_at,
|
||||||
|
asset_balances: r.payload?.asset_balances || {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("Checking for matches...", submissions.length, inventorySnapshots.length);
|
||||||
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
submissions,
|
||||||
|
inventorySnapshots,
|
||||||
|
});
|
||||||
|
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found", completed.length, "completed trades out of", records.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
49
check-matches.mjs
Normal file
49
check-matches.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
const expectedDeltasRows = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
COALESCE(observed_at, ingested_at) as submitted_at,
|
||||||
|
payload->>'expected_inventory_delta' as expected_inventory_delta
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 15000
|
||||||
|
`);
|
||||||
|
const submissions = expectedDeltasRows.rows.map(r => ({
|
||||||
|
quote_id: r.quote_id,
|
||||||
|
submitted_at: r.submitted_at,
|
||||||
|
expected_inventory_delta: typeof r.expected_inventory_delta === 'string' && r.expected_inventory_delta ? JSON.parse(r.expected_inventory_delta) : r.expected_inventory_delta
|
||||||
|
}));
|
||||||
|
|
||||||
|
const inventoryRows = await pool.query(`
|
||||||
|
SELECT
|
||||||
|
event_id as inventory_id,
|
||||||
|
observed_at,
|
||||||
|
payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 50000
|
||||||
|
`);
|
||||||
|
const inventorySnapshots = inventoryRows.rows.map(r => ({
|
||||||
|
inventory_id: r.inventory_id,
|
||||||
|
observed_at: r.observed_at,
|
||||||
|
asset_balances: r.payload?.asset_balances || {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("Checking for matches...", submissions.length, inventorySnapshots.length);
|
||||||
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
submissions,
|
||||||
|
inventorySnapshots,
|
||||||
|
});
|
||||||
|
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found", completed.length, "completed trades out of", records.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
23
debug-pod.yaml
Normal file
23
debug-pod.yaml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: debug-disk
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
hostPID: true
|
||||||
|
hostNetwork: true
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: shell
|
||||||
|
image: alpine
|
||||||
|
command: ["/bin/sh", "-c", "df -h /host; du -sh /host/var/lib/rancher/k3s/storage/* 2>/dev/null; sleep 3600"]
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: host-root
|
||||||
|
mountPath: /host
|
||||||
|
volumes:
|
||||||
|
- name: host-root
|
||||||
|
hostPath:
|
||||||
|
path: /
|
||||||
BIN
docs/._system-architecture.md
Executable file
BIN
docs/._system-architecture.md
Executable file
Binary file not shown.
982
docs/system-architecture.md
Normal file
982
docs/system-architecture.md
Normal file
|
|
@ -0,0 +1,982 @@
|
||||||
|
# Current System Architecture
|
||||||
|
|
||||||
|
This document explains the current `unrip` runtime at a high level:
|
||||||
|
|
||||||
|
- which components exist
|
||||||
|
- what responsibilities each component owns
|
||||||
|
- which signals move through Kafka/Redpanda
|
||||||
|
- what gets persisted
|
||||||
|
- which attributes are available in the stored data
|
||||||
|
- what kinds of analysis and operator workflows the data supports today
|
||||||
|
|
||||||
|
The system is intentionally bus-first and data-first:
|
||||||
|
|
||||||
|
- components are separate services
|
||||||
|
- the core trading flow moves through Kafka-compatible topics
|
||||||
|
- PostgreSQL is the durable analytical and operator history store
|
||||||
|
- some safety-critical operational state is also persisted in local/PVC-backed JSON files
|
||||||
|
- the browser reads only the operator dashboard backend, not Kafka or PostgreSQL directly
|
||||||
|
|
||||||
|
## Design Rules
|
||||||
|
|
||||||
|
The current architecture follows these rules:
|
||||||
|
|
||||||
|
- venue inputs, strategy, execution, alerting, persistence, and the dashboard are separate services
|
||||||
|
- the core trading path is event-driven, not direct service-to-service RPC
|
||||||
|
- raw and normalized events are both preserved
|
||||||
|
- decisions and execution attempts are auditable after the fact
|
||||||
|
- execution safety depends on persisted idempotency state, not Kafka consumer groups alone
|
||||||
|
|
||||||
|
## System At A Glance
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph External Sources
|
||||||
|
NearWS["NEAR Intents solver relay websocket"]
|
||||||
|
RefData["Kraken + CoinGecko"]
|
||||||
|
Bridge["Bridge RPC"]
|
||||||
|
Verifier["Verifier / NEAR RPC"]
|
||||||
|
BtcObs["BTC address observer"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Runtime Services
|
||||||
|
Ingest["near-intents-ingest"]
|
||||||
|
RefIngest["market-reference-ingest"]
|
||||||
|
Liquidity["liquidity-manager"]
|
||||||
|
Inventory["inventory-sync"]
|
||||||
|
Strategy["strategy-engine"]
|
||||||
|
Executor["trade-executor"]
|
||||||
|
Sentinel["ops-sentinel"]
|
||||||
|
History["history-writer"]
|
||||||
|
Dashboard["operator-dashboard"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Bus[("Redpanda / Kafka")]
|
||||||
|
Postgres[("PostgreSQL")]
|
||||||
|
Browser["Operator browser"]
|
||||||
|
|
||||||
|
NearWS --> Ingest
|
||||||
|
RefData --> RefIngest
|
||||||
|
Bridge --> Liquidity
|
||||||
|
Verifier --> Liquidity
|
||||||
|
BtcObs --> Liquidity
|
||||||
|
Verifier --> Inventory
|
||||||
|
Bridge --> Inventory
|
||||||
|
|
||||||
|
Ingest -->|"raw.near_intents.quote"| Bus
|
||||||
|
Ingest -->|"norm.swap_demand"| Bus
|
||||||
|
RefIngest -->|"ref.market_price"| Bus
|
||||||
|
Liquidity -->|"ops.liquidity_action"| Bus
|
||||||
|
Liquidity -->|"ops.funding_observation"| Bus
|
||||||
|
Inventory -->|"state.intent_inventory"| Bus
|
||||||
|
Strategy -->|"decision.trade_decision"| Bus
|
||||||
|
Strategy -->|"cmd.execute_trade"| Bus
|
||||||
|
Executor -->|"exec.trade_result"| Bus
|
||||||
|
Sentinel -->|"ops.alert"| Bus
|
||||||
|
|
||||||
|
Bus --> Inventory
|
||||||
|
Bus --> Strategy
|
||||||
|
Bus --> Executor
|
||||||
|
Bus --> Sentinel
|
||||||
|
Bus --> History
|
||||||
|
Bus --> Dashboard
|
||||||
|
|
||||||
|
History --> Postgres
|
||||||
|
Postgres --> Dashboard
|
||||||
|
Dashboard --> Browser
|
||||||
|
Dashboard -. "/state + /healthz" .-> Ingest
|
||||||
|
Dashboard -. "/state + /healthz" .-> RefIngest
|
||||||
|
Dashboard -. "/state + /healthz" .-> Liquidity
|
||||||
|
Dashboard -. "/state + /healthz" .-> Inventory
|
||||||
|
Dashboard -. "/state + /healthz" .-> History
|
||||||
|
Dashboard -. "/state + /healthz" .-> Sentinel
|
||||||
|
Dashboard -. "/state + /healthz" .-> Strategy
|
||||||
|
Dashboard -. "/state + /healthz" .-> Executor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components And Responsibilities
|
||||||
|
|
||||||
|
### `near-intents-ingest`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- connect to the NEAR Intents websocket
|
||||||
|
- subscribe to quote traffic
|
||||||
|
- preserve the raw upstream payload
|
||||||
|
- normalize quote requests into the canonical `swap_demand` shape
|
||||||
|
- apply pair filtering before publishing
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `raw.near_intents.quote`
|
||||||
|
- `norm.swap_demand`
|
||||||
|
|
||||||
|
### `market-reference-ingest`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- fetch BTC/EUR reference prices
|
||||||
|
- prefer Kraken and fall back to CoinGecko
|
||||||
|
- publish the reference price used for valuation and strategy checks
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `ref.market_price`
|
||||||
|
|
||||||
|
### `liquidity-manager`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- track bridge deposit handles
|
||||||
|
- observe deposits and withdrawals
|
||||||
|
- observe pre-credit funding activity
|
||||||
|
- estimate and optionally submit withdrawals
|
||||||
|
- publish treasury and funding operations signals
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
- `ops.funding_observation`
|
||||||
|
|
||||||
|
### `inventory-sync`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- combine verifier balances with deposit and withdrawal side inputs
|
||||||
|
- publish the current inventory snapshot
|
||||||
|
- separate spendable balances from pending inbound and pending outbound funds
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
- `ops.funding_observation`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `state.intent_inventory`
|
||||||
|
|
||||||
|
### `strategy-engine`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- consume normalized demand
|
||||||
|
- read the latest reference price and inventory snapshot
|
||||||
|
- evaluate whether a quote is actionable
|
||||||
|
- record why it skipped or accepted a quote
|
||||||
|
- emit an execution command when the strategy is armed and the quote passes checks
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- `norm.swap_demand`
|
||||||
|
- `ref.market_price`
|
||||||
|
- `state.intent_inventory`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `decision.trade_decision`
|
||||||
|
- `cmd.execute_trade`
|
||||||
|
|
||||||
|
### `trade-executor`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- consume execution commands
|
||||||
|
- enforce arming state
|
||||||
|
- enforce command idempotency
|
||||||
|
- submit a quote response back to the venue
|
||||||
|
- publish the execution result
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- `cmd.execute_trade`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `exec.trade_result`
|
||||||
|
|
||||||
|
### `ops-sentinel`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- watch price freshness, inventory freshness, funding progression, liquidity events, and execution outcomes
|
||||||
|
- turn those signals into operator alert transitions
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- `ref.market_price`
|
||||||
|
- `state.intent_inventory`
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
- `ops.funding_observation`
|
||||||
|
- `exec.trade_result`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `ops.alert`
|
||||||
|
|
||||||
|
### `history-writer`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- consume the major event topics
|
||||||
|
- route each topic into a PostgreSQL history table
|
||||||
|
- compute portfolio metrics from the durable event history
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- `raw.near_intents.quote`
|
||||||
|
- `norm.swap_demand`
|
||||||
|
- `ref.market_price`
|
||||||
|
- `state.intent_inventory`
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
- `ops.funding_observation`
|
||||||
|
- `ops.alert`
|
||||||
|
- `decision.trade_decision`
|
||||||
|
- `cmd.execute_trade`
|
||||||
|
- `exec.trade_result`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- rows in PostgreSQL event tables
|
||||||
|
- rows in `portfolio_metrics_snapshots`
|
||||||
|
|
||||||
|
### `operator-dashboard`
|
||||||
|
|
||||||
|
Responsibility:
|
||||||
|
|
||||||
|
- expose one browser-facing API and WebSocket endpoint
|
||||||
|
- read durable history and metrics from PostgreSQL
|
||||||
|
- read live service state from `/state` and `/healthz`
|
||||||
|
- consume a small live Kafka topic set to avoid polling in the browser
|
||||||
|
- fan live updates to the frontend
|
||||||
|
|
||||||
|
Consumes:
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- service control APIs
|
||||||
|
- `norm.swap_demand`
|
||||||
|
- `ref.market_price`
|
||||||
|
- `state.intent_inventory`
|
||||||
|
- `ops.alert`
|
||||||
|
- `exec.trade_result`
|
||||||
|
|
||||||
|
Produces:
|
||||||
|
|
||||||
|
- `/api/session`
|
||||||
|
- `/api/bootstrap`
|
||||||
|
- `/api/trades`
|
||||||
|
- `/api/control/...`
|
||||||
|
- `/ws`
|
||||||
|
|
||||||
|
## Topic Classes And Signals
|
||||||
|
|
||||||
|
The topic naming scheme is meaningful:
|
||||||
|
|
||||||
|
- `raw.*` means source-native truth
|
||||||
|
- `norm.*` means canonical normalized inputs
|
||||||
|
- `ref.*` means reference market data
|
||||||
|
- `state.*` means derived state snapshots
|
||||||
|
- `ops.*` means operational or treasury state transitions
|
||||||
|
- `decision.*` means strategy output explaining what the system thought
|
||||||
|
- `cmd.*` means requested execution work
|
||||||
|
- `exec.*` means execution outcome
|
||||||
|
|
||||||
|
| Topic | Producer | Primary meaning | Main consumers |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `raw.near_intents.quote` | `near-intents-ingest` | Raw venue quote frame | `history-writer` |
|
||||||
|
| `norm.swap_demand` | `near-intents-ingest` | Canonical demand signal from quote requests | `strategy-engine`, `history-writer`, `operator-dashboard` |
|
||||||
|
| `ref.market_price` | `market-reference-ingest` | Reference BTC/EUR pricing for valuation and strategy | `strategy-engine`, `ops-sentinel`, `history-writer`, `operator-dashboard` |
|
||||||
|
| `state.intent_inventory` | `inventory-sync` | Current spendable and pending inventory state | `strategy-engine`, `ops-sentinel`, `history-writer`, `operator-dashboard` |
|
||||||
|
| `ops.liquidity_action` | `liquidity-manager` | Deposit handle, deposit, withdrawal, and treasury actions | `inventory-sync`, `ops-sentinel`, `history-writer` |
|
||||||
|
| `ops.funding_observation` | `liquidity-manager` | Pre-credit inbound funding observations | `inventory-sync`, `ops-sentinel`, `history-writer` |
|
||||||
|
| `ops.alert` | `ops-sentinel` | Raised or cleared operator alerts | `history-writer`, `operator-dashboard` |
|
||||||
|
| `decision.trade_decision` | `strategy-engine` | Strategy explanation for skip or action | `history-writer` |
|
||||||
|
| `cmd.execute_trade` | `strategy-engine` | Instruction for the executor | `trade-executor`, `history-writer` |
|
||||||
|
| `exec.trade_result` | `trade-executor` | Outcome of the execution attempt | `ops-sentinel`, `history-writer`, `operator-dashboard` |
|
||||||
|
|
||||||
|
## Signal Flow
|
||||||
|
|
||||||
|
This is the high-level flow from upstream quote to operator visibility:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["Quote request arrives from NEAR Intents"] --> B["near-intents-ingest"]
|
||||||
|
B --> C["raw.near_intents.quote"]
|
||||||
|
B --> D["norm.swap_demand"]
|
||||||
|
|
||||||
|
E["Reference price refresh"] --> F["market-reference-ingest"]
|
||||||
|
F --> G["ref.market_price"]
|
||||||
|
|
||||||
|
H["Bridge / verifier / BTC observer state"] --> I["liquidity-manager"]
|
||||||
|
I --> J["ops.liquidity_action"]
|
||||||
|
I --> K["ops.funding_observation"]
|
||||||
|
J --> L["inventory-sync"]
|
||||||
|
K --> L
|
||||||
|
L --> M["state.intent_inventory"]
|
||||||
|
|
||||||
|
D --> N["strategy-engine"]
|
||||||
|
G --> N
|
||||||
|
M --> N
|
||||||
|
N --> O["decision.trade_decision"]
|
||||||
|
N --> P["cmd.execute_trade"]
|
||||||
|
|
||||||
|
P --> Q["trade-executor"]
|
||||||
|
Q --> R["exec.trade_result"]
|
||||||
|
|
||||||
|
G --> S["ops-sentinel"]
|
||||||
|
M --> S
|
||||||
|
J --> S
|
||||||
|
K --> S
|
||||||
|
R --> S
|
||||||
|
S --> T["ops.alert"]
|
||||||
|
|
||||||
|
C --> U["history-writer"]
|
||||||
|
D --> U
|
||||||
|
G --> U
|
||||||
|
M --> U
|
||||||
|
J --> U
|
||||||
|
K --> U
|
||||||
|
O --> U
|
||||||
|
P --> U
|
||||||
|
R --> U
|
||||||
|
T --> U
|
||||||
|
U --> V["PostgreSQL"]
|
||||||
|
|
||||||
|
V --> W["operator-dashboard bootstrap reads"]
|
||||||
|
D --> X["operator-dashboard live consumer"]
|
||||||
|
G --> X
|
||||||
|
M --> X
|
||||||
|
T --> X
|
||||||
|
R --> X
|
||||||
|
W --> Y["Browser REST bootstrap"]
|
||||||
|
X --> Z["Browser WebSocket live updates"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sequence Diagrams
|
||||||
|
|
||||||
|
### Quote To Decision To Execution
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Venue as NEAR Intents WS
|
||||||
|
participant Ingest as near-intents-ingest
|
||||||
|
participant Bus as Redpanda/Kafka
|
||||||
|
participant Strategy as strategy-engine
|
||||||
|
participant Executor as trade-executor
|
||||||
|
participant Sentinel as ops-sentinel
|
||||||
|
participant History as history-writer
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
Venue->>Ingest: quote event frame
|
||||||
|
Ingest->>Bus: raw.near_intents.quote
|
||||||
|
Ingest->>Bus: norm.swap_demand
|
||||||
|
|
||||||
|
Bus->>Strategy: norm.swap_demand
|
||||||
|
Bus->>History: raw + norm events
|
||||||
|
History->>DB: insert raw_near_intents_quotes / swap_demand_events
|
||||||
|
|
||||||
|
Note over Strategy: Strategy already tracks latest ref.market_price and state.intent_inventory
|
||||||
|
|
||||||
|
Strategy->>Bus: decision.trade_decision
|
||||||
|
alt actionable and armed
|
||||||
|
Strategy->>Bus: cmd.execute_trade
|
||||||
|
Bus->>Executor: cmd.execute_trade
|
||||||
|
Executor->>Venue: quote_response submission
|
||||||
|
Venue-->>Executor: ack / response
|
||||||
|
Executor->>Bus: exec.trade_result
|
||||||
|
else rejected
|
||||||
|
Note over Strategy: No command emitted
|
||||||
|
end
|
||||||
|
|
||||||
|
Bus->>Sentinel: exec.trade_result
|
||||||
|
Sentinel->>Bus: ops.alert (if alert state changes)
|
||||||
|
|
||||||
|
Bus->>History: decision / command / result / alert
|
||||||
|
History->>DB: insert trade_decisions / execute_trade_commands / trade_execution_results / ops_alerts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard Bootstrap And Live Updates
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Browser
|
||||||
|
participant Dashboard as operator-dashboard
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
participant Services as internal /state + /healthz
|
||||||
|
participant Bus as Redpanda/Kafka
|
||||||
|
|
||||||
|
Browser->>Dashboard: GET /api/bootstrap
|
||||||
|
Dashboard->>DB: load portfolio metric, price, inventory, quotes, trades, alerts
|
||||||
|
Dashboard->>Services: GET /state and /healthz across services
|
||||||
|
DB-->>Dashboard: durable history + metrics
|
||||||
|
Services-->>Dashboard: live service state
|
||||||
|
Dashboard-->>Browser: bootstrap JSON
|
||||||
|
|
||||||
|
Browser->>Dashboard: WebSocket /ws
|
||||||
|
Dashboard-->>Browser: session.ready + initial live state
|
||||||
|
|
||||||
|
Bus->>Dashboard: norm.swap_demand
|
||||||
|
Dashboard-->>Browser: quotes.recent
|
||||||
|
|
||||||
|
Bus->>Dashboard: ref.market_price / state.intent_inventory / ops.alert / exec.trade_result
|
||||||
|
Dashboard-->>Browser: status_bar.updated / alerts.updated
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Envelope
|
||||||
|
|
||||||
|
Every bus event shares the same outer shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "string",
|
||||||
|
"event_type": "string",
|
||||||
|
"venue": "string",
|
||||||
|
"source": "string|null",
|
||||||
|
"schema_version": 1,
|
||||||
|
"observed_at": "ISO-8601|null",
|
||||||
|
"ingested_at": "ISO-8601",
|
||||||
|
"payload": {},
|
||||||
|
"raw": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This matters because it gives every downstream consumer a stable minimum contract:
|
||||||
|
|
||||||
|
- unique event identity
|
||||||
|
- event type
|
||||||
|
- venue/source attribution
|
||||||
|
- two timestamps
|
||||||
|
- the domain payload
|
||||||
|
- optional raw payload preservation
|
||||||
|
|
||||||
|
## What Is Persisted
|
||||||
|
|
||||||
|
There are three different persistence layers in the current system.
|
||||||
|
|
||||||
|
### 1. Kafka / Redpanda Retention
|
||||||
|
|
||||||
|
Kafka is the operational event backbone:
|
||||||
|
|
||||||
|
- it retains the event stream for replay and decoupling
|
||||||
|
- it is not the main analytical store
|
||||||
|
- it lets multiple consumers read the same event flow independently
|
||||||
|
|
||||||
|
What this layer is good for:
|
||||||
|
|
||||||
|
- fan-out
|
||||||
|
- short-horizon replay
|
||||||
|
- decoupled service design
|
||||||
|
- live dashboard updates without browser polling
|
||||||
|
|
||||||
|
### 2. PostgreSQL Durable History
|
||||||
|
|
||||||
|
PostgreSQL is the main durable queryable history layer for operator views and analysis.
|
||||||
|
|
||||||
|
All event tables share the same durable columns:
|
||||||
|
|
||||||
|
| Column | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `event_id` | Unique event id from the envelope |
|
||||||
|
| `topic` | Kafka topic name |
|
||||||
|
| `venue` | Venue or source family |
|
||||||
|
| `source` | Producing service or adapter |
|
||||||
|
| `event_type` | Domain event type |
|
||||||
|
| `observed_at` | When the upstream thing happened, if known |
|
||||||
|
| `ingested_at` | When this service emitted the event |
|
||||||
|
| `quote_id` | Quote correlation key when available |
|
||||||
|
| `pair` | Pair string when available |
|
||||||
|
| `decision_key` | Topic-specific correlation key such as decision id or command id |
|
||||||
|
| `payload` | The structured event payload |
|
||||||
|
| `raw` | The raw upstream payload when preserved |
|
||||||
|
|
||||||
|
#### `raw_near_intents_quotes`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `raw.near_intents.quote`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `message`: original upstream frame
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- schema drift inspection
|
||||||
|
- replay or re-normalization
|
||||||
|
- debugging unexpected parser behavior
|
||||||
|
- checking what fields the venue actually sent
|
||||||
|
|
||||||
|
#### `swap_demand_events`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `norm.swap_demand`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `quote_id`
|
||||||
|
- `asset_in`
|
||||||
|
- `asset_out`
|
||||||
|
- `pair`
|
||||||
|
- `request_kind`
|
||||||
|
- `amount_in`
|
||||||
|
- `amount_out`
|
||||||
|
- `min_deadline_ms`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- quote frequency by time
|
||||||
|
- pair mix and direction analysis
|
||||||
|
- size distributions
|
||||||
|
- exact-in vs exact-out behavior
|
||||||
|
- quote freshness analysis
|
||||||
|
|
||||||
|
#### `market_price_events`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `ref.market_price`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `price_id`
|
||||||
|
- `pair`
|
||||||
|
- `eur_per_btc`
|
||||||
|
- `eure_per_btc`
|
||||||
|
- `btc_per_eur`
|
||||||
|
- `btc_per_eure`
|
||||||
|
- `source_used`
|
||||||
|
- `fallback_in_use`
|
||||||
|
- `divergence_pct`
|
||||||
|
- `eure_per_eur_assumption`
|
||||||
|
- `kraken.price`
|
||||||
|
- `kraken.observed_at`
|
||||||
|
- `kraken.age_ms`
|
||||||
|
- `kraken.healthy`
|
||||||
|
- `kraken.error`
|
||||||
|
- `coingecko.price`
|
||||||
|
- `coingecko.observed_at`
|
||||||
|
- `coingecko.age_ms`
|
||||||
|
- `coingecko.healthy`
|
||||||
|
- `coingecko.error`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- valuation at a point in time
|
||||||
|
- price freshness tracking
|
||||||
|
- fallback source usage
|
||||||
|
- disagreement between Kraken and CoinGecko
|
||||||
|
- linking strategy decisions to the exact price snapshot they saw
|
||||||
|
|
||||||
|
#### `intent_inventory_snapshots`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `state.intent_inventory`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `inventory_id`
|
||||||
|
- `account_id`
|
||||||
|
- `synced_at`
|
||||||
|
- `spendable`
|
||||||
|
- `pending_inbound`
|
||||||
|
- `pending_outbound`
|
||||||
|
- `deposits`
|
||||||
|
- `withdrawals`
|
||||||
|
- `reconciliation_status`
|
||||||
|
|
||||||
|
Deposit record attributes:
|
||||||
|
|
||||||
|
- `tx_hash`
|
||||||
|
- `chain`
|
||||||
|
- `asset_id`
|
||||||
|
- `amount`
|
||||||
|
- `address`
|
||||||
|
- `status`
|
||||||
|
- `decimals`
|
||||||
|
|
||||||
|
Withdrawal record attributes:
|
||||||
|
|
||||||
|
- `withdrawal_hash`
|
||||||
|
- `asset_id`
|
||||||
|
- `chain`
|
||||||
|
- `amount`
|
||||||
|
- `status`
|
||||||
|
- `address`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- spendable balance history
|
||||||
|
- pending transfer tracking
|
||||||
|
- treasury state over time
|
||||||
|
- deposit and withdrawal visibility
|
||||||
|
- linking a decision to the inventory snapshot it used
|
||||||
|
|
||||||
|
#### `liquidity_actions`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `ops.liquidity_action`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `liquidity_action_id`
|
||||||
|
- `account_id`
|
||||||
|
- `action_type`
|
||||||
|
- `status`
|
||||||
|
- `chain`
|
||||||
|
- `asset_id`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
The `details` object varies by action type. Common examples include:
|
||||||
|
|
||||||
|
- refreshed deposit addresses
|
||||||
|
- observed deposit records
|
||||||
|
- tracked withdrawal objects
|
||||||
|
- changed withdrawal status objects
|
||||||
|
- submitted withdrawal details
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- treasury workflow timelines
|
||||||
|
- deposit handle rotation history
|
||||||
|
- withdrawal lifecycle tracking
|
||||||
|
- identifying when operator actions changed state
|
||||||
|
|
||||||
|
#### `funding_observations`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `ops.funding_observation`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `funding_observation_id`
|
||||||
|
- `account_id`
|
||||||
|
- `asset_id`
|
||||||
|
- `chain`
|
||||||
|
- `funding_handle`
|
||||||
|
- `source`
|
||||||
|
- `tx_hash`
|
||||||
|
- `status`
|
||||||
|
- `amount`
|
||||||
|
- `confirmations`
|
||||||
|
- `first_seen_at`
|
||||||
|
- `last_seen_at`
|
||||||
|
- `credited_at`
|
||||||
|
- `bridge_deposit_tx_hash`
|
||||||
|
- `bridge_status`
|
||||||
|
- `credit_correlation`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- pre-credit deposit detection
|
||||||
|
- funding aging and stuck funding analysis
|
||||||
|
- credit lag measurement
|
||||||
|
- correlation between observed chain transactions and bridge crediting
|
||||||
|
|
||||||
|
#### `ops_alerts`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `ops.alert`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `alert_event_id`
|
||||||
|
- `alert_code`
|
||||||
|
- `status`
|
||||||
|
- `severity`
|
||||||
|
- `reason`
|
||||||
|
- `service_scope`
|
||||||
|
- `pair`
|
||||||
|
- `asset_id`
|
||||||
|
- `raised_at`
|
||||||
|
- `cleared_at`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- alert timelines
|
||||||
|
- mean time to recovery
|
||||||
|
- stale data and treasury issue analysis
|
||||||
|
- operator incident review
|
||||||
|
|
||||||
|
#### `trade_decisions`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `decision.trade_decision`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `decision_id`
|
||||||
|
- `quote_id`
|
||||||
|
- `pair`
|
||||||
|
- `direction`
|
||||||
|
- `request_kind`
|
||||||
|
- `decision`
|
||||||
|
- `decision_reason`
|
||||||
|
- `threshold_pct`
|
||||||
|
- `max_notional_eure`
|
||||||
|
- `strategy_armed`
|
||||||
|
- `assumptions.eure_per_eur`
|
||||||
|
- `price_freshness_ms`
|
||||||
|
- `inventory_freshness_ms`
|
||||||
|
- `reference_rate`
|
||||||
|
- `implied_rate`
|
||||||
|
- `gross_edge_pct`
|
||||||
|
- `inventory_asset`
|
||||||
|
- `inventory_required`
|
||||||
|
- `inventory_available`
|
||||||
|
- `inventory_id`
|
||||||
|
- `price_id`
|
||||||
|
- `eure_notional`
|
||||||
|
- `proposed_amount_in`
|
||||||
|
- `proposed_amount_out`
|
||||||
|
- `pending_inbound`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- understanding why quotes were skipped
|
||||||
|
- measuring how many opportunities reached the actionable state
|
||||||
|
- analyzing insufficient inventory vs stale data vs threshold rejects
|
||||||
|
- reconstructing the exact decision context used by the strategy
|
||||||
|
|
||||||
|
#### `execute_trade_commands`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `cmd.execute_trade`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `command_id`
|
||||||
|
- `decision_id`
|
||||||
|
- `idempotency_key`
|
||||||
|
- `execution_key`
|
||||||
|
- `quote_id`
|
||||||
|
- `pair`
|
||||||
|
- `asset_in`
|
||||||
|
- `asset_out`
|
||||||
|
- `amount_in`
|
||||||
|
- `amount_out`
|
||||||
|
- `request_kind`
|
||||||
|
- `min_deadline_ms`
|
||||||
|
- `quote_output.amount_in`
|
||||||
|
- `quote_output.amount_out`
|
||||||
|
- `proposed_amount_in`
|
||||||
|
- `proposed_amount_out`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- seeing exactly what execution was asked to do
|
||||||
|
- duplicate detection analysis
|
||||||
|
- comparing command intent to result outcome
|
||||||
|
- joining strategy decisions to actual execution attempts
|
||||||
|
|
||||||
|
#### `trade_execution_results`
|
||||||
|
|
||||||
|
Source topic:
|
||||||
|
|
||||||
|
- `exec.trade_result`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `command_id`
|
||||||
|
- `decision_id`
|
||||||
|
- `idempotency_key`
|
||||||
|
- `execution_key`
|
||||||
|
- `quote_id`
|
||||||
|
- `pair`
|
||||||
|
- `status`
|
||||||
|
- `result_code`
|
||||||
|
- `note`
|
||||||
|
- `venue_response`
|
||||||
|
- `error`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- execution success and failure rates
|
||||||
|
- venue response analysis
|
||||||
|
- distinguishing disarmed rejects from submission failures
|
||||||
|
- command-to-result conversion tracking
|
||||||
|
|
||||||
|
#### `portfolio_metrics_snapshots`
|
||||||
|
|
||||||
|
This table is derived from durable history rather than copied directly from a Kafka event.
|
||||||
|
|
||||||
|
Top-level columns:
|
||||||
|
|
||||||
|
- `metric_id`
|
||||||
|
- `computed_at`
|
||||||
|
- `baseline_anchor_at`
|
||||||
|
- `baseline_status`
|
||||||
|
- `payload`
|
||||||
|
|
||||||
|
Key payload attributes:
|
||||||
|
|
||||||
|
- `metric_version`
|
||||||
|
- `baseline_status`
|
||||||
|
- `command_count`
|
||||||
|
- `result_count`
|
||||||
|
- `current_price.price_id`
|
||||||
|
- `current_price.observed_at`
|
||||||
|
- `current_price.eure_per_btc`
|
||||||
|
- `current_inventory.inventory_id`
|
||||||
|
- `current_inventory.synced_at`
|
||||||
|
- `current_inventory.btc_units`
|
||||||
|
- `current_inventory.btc`
|
||||||
|
- `current_inventory.eure_units`
|
||||||
|
- `current_inventory.eure`
|
||||||
|
- `current_portfolio_value_eure`
|
||||||
|
- `current_btc_mark_value_eure`
|
||||||
|
- `current_eure_cash_value_eure`
|
||||||
|
- `trade_pnl_eure`
|
||||||
|
- `mark_to_market_pnl_eure`
|
||||||
|
- `price_move_pnl_eure`
|
||||||
|
- `baseline_portfolio_value_eure_at_baseline_price`
|
||||||
|
- `baseline_portfolio_value_eure_at_current_price`
|
||||||
|
- `current_portfolio_value_eure_at_baseline_price`
|
||||||
|
- `inventory_delta.btc_units`
|
||||||
|
- `inventory_delta.btc`
|
||||||
|
- `inventory_delta.eure_units`
|
||||||
|
- `inventory_delta.eure`
|
||||||
|
- `baseline.anchor`
|
||||||
|
- `baseline.command_at`
|
||||||
|
- `baseline.price`
|
||||||
|
- `baseline.inventory`
|
||||||
|
|
||||||
|
Useful for:
|
||||||
|
|
||||||
|
- current operator PnL display
|
||||||
|
- separating trade contribution from price movement
|
||||||
|
- baseline vs current comparison
|
||||||
|
- current portfolio valuation using the current price snapshot
|
||||||
|
|
||||||
|
### 3. JSON / PVC-Backed Operational State
|
||||||
|
|
||||||
|
Some important state is persisted outside PostgreSQL because it is operational control state, not analytical history.
|
||||||
|
|
||||||
|
#### Strategy armed state
|
||||||
|
|
||||||
|
Stored in:
|
||||||
|
|
||||||
|
- `strategyStateDir/strategy-engine-control.json`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
- `armed`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- preserve strategy arming/disarming across restarts
|
||||||
|
|
||||||
|
#### Executor armed state
|
||||||
|
|
||||||
|
Stored in:
|
||||||
|
|
||||||
|
- `executorStateDir/trade-executor-control.json`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
- `armed`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- preserve executor arming/disarming across restarts
|
||||||
|
|
||||||
|
#### Executor command idempotency state
|
||||||
|
|
||||||
|
Stored in:
|
||||||
|
|
||||||
|
- `executorStateDir/trade-executor-commands.json`
|
||||||
|
|
||||||
|
Attributes per command id:
|
||||||
|
|
||||||
|
- `quote_id`
|
||||||
|
- `idempotency_key`
|
||||||
|
- `execution_key`
|
||||||
|
- `status`
|
||||||
|
- `updated_at`
|
||||||
|
- result or error metadata when available
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- prevent duplicate execution across restart or replay scenarios
|
||||||
|
|
||||||
|
#### Liquidity manager state
|
||||||
|
|
||||||
|
Stored in:
|
||||||
|
|
||||||
|
- `liquidityStateDir/liquidity.json`
|
||||||
|
|
||||||
|
Key attributes:
|
||||||
|
|
||||||
|
- `paused`
|
||||||
|
- `funding_observer_paused`
|
||||||
|
- `withdrawals_frozen`
|
||||||
|
- `deposit_addresses`
|
||||||
|
- `deposits`
|
||||||
|
- `tracked_withdrawals`
|
||||||
|
- `supported_tokens`
|
||||||
|
- `funding_observations`
|
||||||
|
- `funding_observations_by_handle`
|
||||||
|
- `funding_visibility_by_asset`
|
||||||
|
- `uncredited_funding_total_by_asset`
|
||||||
|
- `credit_correlation`
|
||||||
|
- `observer_health`
|
||||||
|
- `last_refresh_at`
|
||||||
|
- `last_funding_observation_at`
|
||||||
|
- `funding_observer_last_refresh_at`
|
||||||
|
- `funding_observer_last_error`
|
||||||
|
- `last_error`
|
||||||
|
- `last_withdrawal_request`
|
||||||
|
- `last_withdrawal_result`
|
||||||
|
- `publish_count`
|
||||||
|
- `funding_publish_count`
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- preserve treasury and observer operational state across restarts
|
||||||
|
- back service `/state` responses
|
||||||
|
|
||||||
|
## What The Data Lets You Do
|
||||||
|
|
||||||
|
With the currently persisted data, you can already answer questions like:
|
||||||
|
|
||||||
|
- How many quotes arrived, at what sizes, and in which direction?
|
||||||
|
- Which quotes were rejected because of stale price, stale inventory, insufficient inventory, pending deposits, unsupported pair, or max notional?
|
||||||
|
- Which decisions became actual commands?
|
||||||
|
- Which commands became successful submitted results?
|
||||||
|
- What was the reference price and inventory snapshot seen by the strategy when it made a decision?
|
||||||
|
- How often did the pricing source fall back from Kraken to CoinGecko?
|
||||||
|
- How much pre-credit funding was visible before bridge crediting happened?
|
||||||
|
- How long did funding remain in a pending or stuck state?
|
||||||
|
- What alert transitions happened and how long did they remain raised?
|
||||||
|
- What is the current portfolio value, and how much of the change looks like price move vs trade contribution?
|
||||||
|
|
||||||
|
## Current Limits
|
||||||
|
|
||||||
|
A few important limits are worth naming plainly:
|
||||||
|
|
||||||
|
- Kafka retention is not the long-term historical warehouse; PostgreSQL is the main durable query surface
|
||||||
|
- raw venue schemas can drift, so raw quote storage matters for parser changes
|
||||||
|
- execution results currently represent submission outcomes, not full realized settlement economics
|
||||||
|
- fee coverage is not yet complete enough to treat all PnL numbers as full realized net profit
|
||||||
|
- some safety-critical state lives in JSON/PVC-backed operational stores, not only in PostgreSQL
|
||||||
|
|
||||||
|
## Recommended Reading Order
|
||||||
|
|
||||||
|
If you want to trace the system from code:
|
||||||
|
|
||||||
|
1. `src/lib/config.mjs`
|
||||||
|
2. `src/venues/near-intents/ws.mjs`
|
||||||
|
3. `src/apps/strategy-engine.mjs`
|
||||||
|
4. `src/apps/trade-executor.mjs`
|
||||||
|
5. `src/apps/history-writer.mjs`
|
||||||
|
6. `src/core/history-records.mjs`
|
||||||
|
7. `src/lib/postgres.mjs`
|
||||||
|
8. `src/apps/operator-dashboard.mjs`
|
||||||
16
fix-attributions-fast.mjs
Normal file
16
fix-attributions-fast.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createPostgresPool, refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
console.log("Running refreshQuoteOutcomes with large limits to re-attribute historical trades...");
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
submissionLimit: 15000,
|
||||||
|
inventoryLimit: 50000,
|
||||||
|
});
|
||||||
|
console.log("Refreshed", records.length, "quotes.");
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found", completed.length, "completed trades!");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
16
fix-attributions.mjs
Normal file
16
fix-attributions.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createPostgresPool, refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
console.log("Running refreshQuoteOutcomes with large limits to re-attribute historical trades...");
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
submissionLimit: 50000,
|
||||||
|
inventoryLimit: 600000,
|
||||||
|
});
|
||||||
|
console.log("Refreshed", records.length, "quotes.");
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found", completed.length, "completed trades!");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
8
patch-postgres-payload.mjs
Normal file
8
patch-postgres-payload.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
const file = 'src/lib/postgres.mjs';
|
||||||
|
let content = fs.readFileSync(file, 'utf8');
|
||||||
|
content = content.replace(
|
||||||
|
'SELECT event_id, observed_at, ingested_at, quote_id, payload\n FROM intent_inventory_snapshots',
|
||||||
|
'SELECT event_id, observed_at, ingested_at, quote_id, jsonb_build_object(\\'spendable\\', payload->\\'spendable\\', \\'synced_at\\', payload->\\'synced_at\\') AS payload\n FROM intent_inventory_snapshots'
|
||||||
|
);
|
||||||
|
fs.writeFileSync(file, content);
|
||||||
47
profile-queries.mjs
Normal file
47
profile-queries.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { loadConfig } from './src/lib/config.mjs';
|
||||||
|
import * as pg from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const config = loadConfig();
|
||||||
|
const pool = pg.createPostgresPool({ connectionString: config.postgresUrl });
|
||||||
|
|
||||||
|
const fns = [
|
||||||
|
{ name: 'loadLatestPortfolioMetric', fn: () => pg.loadLatestPortfolioMetric(pool) },
|
||||||
|
{ name: 'loadLatestInventorySnapshot', fn: () => pg.loadLatestInventorySnapshot(pool) },
|
||||||
|
{ name: 'loadLatestMarketPrice', fn: () => pg.loadLatestMarketPrice(pool) },
|
||||||
|
{ name: 'loadRecentQuotes', fn: () => pg.loadRecentQuotes(pool, { limit: 100 }) },
|
||||||
|
{ name: 'loadSubmissionSummary', fn: () => pg.loadSubmissionSummary(pool) },
|
||||||
|
{ name: 'loadSubmissionPage', fn: () => pg.loadSubmissionPage(pool, { page: 1, pageSize: 20 }) },
|
||||||
|
{ name: 'loadCurrentFundingObservations', fn: () => pg.loadCurrentFundingObservations(pool) },
|
||||||
|
{ name: 'loadRecentDepositStatuses', fn: () => pg.loadRecentDepositStatuses(pool, { limit: 20 }) },
|
||||||
|
{ name: 'loadRecentTradeDecisions', fn: () => pg.loadRecentTradeDecisions(pool, { limit: 20 }) },
|
||||||
|
{ name: 'loadRecentExecuteTradeCommands', fn: () => pg.loadRecentExecuteTradeCommands(pool, { limit: 40 }) },
|
||||||
|
{ name: 'loadRecentExecutionResults', fn: () => pg.loadRecentExecutionResults(pool, { limit: 40 }) },
|
||||||
|
{ name: 'loadRecentQuoteOutcomes', fn: () => pg.loadRecentQuoteOutcomes(pool, { limit: 200 }) },
|
||||||
|
{ name: 'loadSuccessfulQuoteLifecycleEvidence', fn: () => pg.loadSuccessfulQuoteLifecycleEvidence(pool, { limit: 50 }) },
|
||||||
|
{ name: 'loadQuoteLifecycleRetentionSummary', fn: () => pg.loadQuoteLifecycleRetentionSummary(pool) },
|
||||||
|
{ name: 'loadRecentQuoteLifecycleRollups', fn: () => pg.loadRecentQuoteLifecycleRollups(pool, { limit: 40 }) },
|
||||||
|
{ name: 'loadQuoteLifecycleStatistics', fn: () => pg.loadQuoteLifecycleStatistics(pool, { limit: 1000 }) },
|
||||||
|
{ name: 'loadRecentIntentRequests', fn: () => pg.loadRecentIntentRequests(pool, { limit: 20, btcAsset: config.tradingBtc, eureAsset: config.tradingEure, refreshOutcomes: false }) },
|
||||||
|
{ name: 'loadRecentAlertTransitions', fn: () => pg.loadRecentAlertTransitions(pool, { limit: 20 }) },
|
||||||
|
{ name: 'loadRecentEnvironmentStatuses', fn: () => pg.loadRecentEnvironmentStatuses(pool, { limit: 20 }) },
|
||||||
|
{ name: 'loadAssetCatalogSummary', fn: () => pg.loadAssetCatalogSummary(pool, { limit: 250 }) },
|
||||||
|
{ name: 'loadPairConfigSummary', fn: () => pg.loadPairConfigSummary(pool) },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { name, fn } of fns) {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
console.log(`${name}: ${duration}ms`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`${name}: ERROR ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
18
recover-pg.yaml
Normal file
18
recover-pg.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: recover-pg
|
||||||
|
namespace: unrip
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: postgres:16
|
||||||
|
command: ["/bin/sh", "-c"]
|
||||||
|
args: ["su postgres -c '/usr/lib/postgresql/16/bin/pg_resetwal -f /var/lib/postgresql/data'; sleep 3600"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: postgres-data
|
||||||
1
research/experiments/.gitkeep
Normal file
1
research/experiments/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
97
scripts/dev/operator-dashboard-dev.sh
Executable file
97
scripts/dev/operator-dashboard-dev.sh
Executable file
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
PLATFORM_REPO_DIR="${PLATFORM_REPO_DIR:-$ROOT_DIR/../unrip3}"
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG_PATH:-${KUBECONFIG:-$PLATFORM_REPO_DIR/.state/hetzner/kubeconfig.yaml}}"
|
||||||
|
NAMESPACE="${OPERATOR_DASHBOARD_DEV_NAMESPACE:-unrip}"
|
||||||
|
SERVICE_NAME="${OPERATOR_DASHBOARD_DEV_SERVICE:-operator-dashboard}"
|
||||||
|
UPSTREAM_PORT="${OPERATOR_DASHBOARD_DEV_UPSTREAM_PORT:-18090}"
|
||||||
|
SERVICE_PORT="${OPERATOR_DASHBOARD_DEV_SERVICE_PORT:-8090}"
|
||||||
|
DEV_HOST="${OPERATOR_DASHBOARD_DEV_HOST:-0.0.0.0}"
|
||||||
|
DEV_PORT="${OPERATOR_DASHBOARD_DEV_PORT:-5173}"
|
||||||
|
USERNAME="${OPERATOR_DASHBOARD_DEV_USERNAME:-admin}"
|
||||||
|
UPSTREAM_HEALTH_TIMEOUT_SECONDS="${OPERATOR_DASHBOARD_DEV_HEALTH_TIMEOUT_SECONDS:-2}"
|
||||||
|
EXISTING_UPSTREAM_PROBE_TIMEOUT_SECONDS="${OPERATOR_DASHBOARD_DEV_EXISTING_PORT_TIMEOUT_SECONDS:-0.5}"
|
||||||
|
UPSTREAM_READY_ATTEMPTS="${OPERATOR_DASHBOARD_DEV_READY_ATTEMPTS:-50}"
|
||||||
|
UPSTREAM_READY_SLEEP_SECONDS="${OPERATOR_DASHBOARD_DEV_READY_SLEEP_SECONDS:-0.2}"
|
||||||
|
KUBECTL_REQUEST_TIMEOUT="${OPERATOR_DASHBOARD_DEV_KUBECTL_REQUEST_TIMEOUT:-10s}"
|
||||||
|
PORT_FORWARD_LOG="${OPERATOR_DASHBOARD_DEV_PORT_FORWARD_LOG:-/tmp/operator-dashboard-dev-port-forward.log}"
|
||||||
|
|
||||||
|
if [[ -z "${OPERATOR_DASHBOARD_DEV_BASIC_AUTH_PASSWORD:-}" ]]; then
|
||||||
|
if ! command -v pass >/dev/null 2>&1; then
|
||||||
|
echo "pass is required unless OPERATOR_DASHBOARD_DEV_BASIC_AUTH_PASSWORD is set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
OPERATOR_DASHBOARD_DEV_BASIC_AUTH_PASSWORD="$(pass unrip/dashboard)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export OPERATOR_DASHBOARD_DEV_BASIC_AUTH_HEADER="Basic $(printf '%s:%s' "$USERNAME" "$OPERATOR_DASHBOARD_DEV_BASIC_AUTH_PASSWORD" | base64 | tr -d '\n')"
|
||||||
|
export OPERATOR_DASHBOARD_DEV_UPSTREAM_URL="http://127.0.0.1:${UPSTREAM_PORT}"
|
||||||
|
export OPERATOR_DASHBOARD_DEV_HOST="$DEV_HOST"
|
||||||
|
export OPERATOR_DASHBOARD_DEV_PORT="$DEV_PORT"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${PORT_FORWARD_SUPERVISOR_PID:-}" ]]; then
|
||||||
|
pkill -P "$PORT_FORWARD_SUPERVISOR_PID" >/dev/null 2>&1 || true
|
||||||
|
kill "$PORT_FORWARD_SUPERVISOR_PID" >/dev/null 2>&1 || true
|
||||||
|
wait "$PORT_FORWARD_SUPERVISOR_PID" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
maintain_port_forward() {
|
||||||
|
while true; do
|
||||||
|
KUBECONFIG="$KUBECONFIG_PATH" kubectl --request-timeout="$KUBECTL_REQUEST_TIMEOUT" port-forward -n "$NAMESPACE" "svc/${SERVICE_NAME}" "${UPSTREAM_PORT}:${SERVICE_PORT}" || true
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream_ready() {
|
||||||
|
local timeout_seconds="${1:-$UPSTREAM_HEALTH_TIMEOUT_SECONDS}"
|
||||||
|
curl -sf \
|
||||||
|
--connect-timeout "$timeout_seconds" \
|
||||||
|
--max-time "$timeout_seconds" \
|
||||||
|
"${OPERATOR_DASHBOARD_DEV_UPSTREAM_URL}/healthz" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
port_in_use() {
|
||||||
|
timeout 1 bash -c ":</dev/tcp/127.0.0.1/${UPSTREAM_PORT}" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if port_in_use; then
|
||||||
|
if upstream_ready "$EXISTING_UPSTREAM_PROBE_TIMEOUT_SECONDS"; then
|
||||||
|
echo "reusing existing operator-dashboard upstream on port ${UPSTREAM_PORT}" >&2
|
||||||
|
else
|
||||||
|
echo "port ${UPSTREAM_PORT} is already in use but /healthz did not respond; stop the stale port-forward or set OPERATOR_DASHBOARD_DEV_UPSTREAM_PORT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
exec node_modules/.bin/vite --config vite.operator-dashboard.config.mjs --host "$DEV_HOST" --port "$DEV_PORT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
: >"$PORT_FORWARD_LOG"
|
||||||
|
maintain_port_forward >>"$PORT_FORWARD_LOG" 2>&1 &
|
||||||
|
PORT_FORWARD_SUPERVISOR_PID=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 "$UPSTREAM_READY_ATTEMPTS"); do
|
||||||
|
if upstream_ready; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -q "address already in use" "$PORT_FORWARD_LOG"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep "$UPSTREAM_READY_SLEEP_SECONDS"
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! upstream_ready; then
|
||||||
|
echo "operator-dashboard upstream did not become ready; see $PORT_FORWARD_LOG" >&2
|
||||||
|
if grep -q "address already in use" "$PORT_FORWARD_LOG"; then
|
||||||
|
echo "port ${UPSTREAM_PORT} is already in use; stop the old port-forward or set OPERATOR_DASHBOARD_DEV_UPSTREAM_PORT" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
exec node_modules/.bin/vite --config vite.operator-dashboard.config.mjs --host "$DEV_HOST" --port "$DEV_PORT"
|
||||||
18
scripts/dev/operator-dashboard-forward.sh
Executable file
18
scripts/dev/operator-dashboard-forward.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
PLATFORM_REPO_DIR="${PLATFORM_REPO_DIR:-$ROOT_DIR/../unrip3}"
|
||||||
|
KUBECONFIG_PATH="${KUBECONFIG_PATH:-${KUBECONFIG:-$PLATFORM_REPO_DIR/.state/hetzner/kubeconfig.yaml}}"
|
||||||
|
NAMESPACE="${OPERATOR_DASHBOARD_FORWARD_NAMESPACE:-unrip}"
|
||||||
|
SERVICE_NAME="${OPERATOR_DASHBOARD_FORWARD_SERVICE:-operator-dashboard}"
|
||||||
|
FORWARD_PORT="${OPERATOR_DASHBOARD_FORWARD_PORT:-8090}"
|
||||||
|
SERVICE_PORT="${OPERATOR_DASHBOARD_FORWARD_SERVICE_PORT:-8090}"
|
||||||
|
FORWARD_ADDRESS="${OPERATOR_DASHBOARD_FORWARD_ADDRESS:-0.0.0.0}"
|
||||||
|
|
||||||
|
exec env KUBECONFIG="$KUBECONFIG_PATH" \
|
||||||
|
kubectl port-forward \
|
||||||
|
--address "$FORWARD_ADDRESS" \
|
||||||
|
-n "$NAMESPACE" \
|
||||||
|
"svc/${SERVICE_NAME}" \
|
||||||
|
"${FORWARD_PORT}:${SERVICE_PORT}"
|
||||||
|
|
@ -4473,10 +4473,9 @@ export async function refreshQuoteOutcomes(pool, {
|
||||||
eureAsset = null,
|
eureAsset = null,
|
||||||
now = Date.now(),
|
now = Date.now(),
|
||||||
submissionLimit = 1000,
|
submissionLimit = 1000,
|
||||||
inventoryLimit = 50000,
|
inventoryWindowBufferMs = 15 * 60 * 1000,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 1000);
|
const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 1000);
|
||||||
const safeInventoryLimit = Math.max(1, Number(inventoryLimit) || 50000);
|
|
||||||
const submissionsResult = await pool.query(
|
const submissionsResult = await pool.query(
|
||||||
`
|
`
|
||||||
WITH recent_submissions AS (
|
WITH recent_submissions AS (
|
||||||
|
|
@ -4504,6 +4503,15 @@ export async function refreshQuoteOutcomes(pool, {
|
||||||
const quoteIds = [...new Set(submissionsResult.rows.map((row) => row.quote_id).filter(Boolean))];
|
const quoteIds = [...new Set(submissionsResult.rows.map((row) => row.quote_id).filter(Boolean))];
|
||||||
if (!quoteIds.length) return [];
|
if (!quoteIds.length) return [];
|
||||||
|
|
||||||
|
// Scope the inventory fetch to the time window of this submission batch
|
||||||
|
// instead of a blind LIMIT that pulls hundreds of MB.
|
||||||
|
const submissionTimestamps = submissionsResult.rows
|
||||||
|
.map((row) => new Date(row.observed_at || row.ingested_at).getTime())
|
||||||
|
.filter(Number.isFinite);
|
||||||
|
const bufferMs = Math.max(0, Number(inventoryWindowBufferMs) || 15 * 60 * 1000);
|
||||||
|
const inventoryWindowStart = new Date(Math.min(...submissionTimestamps) - bufferMs).toISOString();
|
||||||
|
const inventoryWindowEnd = new Date(Math.max(...submissionTimestamps) + bufferMs).toISOString();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
commandsResult,
|
commandsResult,
|
||||||
decisionsResult,
|
decisionsResult,
|
||||||
|
|
@ -4522,15 +4530,12 @@ export async function refreshQuoteOutcomes(pool, {
|
||||||
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
||||||
`, [quoteIds]),
|
`, [quoteIds]),
|
||||||
pool.query(`
|
pool.query(`
|
||||||
SELECT event_id, observed_at, ingested_at, quote_id, jsonb_build_object('spendable', payload->'spendable', 'synced_at', payload->'synced_at') AS payload
|
SELECT event_id, observed_at, ingested_at, quote_id,
|
||||||
FROM (
|
jsonb_build_object('spendable', payload->'spendable', 'synced_at', payload->'synced_at') AS payload
|
||||||
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
|
||||||
FROM intent_inventory_snapshots
|
FROM intent_inventory_snapshots
|
||||||
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
WHERE COALESCE(observed_at, ingested_at) BETWEEN $1 AND $2
|
||||||
LIMIT $1
|
|
||||||
) recent_inventory_snapshots
|
|
||||||
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
||||||
`, [safeInventoryLimit]),
|
`, [inventoryWindowStart, inventoryWindowEnd]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const records = deriveQuoteOutcomeRecords({
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
|
|
||||||
11
test-all-completed.mjs
Normal file
11
test-all-completed.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
const rows = await pool.query(`
|
||||||
|
SELECT count(*) FROM quote_outcome_attributions WHERE outcome_status = 'completed';
|
||||||
|
`);
|
||||||
|
console.log("Total completed in DB:", rows.rows[0].count);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
20
test-dashboard-fetch.mjs
Normal file
20
test-dashboard-fetch.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
console.log("Running refreshQuoteOutcomes...");
|
||||||
|
try {
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log("Records length:", records.length);
|
||||||
|
console.log("Completed:", records.filter(r => r.outcome_status === 'completed').length);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error:", err);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
37
test-dashboard-speed.mjs
Normal file
37
test-dashboard-speed.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import * as db from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function measure(name, fn) {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
console.log(`[OK] ${name}: ${Date.now() - start}ms`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ERR] ${name}: ${Date.now() - start}ms`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
await Promise.all([
|
||||||
|
measure('loadLatestPortfolioMetric', () => db.loadLatestPortfolioMetric(pool)),
|
||||||
|
measure('loadLatestInventorySnapshot', () => db.loadLatestInventorySnapshot(pool)),
|
||||||
|
measure('loadLatestMarketPrice', () => db.loadLatestMarketPrice(pool)),
|
||||||
|
measure('loadRecentQuotes', () => db.loadRecentQuotes(pool, { limit: 100 })),
|
||||||
|
measure('loadSubmissionSummary', () => db.loadSubmissionSummary(pool)),
|
||||||
|
measure('loadSubmissionPage', () => db.loadSubmissionPage(pool, { page: 1, pageSize: 50 })),
|
||||||
|
measure('loadCurrentFundingObservations', () => db.loadCurrentFundingObservations(pool)),
|
||||||
|
measure('loadRecentDepositStatuses', () => db.loadRecentDepositStatuses(pool, { limit: 20 })),
|
||||||
|
measure('loadRecentTradeDecisions', () => db.loadRecentTradeDecisions(pool, { limit: 20 })),
|
||||||
|
measure('loadRecentExecuteTradeCommands', () => db.loadRecentExecuteTradeCommands(pool, { limit: 40 })),
|
||||||
|
measure('loadRecentExecutionResults', () => db.loadRecentExecutionResults(pool, { limit: 40 })),
|
||||||
|
measure('loadRecentQuoteOutcomes', () => db.loadRecentQuoteOutcomes(pool, { limit: 200 })),
|
||||||
|
measure('loadSuccessfulQuoteLifecycleEvidence', () => db.loadSuccessfulQuoteLifecycleEvidence(pool, { limit: 50 })),
|
||||||
|
measure('loadQuoteLifecycleRetentionSummary', () => db.loadQuoteLifecycleRetentionSummary(pool)),
|
||||||
|
measure('loadRecentQuoteLifecycleRollups', () => db.loadRecentQuoteLifecycleRollups(pool, { limit: 40 })),
|
||||||
|
measure('loadQuoteLifecycleStatistics', () => db.loadQuoteLifecycleStatistics(pool, { limit: 1000 })),
|
||||||
|
measure('loadRecentIntentRequests', () => db.loadRecentIntentRequests(pool, { limit: 50 })),
|
||||||
|
]);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
38
test-deltas.mjs
Normal file
38
test-deltas.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
const inventoryRows = await pool.query(`
|
||||||
|
SELECT event_id as inventory_id, observed_at, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 2000
|
||||||
|
`);
|
||||||
|
const snapshots = inventoryRows.rows.map(r => ({
|
||||||
|
inventory_id: r.inventory_id,
|
||||||
|
observed_at: r.observed_at,
|
||||||
|
spendable: r.payload?.spendable || {},
|
||||||
|
})).sort((a,b) => new Date(a.observed_at) - new Date(b.observed_at));
|
||||||
|
|
||||||
|
const activeAssetIds = Object.keys(snapshots[0].spendable);
|
||||||
|
const deltas = [];
|
||||||
|
for (let i = 1; i < snapshots.length; i++) {
|
||||||
|
const prev = snapshots[i-1].spendable;
|
||||||
|
const curr = snapshots[i].spendable;
|
||||||
|
const delta = {};
|
||||||
|
for (const a of activeAssetIds) {
|
||||||
|
const p = BigInt(prev[a] || 0);
|
||||||
|
const c = BigInt(curr[a] || 0);
|
||||||
|
if (c - p !== 0n) delta[a] = (c - p).toString();
|
||||||
|
}
|
||||||
|
if (Object.keys(delta).length > 0) {
|
||||||
|
deltas.push({ observed_at: snapshots[i].observed_at, delta });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Total deltas:", deltas.length);
|
||||||
|
for (let i = 0; i < Math.min(5, deltas.length); i++) {
|
||||||
|
console.log(deltas[i].observed_at, Object.entries(deltas[i].delta).filter(x => x[1] !== '0'));
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
43
test-matches-all.mjs
Normal file
43
test-matches-all.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
const submissionsResult = await pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
`);
|
||||||
|
const submissions = submissionsResult.rows;
|
||||||
|
|
||||||
|
const [commandsResult, decisionsResult, inventoryResult] = await Promise.all([
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM execute_trade_commands`),
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM trade_decisions`),
|
||||||
|
pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 15000
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const btcAsset = { assetId: 'nep141:nbtc.bridge.near' };
|
||||||
|
const eureAsset = { assetId: 'nep141:eure.omft.near' };
|
||||||
|
|
||||||
|
console.log("Running derive...");
|
||||||
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
submissions,
|
||||||
|
commands: commandsResult.rows,
|
||||||
|
decisions: decisionsResult.rows,
|
||||||
|
inventorySnapshots: inventoryResult.rows,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
now: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found completed:", completed.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
45
test-matches-remote-5000.mjs
Normal file
45
test-matches-remote-5000.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
const submissionsResult = await pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
`);
|
||||||
|
const submissions = submissionsResult.rows;
|
||||||
|
|
||||||
|
const [commandsResult, decisionsResult, inventoryResult] = await Promise.all([
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM execute_trade_commands`),
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM trade_decisions`),
|
||||||
|
pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 5000
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Memory before derive:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
|
||||||
|
const btcAsset = { assetId: 'nep141:nbtc.bridge.near' };
|
||||||
|
const eureAsset = { assetId: 'nep141:eure.omft.near' };
|
||||||
|
|
||||||
|
console.log("Running derive...");
|
||||||
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
submissions,
|
||||||
|
commands: commandsResult.rows,
|
||||||
|
decisions: decisionsResult.rows,
|
||||||
|
inventorySnapshots: inventoryResult.rows,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
now: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found completed:", completed.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
43
test-matches-remote.mjs
Normal file
43
test-matches-remote.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
const submissionsResult = await pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
`);
|
||||||
|
const submissions = submissionsResult.rows;
|
||||||
|
|
||||||
|
const [commandsResult, decisionsResult, inventoryResult] = await Promise.all([
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM execute_trade_commands`),
|
||||||
|
pool.query(`SELECT event_id, observed_at, ingested_at, quote_id, payload FROM trade_decisions`),
|
||||||
|
pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 15000
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const btcAsset = { assetId: 'nep141:nbtc.bridge.near' };
|
||||||
|
const eureAsset = { assetId: 'nep141:eure.omft.near' };
|
||||||
|
|
||||||
|
console.log("Running derive...");
|
||||||
|
const records = deriveQuoteOutcomeRecords({
|
||||||
|
submissions,
|
||||||
|
commands: commandsResult.rows,
|
||||||
|
decisions: decisionsResult.rows,
|
||||||
|
inventorySnapshots: inventoryResult.rows,
|
||||||
|
btcAsset,
|
||||||
|
eureAsset,
|
||||||
|
now: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const completed = records.filter(r => r.outcome_status === 'completed');
|
||||||
|
console.log("Found completed:", completed.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
15
test-memory-2.mjs
Normal file
15
test-memory-2.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Memory before:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT payload
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE payload->>'status' = 'submitted'
|
||||||
|
LIMIT 1000
|
||||||
|
`);
|
||||||
|
console.log("Memory after fetch:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
13
test-memory-3.mjs
Normal file
13
test-memory-3.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { createPostgresPool, refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Memory before:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log("Memory after:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
20
test-memory.mjs
Normal file
20
test-memory.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Memory before:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT event_id, observed_at, ingested_at, quote_id, jsonb_build_object('spendable', payload->'spendable', 'synced_at', payload->'synced_at') AS payload
|
||||||
|
FROM (
|
||||||
|
SELECT event_id, observed_at, ingested_at, quote_id, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 50000
|
||||||
|
) recent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) ASC
|
||||||
|
`);
|
||||||
|
console.log("Memory after fetch:", process.memoryUsage().heapUsed / 1024 / 1024, "MB");
|
||||||
|
console.log("Rows fetched:", result.rows.length);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
15
test-outcomes-2.mjs
Normal file
15
test-outcomes-2.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
async function main() {
|
||||||
|
console.log("Starting...");
|
||||||
|
try {
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log("Returned:", records.length);
|
||||||
|
} catch(e) { console.error("Error:", e); }
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main();
|
||||||
17
test-outcomes-3.mjs
Normal file
17
test-outcomes-3.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
async function main() {
|
||||||
|
console.log("Starting debug run...");
|
||||||
|
try {
|
||||||
|
const t0 = Date.now();
|
||||||
|
console.log("Calling pool.query inside refreshQuoteOutcomes...");
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log(`Returned: ${records.length} in ${Date.now() - t0}ms`);
|
||||||
|
} catch(e) { console.error("Error:", e); }
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main();
|
||||||
20
test-outcomes-4.mjs
Normal file
20
test-outcomes-4.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
import { deriveInventoryDeltas } from './src/core/quote-outcomes.mjs';
|
||||||
|
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Starting debug run...");
|
||||||
|
try {
|
||||||
|
const t0 = Date.now();
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log(`Returned: ${records.length} in ${Date.now() - t0}ms`);
|
||||||
|
} catch(e) { console.error("Error:", e); }
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
23
test-outcomes-5.mjs
Normal file
23
test-outcomes-5.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Starting debug run...");
|
||||||
|
try {
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log("Returned records:", records.length);
|
||||||
|
let completed = 0;
|
||||||
|
for (const r of records) {
|
||||||
|
if (r.outcome_status === 'completed') completed++;
|
||||||
|
}
|
||||||
|
console.log("Completed outcomes:", completed);
|
||||||
|
} catch(e) { console.error("Error:", e); }
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
16
test-outcomes.mjs
Normal file
16
test-outcomes.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
import { refreshQuoteOutcomes } from './src/lib/postgres.mjs';
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
async function main() {
|
||||||
|
console.log("Starting refreshQuoteOutcomes...");
|
||||||
|
try {
|
||||||
|
const records = await refreshQuoteOutcomes(pool, {
|
||||||
|
btcAsset: { assetId: 'nep141:nbtc.bridge.near' },
|
||||||
|
eureAsset: { assetId: 'nep141:eure.omft.near' }
|
||||||
|
});
|
||||||
|
console.log("Returned records:", records.length);
|
||||||
|
console.log("Completed:", records.filter(r => r.outcome_status === 'completed').length);
|
||||||
|
} catch(e) { console.error("Error:", e); }
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main();
|
||||||
14
test-perf.mjs
Normal file
14
test-perf.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { deriveQuoteOutcomeRecords } from './src/core/quote-outcomes.mjs';
|
||||||
|
const submissions = Array.from({length: 2000}).map((_, i) => ({
|
||||||
|
quote_id: 'q' + i,
|
||||||
|
submitted_at: new Date().toISOString(),
|
||||||
|
expected_inventory_delta: { asset1: 10n }
|
||||||
|
}));
|
||||||
|
const inventorySnapshots = Array.from({length: 5000}).map((_, i) => ({
|
||||||
|
inventory_id: 'i' + i,
|
||||||
|
observed_at: new Date(Date.now() + i * 1000).toISOString(),
|
||||||
|
asset_balances: { asset1: BigInt(i * 10) }
|
||||||
|
}));
|
||||||
|
console.time('derive');
|
||||||
|
deriveQuoteOutcomeRecords({ submissions, inventorySnapshots });
|
||||||
|
console.timeEnd('derive');
|
||||||
14
test-recent.mjs
Normal file
14
test-recent.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { createPostgresPool } from './src/lib/postgres.mjs';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pool = createPostgresPool({ connectionString: process.env.POSTGRES_URL });
|
||||||
|
const inventoryRows = await pool.query(`
|
||||||
|
SELECT event_id as inventory_id, observed_at, payload
|
||||||
|
FROM intent_inventory_snapshots
|
||||||
|
ORDER BY COALESCE(observed_at, ingested_at) DESC
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
console.log("Most recent snapshot:", inventoryRows.rows[0].observed_at);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
main().catch(console.error);
|
||||||
39
test/operator-dashboard-dev-script.test.mjs
Normal file
39
test/operator-dashboard-dev-script.test.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import net from 'node:net';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = resolve(__dirname, '..');
|
||||||
|
const scriptPath = resolve(repoRoot, 'scripts/dev/operator-dashboard-dev.sh');
|
||||||
|
|
||||||
|
test('operator dashboard dev script fails fast on a stale occupied upstream port', async (t) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
await new Promise((resolvePromise, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', resolvePromise);
|
||||||
|
});
|
||||||
|
t.after(() => server.close());
|
||||||
|
|
||||||
|
const port = server.address().port;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const result = spawnSync('bash', [scriptPath], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 2_000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
OPERATOR_DASHBOARD_DEV_BASIC_AUTH_PASSWORD: 'test-password',
|
||||||
|
OPERATOR_DASHBOARD_DEV_UPSTREAM_PORT: String(port),
|
||||||
|
OPERATOR_DASHBOARD_DEV_EXISTING_PORT_TIMEOUT_SECONDS: '0.2',
|
||||||
|
OPERATOR_DASHBOARD_DEV_READY_ATTEMPTS: '50',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.notEqual(result.error?.code, 'ETIMEDOUT');
|
||||||
|
assert.notEqual(result.status, 0);
|
||||||
|
assert.ok(Date.now() - startedAt < 1_500);
|
||||||
|
assert.match(result.stderr, /already in use but \/healthz did not respond/);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue