Skip to content

Commit 3f3d43f

Browse files
committed
feat: unified market discovery with browse, search, and pagination
Refactor market reference discovery to support both search and browse modes with server-side pagination across all market adapters. Markets: - Polymarket: add browse support with sort options (volume, liquidity, newest, ending soon), paginated search, and structured reference results with price/volume/liquidity/endDate metadata - Hyperliquid: add browse support sorted by volume, paginated results, and unified reference result format - Extend MarketAdapter interface with browseReferences(), browseOptions, and pagination (limit/offset) for both search and browse API: - Add GET /markets/:id/browse endpoint with sort/limit/offset params - Update search endpoint to support limit/offset pagination - Rename 'symbol' to 'reference' throughout order placement pipeline Core: - Update schemas to use 'reference' instead of 'symbol' in order inputs - Add BrowseOption and pagination types to market adapter interface Web UI: - Redesign MarketSearchPanel with tabbed browse/search, sort controls, stat grid cards with price/volume/liquidity/endDate, hover glow effects, and load-more pagination - Add market card animations (card-glow-pulse, radial hover gradient) - Update TradeTicketCard and PortfolioPanels styling - Add items-start to grid layout to prevent column stretching Docs: - Update README, admin guide, API reference, architecture, testing, trading agent, and trading model docs to reflect new discovery model
1 parent c6cf9e0 commit 3f3d43f

26 files changed

Lines changed: 1503 additions & 1432 deletions

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A self-hosted paper trading engine with a clean REST API. Simulated trading acro
66

77
- **Market agnostic** — unified API across all markets, discover capabilities at runtime
88
- **Polymarket** — prediction market trading with live odds from the CLOB API
9-
- **Hyperliquid** — perpetual futures with symbol-level fractional size precision and max leverage limits
9+
- **Hyperliquid** — perpetual futures with reference-level fractional size precision and max leverage limits
1010
- **Extensible** — add new markets by implementing a simple adapter interface
1111
- **Agent-friendly** — skill-based integration with version-aware SSE events, self-describing market capabilities
1212
- **Decision transparency** — every action requires reasoning; journal + timeline for full audit trail
@@ -56,19 +56,19 @@ You can start from [.env.example](.env.example).
5656

5757
### Trading Constraints
5858

59-
Order payload `quantity` is decimal-capable at schema layer, then validated per market/symbol.
59+
Order payload `quantity` is decimal-capable at schema layer, then validated per market/reference.
6060

6161
Discover constraints before placing orders:
6262

6363
```bash
64-
GET /api/markets/:market/trading-constraints?symbol=<symbol>
64+
GET /api/markets/:market/trading-constraints?reference=<reference>
6565
```
6666

6767
Example response:
6868

6969
```json
7070
{
71-
"symbol": "BTC",
71+
"reference": "BTC",
7272
"constraints": {
7373
"minQuantity": 0.00001,
7474
"quantityStep": 0.00001,
@@ -80,7 +80,28 @@ Example response:
8080

8181
Notes:
8282
- Some markets require integer quantities (`supportsFractional: false`, usually `quantityStep: 1`).
83-
- Hyperliquid derives `quantityStep` and fractional support from `szDecimals`, and enforces symbol `maxLeverage`.
83+
- Search and browse surfaces now return lightweight market references. Execution endpoints (`quote`, `orderbook`, `resolve`, order placement) accept those references directly.
84+
- Discovery is intentionally separate from execution: `browse` and `search` help humans and agents find candidates quickly, then adapters lazily normalize the chosen `reference` only when a quote or order is requested.
85+
- For Polymarket, discovery references are typically market slugs. The adapter resolves those slugs into outcome token ids behind the scenes when you ask for quotes or place orders.
86+
- Hyperliquid derives `quantityStep` and fractional support from `szDecimals`, and enforces per-reference `maxLeverage`.
87+
- Browse sort options are market-specific and discoverable from `GET /api/markets`. Polymarket exposes `volume`, `liquidity`, `endingSoon`, and `newest`; Hyperliquid exposes `price`.
88+
89+
### Market Discovery
90+
91+
Typical discovery flow:
92+
93+
```bash
94+
GET /api/markets
95+
GET /api/markets/:market/browse?sort=<market-specific-sort>
96+
GET /api/markets/:market/search?q=iran
97+
GET /api/markets/:market/quote?reference=<reference>
98+
POST /api/orders
99+
```
100+
101+
The platform now treats `reference` as the single external identifier across markets:
102+
- Polymarket: usually a slug during discovery, resolved lazily to a token id for execution
103+
- Hyperliquid: usually a ticker such as `BTC`
104+
- Future markets: whatever adapter-specific identifier makes the most sense externally
84105

85106
### Running the Server
86107

docs/admin-guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ A user detail view shows:
4949
The trade page allows an admin to:
5050
- create a new trader account
5151
- choose a market dynamically from runtime discovery
52-
- search assets
52+
- search and browse market references
5353
- inspect quotes and trading constraints
5454
- select a target user
5555
- place market or limit orders on that user's behalf
@@ -110,7 +110,7 @@ Important rules:
110110
- optional `accountId` must match the user's default account if supplied
111111
- `Idempotency-Key` is supported and should be used by the dashboard or scripts for retry-safe writes
112112
- `leverage` and `reduceOnly` are only valid for perp markets
113-
- symbol normalization and trading-constraint validation still apply
113+
- reference normalization and trading-constraint validation still apply
114114
- market orders fill immediately using directional executable prices
115115
- limit orders remain pending until the background reconciler fills or cancels them
116116

docs/api-reference.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Notes:
5050

5151
Required fields:
5252
- `market`
53-
- `symbol`
53+
- `reference`
5454
- `side`
5555
- `type`
5656
- `quantity`
@@ -64,7 +64,7 @@ Optional fields:
6464
Rules:
6565
- `reasoning` is required for state-changing writes
6666
- `leverage` and `reduceOnly` are valid only for perp markets
67-
- quantity is validated against per-symbol trading constraints
67+
- quantity is validated against per-reference trading constraints
6868
- normal market-order execution uses directional executable prices: `buy -> ask`, `sell -> bid`
6969
- limit orders remain `pending` until the background reconciler fills or cancels them
7070

@@ -86,8 +86,9 @@ Rules:
8686
| Method | Endpoint | Auth | Description |
8787
|--------|----------|------|-------------|
8888
| `GET` | `/api/markets` | key or admin | Discover registered markets and capabilities |
89-
| `GET` | `/api/markets/:market/search` | key or admin | Search or browse assets |
90-
| `GET` | `/api/markets/:market/trading-constraints` | key or admin | Get symbol-level quantity and leverage constraints |
89+
| `GET` | `/api/markets/:market/search` | key or admin | Search market references |
90+
| `GET` | `/api/markets/:market/browse` | key or admin | Browse active market references |
91+
| `GET` | `/api/markets/:market/trading-constraints` | key or admin | Get reference-level quantity and leverage constraints |
9192
| `GET` | `/api/markets/:market/quote` | key or admin | Get one quote |
9293
| `GET` | `/api/markets/:market/quotes` | key or admin | Get quotes in batch |
9394
| `GET` | `/api/markets/:market/orderbook` | key or admin | Get one orderbook |
@@ -96,13 +97,38 @@ Rules:
9697
| `GET` | `/api/markets/:market/fundings` | key or admin | Get funding rates in batch |
9798
| `GET` | `/api/markets/:market/resolve` | key or admin | Get settlement or resolution status |
9899

100+
### Search and browse contract
101+
102+
- `search` requires a non-empty `q`
103+
- `browse` is explicit and accepts a market-specific `sort` string
104+
- browse options are discoverable from `GET /api/markets`
105+
- current defaults:
106+
- Polymarket: `volume`, `liquidity`, `endingSoon`, `newest`
107+
- Hyperliquid: `price`
108+
- both endpoints return lightweight discovery records shaped like:
109+
110+
```json
111+
{
112+
"reference": "btc",
113+
"name": "BTC-PERP",
114+
"price": 94321.1,
115+
"volume": 12003455.2,
116+
"liquidity": 882100.4,
117+
"endDate": null,
118+
"metadata": {}
119+
}
120+
```
121+
122+
- discovery results are not required to be execution-ready exchange ids
123+
- adapters normalize the supplied `reference` lazily when `quote`, `orderbook`, `resolve`, or order placement is called
124+
99125
### Trading constraints response
100126

101127
Example:
102128

103129
```json
104130
{
105-
"symbol": "BTC",
131+
"reference": "BTC",
106132
"constraints": {
107133
"minQuantity": 0.00001,
108134
"quantityStep": 0.00001,
@@ -156,6 +182,9 @@ The structured liquidation event includes:
156182
- `cancelledReduceOnlyOrderIds`
157183
- `liquidatedAt`
158184

185+
Note:
186+
- timeline and settlement/liquidation audit surfaces still expose internal normalized `symbol` fields because accounting is stored against resolved execution identifiers
187+
159188
## Admin API
160189

161190
The full operator workflow is documented in [Admin Guide](admin-guide.md). The main admin-only endpoints are:

docs/architecture.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,14 @@ It should not know anything about:
109109
The `markets` package owns market-specific reads.
110110

111111
Adapters expose a common interface so the rest of the system can ask for:
112-
- searchable assets
112+
- searchable market references
113+
- browseable market references
113114
- quotes
114115
- orderbooks
115116
- funding rates
116117
- resolution information
117118
- trading constraints
118-
- symbol normalization when needed
119+
- reference normalization when needed
119120

120121
Adapters do not execute trades. They are data providers for the simulation engine.
121122

@@ -161,18 +162,19 @@ Examples:
161162
- markets with `funding` are treated as perp markets
162163
- markets with `resolve` support settlement checks
163164
- markets with `orderbook` can expose live depth
164-
- markets with `search` can drive browse/search UX
165+
- markets with `search` and `browse` can drive discovery UX for humans and agents
165166

166167
This avoids hardcoding business logic around a market name such as `if market === "hyperliquid"`.
167168

168169
The adapter contract currently centers around methods like:
169170
- `search(query)`
170-
- `getQuote(symbol)`
171-
- `getOrderbook(symbol)`
172-
- `getFundingRate(symbol)`
173-
- `resolve(symbol)`
174-
- `getTradingConstraints(symbol)`
175-
- `normalizeSymbol(symbol)`
171+
- `browse(options)`
172+
- `getQuote(reference)`
173+
- `getOrderbook(reference)`
174+
- `getFundingRate(reference)`
175+
- `resolve(reference)`
176+
- `getTradingConstraints(reference)`
177+
- `normalizeReference(reference)`
176178

177179
## Request Flow
178180

@@ -182,7 +184,7 @@ A typical order request follows this path.
182184
2. Route resolves the acting identity and target account.
183185
3. Route delegates to the shared order-placement service.
184186
4. The service loads the adapter from the registry.
185-
5. The service normalizes the symbol and validates trading constraints.
187+
5. The service normalizes the external reference and validates trading constraints.
186188
6. The service fetches a quote.
187189
7. The service chooses spot or perp engine based on capabilities.
188190
8. The service performs transactional writes to accounts, orders, trades, and positions.

docs/testing.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,40 +118,45 @@ TRADE_SYMBOL=""
118118
while read -r MARKET_ID; do
119119
[[ -n "$MARKET_ID" ]] || continue
120120

121-
SEARCH_PAYLOAD="$(auth_get "/api/markets/$MARKET_ID/search?limit=1")"
122-
SYMBOL="$(jq -r '.results[0].symbol // empty' <<<"$SEARCH_PAYLOAD")"
123-
[[ -n "$SYMBOL" ]] || continue
121+
SORT="$(jq -r --arg m "$MARKET_ID" '.markets[] | select(.id == $m) | .browseOptions[0].value // empty' <<<"$MARKETS_PAYLOAD")"
122+
BROWSE_URL="/api/markets/$MARKET_ID/browse?limit=1"
123+
if [[ -n "$SORT" ]]; then
124+
BROWSE_URL="$BROWSE_URL&sort=$SORT"
125+
fi
126+
BROWSE_PAYLOAD="$(auth_get "$BROWSE_URL")"
127+
REFERENCE="$(jq -r '.results[0].reference // empty' <<<"$BROWSE_PAYLOAD")"
128+
[[ -n "$REFERENCE" ]] || continue
124129

125130
CAPS="$(jq -r --arg m "$MARKET_ID" '.markets[] | select(.id == $m) | .capabilities[]?' <<<"$MARKETS_PAYLOAD")"
126131
if grep -qx "quote" <<<"$CAPS"; then
127-
auth_get "/api/markets/$MARKET_ID/quote?symbol=$SYMBOL" >/dev/null
132+
auth_get "/api/markets/$MARKET_ID/quote?reference=$REFERENCE" >/dev/null
128133
fi
129134
if grep -qx "orderbook" <<<"$CAPS"; then
130-
auth_get "/api/markets/$MARKET_ID/orderbook?symbol=$SYMBOL" >/dev/null
135+
auth_get "/api/markets/$MARKET_ID/orderbook?reference=$REFERENCE" >/dev/null
131136
fi
132137
if grep -qx "funding" <<<"$CAPS"; then
133-
auth_get "/api/markets/$MARKET_ID/funding?symbol=$SYMBOL" >/dev/null
138+
auth_get "/api/markets/$MARKET_ID/funding?reference=$REFERENCE" >/dev/null
134139
fi
135140
if grep -qx "resolve" <<<"$CAPS"; then
136-
auth_get "/api/markets/$MARKET_ID/resolve?symbol=$SYMBOL" >/dev/null
141+
auth_get "/api/markets/$MARKET_ID/resolve?reference=$REFERENCE" >/dev/null
137142
fi
138143

139144
if [[ -z "$TRADE_MARKET" ]]; then
140145
TRADE_MARKET="$MARKET_ID"
141-
TRADE_SYMBOL="$SYMBOL"
146+
TRADE_SYMBOL="$REFERENCE"
142147
fi
143148
done < <(jq -r '.markets[].id' <<<"$MARKETS_PAYLOAD")
144149

145150
[[ -n "$TRADE_MARKET" && -n "$TRADE_SYMBOL" ]] || {
146-
echo "no tradeable symbol found"
151+
echo "no tradeable reference found"
147152
exit 1
148153
}
149154

150155
echo "[3/8] Place market order"
151156
MARKET_ORDER_PAYLOAD="$(auth_post "/api/orders" "$(jq -nc \
152157
--arg m "$TRADE_MARKET" \
153158
--arg s "$TRADE_SYMBOL" \
154-
'{market:$m,symbol:$s,side:"buy",type:"market",quantity:1,reasoning:"e2e smoke: open starter position"}'
159+
'{market:$m,reference:$s,side:"buy",type:"market",quantity:1,reasoning:"e2e smoke: open starter position"}'
155160
)")"
156161
MARKET_ORDER_ID="$(jq -r '.id // empty' <<<"$MARKET_ORDER_PAYLOAD")"
157162
[[ -n "$MARKET_ORDER_ID" ]] || { echo "market order failed: $MARKET_ORDER_PAYLOAD"; exit 1; }
@@ -160,7 +165,7 @@ echo "[4/8] Place and cancel pending limit order"
160165
LIMIT_ORDER_PAYLOAD="$(auth_post "/api/orders" "$(jq -nc \
161166
--arg m "$TRADE_MARKET" \
162167
--arg s "$TRADE_SYMBOL" \
163-
'{market:$m,symbol:$s,side:"sell",type:"limit",quantity:1,limitPrice:0.99,reasoning:"e2e smoke: pending order for cancel flow"}'
168+
'{market:$m,reference:$s,side:"sell",type:"limit",quantity:1,limitPrice:0.99,reasoning:"e2e smoke: pending order for cancel flow"}'
164169
)")"
165170
LIMIT_ORDER_ID="$(jq -r '.id // empty' <<<"$LIMIT_ORDER_PAYLOAD")"
166171
[[ -n "$LIMIT_ORDER_ID" ]] || { echo "limit order failed: $LIMIT_ORDER_PAYLOAD"; exit 1; }
@@ -195,7 +200,7 @@ MISSING_REASONING_CODE="$(curl -sS -o /tmp/unimarket-missing-reasoning.out -w "%
195200
-X POST "$BASE_URL/api/orders" \
196201
-H "Authorization: Bearer $API_KEY" \
197202
-H "Content-Type: application/json" \
198-
-d "{\"market\":\"$TRADE_MARKET\",\"symbol\":\"$TRADE_SYMBOL\",\"side\":\"buy\",\"type\":\"market\",\"quantity\":1}")"
203+
-d "{\"market\":\"$TRADE_MARKET\",\"reference\":\"$TRADE_SYMBOL\",\"side\":\"buy\",\"type\":\"market\",\"quantity\":1}")"
199204
[[ "$MISSING_REASONING_CODE" == "400" ]] || { echo "expected 400 for missing reasoning"; exit 1; }
200205

201206
echo "[8/8] Optional admin checks"

docs/trading-agent.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,23 @@ Pragmatic starting defaults:
152152

153153
Use a repeatable sequence:
154154
1. `GET /api/markets`
155-
2. `GET /api/markets/:market/search`
156-
3. `GET /api/markets/:market/quotes`
157-
4. `GET /api/markets/:market/orderbooks`
158-
5. optional `funding` and `resolve` reads when the market supports them
159-
6. account, portfolio, positions, and order reads
160-
7. decision and journal write
155+
2. `GET /api/markets/:market/browse`
156+
3. optional `GET /api/markets/:market/search` when the agent has a concrete query
157+
4. keep the returned `reference` for the candidate you want to investigate
158+
5. `GET /api/markets/:market/quotes`
159+
6. `GET /api/markets/:market/orderbooks`
160+
7. optional `funding` and `resolve` reads when the market supports them
161+
8. account, portfolio, positions, and order reads
162+
9. decision and journal write
161163

162164
## What the Agent Should Know About the Platform
163165

164166
A few design facts help agents make better decisions.
165167

166168
- unimarket is simulation-first; it does not place real exchange trades in core flows
169+
- discovery surfaces return market `reference` values; execution endpoints accept the same reference and let adapters normalize it internally
170+
- the agent should treat `reference` as the only external market identifier it needs to persist between discovery and execution
171+
- on Polymarket, a discovery `reference` is usually a slug preview, not an already-resolved token id
167172
- markets without `funding` behave like spot inventory
168173
- prediction-market bearish views are expressed by buying the opposite outcome token, not by opening a naked short
169174
- markets with `funding` behave like perp markets with leverage, funding, and liquidation

docs/trading-model.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ For prediction markets, directional expression is outcome-based:
4242

4343
For Polymarket specifically:
4444
- asset discovery comes from Gamma
45-
- query search uses Gamma `search-v2`, then hydrates selected market slugs through `/markets`
45+
- query search uses Gamma `search-v2`
46+
- browse uses Gamma `/events`
47+
- external discovery results use lightweight market `reference` values, typically Polymarket slugs
4648
- quotes and orderbooks come from the CLOB API
47-
- symbol normalization can map a `conditionId` to a concrete outcome `tokenId`
48-
- if you want a specific outcome such as `NO`, pass the corresponding token id explicitly
49+
- reference normalization can map a slug or `conditionId` to a concrete outcome `tokenId`
50+
- if you want a specific outcome such as `NO`, pass the corresponding token id explicitly as the reference
4951

5052
### Perp-like markets
5153

@@ -69,8 +71,8 @@ All state-changing order flows follow the same broad structure.
6971

7072
1. Validate request payload.
7173
2. Resolve the market adapter.
72-
3. Normalize the symbol if the adapter supports normalization.
73-
4. Load symbol-level trading constraints.
74+
3. Normalize the external reference if the adapter supports normalization.
75+
4. Load reference-level trading constraints.
7476
5. Validate quantity step, minimum quantity, fractional support, and optional max leverage.
7577
6. Fetch a fresh quote.
7678
7. Either fill immediately or store a pending limit order.
@@ -117,7 +119,7 @@ Future evolution:
117119

118120
## Trading Constraints
119121

120-
Every market may expose symbol-level constraints through `getTradingConstraints(symbol)`.
122+
Every market may expose reference-level constraints through `getTradingConstraints(reference)`.
121123

122124
The engine uses these fields:
123125
- `minQuantity`
@@ -131,7 +133,7 @@ If a market does not provide custom constraints, unimarket falls back to:
131133
- `supportsFractional = false`
132134
- `maxLeverage = null`
133135

134-
This keeps order validation market-agnostic while still allowing symbol-specific precision and leverage rules.
136+
This keeps order validation market-agnostic while still allowing reference-specific precision and leverage rules.
135137

136138
## Spot Position Accounting
137139

0 commit comments

Comments
 (0)