Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 204 additions & 1 deletion internal/migrations/038-order-book-queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,207 @@ PUBLIC VIEW RETURNS TABLE(
{
RETURN NEXT $pos.query_id, $pos.outcome, $pos.price, $pos.amount, $pos.position_type;
}
};
};

/**
* get_market_depth($query_id, $outcome)
*
* Aggregate order volume at each price level (for depth chart visualization).
* Useful for UIs showing market liquidity distribution.
*
* Parameters:
* - $query_id: Market ID
* - $outcome: TRUE for YES, FALSE for NO
*
* Returns TABLE of:
* - price: Absolute price level (1-99)
* - buy_volume: Total shares in buy orders at this price
* - sell_volume: Total shares in sell orders at this price
*
* Sorting: By price ascending (best prices first)
*
* Usage:
* kwil-cli database call --action get_market_depth \
* --inputs '[{"$query_id": 1, "$outcome": true}]'
*
* Example Output:
* price | buy_volume | sell_volume
* 50 | 5000 | 3000
* 51 | 2000 | 1500
* 52 | 1000 | 0
*/
CREATE OR REPLACE ACTION get_market_depth(
$query_id INT,
$outcome BOOL
) PUBLIC VIEW RETURNS TABLE(
price INT,
buy_volume INT8,
sell_volume INT8
) {
if $query_id IS NULL {
ERROR('query_id is required');
}
if $outcome IS NULL {
ERROR('outcome is required');
}

-- Aggregate volume at each price level
for $depth in
SELECT
abs(price) as abs_price,
COALESCE(SUM(CASE WHEN price < 0 THEN amount ELSE 0::INT8 END)::INT8, 0::INT8) as buy_vol,
COALESCE(SUM(CASE WHEN price > 0 THEN amount ELSE 0::INT8 END)::INT8, 0::INT8) as sell_vol
FROM ob_positions
WHERE query_id = $query_id
AND outcome = $outcome
AND price != 0 -- Exclude holdings
GROUP BY abs(price)
ORDER BY abs(price) ASC
{
RETURN NEXT $depth.abs_price, $depth.buy_vol, $depth.sell_vol;
}
};

/**
* get_best_prices($query_id, $outcome)
*
* Get current best bid (highest buy) and best ask (lowest sell) for a market.
* Shows the current market spread.
*
* Parameters:
* - $query_id: Market ID
* - $outcome: TRUE for YES, FALSE for NO
*
* Returns:
* - best_bid: Highest buy order price (NULL if no buy orders)
* - best_ask: Lowest sell order price (NULL if no sell orders)
* - spread: Difference between ask and bid (NULL if either side missing)
*
* Usage:
* kwil-cli database call --action get_best_prices \
* --inputs '[{"$query_id": 1, "$outcome": true}]'
*
* Example Output:
* best_bid | best_ask | spread
* 55 | 58 | 3
*/
CREATE OR REPLACE ACTION get_best_prices(
$query_id INT,
$outcome BOOL
) PUBLIC VIEW RETURNS (
best_bid INT,
best_ask INT,
spread INT
) {
if $query_id IS NULL {
ERROR('query_id is required');
}
if $outcome IS NULL {
ERROR('outcome is required');
}

-- Get best bid (highest buy order)
-- Buy orders have negative price, so MAX(ABS(price)) gives the highest bid
$best_bid INT;
for $row in
SELECT COALESCE(MAX(abs(price)), NULL)::INT as max_bid
FROM ob_positions
WHERE query_id = $query_id
AND outcome = $outcome
AND price < 0
{
$best_bid := $row.max_bid;
}

-- Get best ask (lowest sell order)
$best_ask INT;
for $row in
SELECT COALESCE(MIN(price), NULL)::INT as min_ask
FROM ob_positions
WHERE query_id = $query_id
AND outcome = $outcome
AND price > 0
{
$best_ask := $row.min_ask;
}

-- Calculate spread
$spread INT;
if $best_bid IS NOT NULL AND $best_ask IS NOT NULL {
$spread := $best_ask - $best_bid;
}

RETURN $best_bid, $best_ask, $spread;
};

/**
* get_user_collateral()
*
* Show caller's total collateral locked across all markets.
* Useful for user dashboards showing "total value locked in prediction markets".
*
* Returns:
* - total_locked: Total collateral (shares value + buy orders locked)
* - buy_orders_locked: Collateral locked in open buy orders
* - shares_value: Value of shares held (holdings + open sells, at $1.00 per share)
*
* All values in wei (18 decimals).
*
* Usage:
* kwil-cli database call --action get_user_collateral
*
* Example Output:
* total_locked | buy_orders_locked | shares_value
* 2500000000000000000000 | 500000000000000000000 | 2000000000000000000000
* (2500 tokens) | (500 tokens) | (2000 tokens)
*/
CREATE OR REPLACE ACTION get_user_collateral()
PUBLIC VIEW RETURNS (
total_locked NUMERIC(78, 0),
buy_orders_locked NUMERIC(78, 0),
shares_value NUMERIC(78, 0)
) {
-- Get caller's wallet address
$caller_bytes BYTEA := decode(substring(LOWER(@caller), 3, 40), 'hex');

-- Lookup participant ID
$participant_id INT;
for $row in SELECT id FROM ob_participants WHERE wallet_address = $caller_bytes {
$participant_id := $row.id;
}

-- Return zeros if participant doesn't exist
if $participant_id IS NULL {
RETURN 0::NUMERIC(78, 0), 0::NUMERIC(78, 0), 0::NUMERIC(78, 0);
}

-- Calculate locked collateral from buy orders
-- Buy order collateral = |price| * amount * 0.01 (price in cents)
-- Convert to wei: multiply by 10^18, divide by 100
$buy_locked NUMERIC(78, 0);
for $row in
SELECT COALESCE(SUM(abs(price)::NUMERIC(78, 0) * amount::NUMERIC(78, 0) * '10000000000000000'::NUMERIC(78, 0))::NUMERIC(78, 0), 0::NUMERIC(78, 0)) as total
FROM ob_positions
WHERE participant_id = $participant_id
AND price < 0
{
$buy_locked := $row.total;
}

-- Calculate value of shares held (holdings + open sells)
-- Each share = $1.00 = 10^18 wei
$shares_value NUMERIC(78, 0);
for $row in
SELECT COALESCE(SUM(amount::NUMERIC(78, 0) * '1000000000000000000'::NUMERIC(78, 0))::NUMERIC(78, 0), 0::NUMERIC(78, 0)) as total
FROM ob_positions
WHERE participant_id = $participant_id
AND price >= 0
{
$shares_value := $row.total;
}

-- Total locked collateral
$total_locked NUMERIC(78, 0) := $buy_locked + $shares_value;

RETURN $total_locked, $buy_locked, $shares_value;
};
20 changes: 10 additions & 10 deletions tests/streams/order_book/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,20 @@ func TestQueries(t *testing.T) {
testGetUserPositionsMixed(t),

// get_market_depth tests
//testGetMarketDepthEmpty(t),
//testGetMarketDepthAggregation(t),
testGetMarketDepthEmpty(t),
testGetMarketDepthAggregation(t),

// get_best_prices tests
//testGetBestPricesNoOrders(t),
//testGetBestPricesOnlyBuy(t),
//testGetBestPricesOnlySell(t),
//testGetBestPricesBothSides(t),
testGetBestPricesNoOrders(t),
testGetBestPricesOnlyBuy(t),
testGetBestPricesOnlySell(t),
testGetBestPricesBothSides(t),

// get_user_collateral tests
//testGetUserCollateralEmpty(t),
//testGetUserCollateralWithBuyOrders(t),
//testGetUserCollateralWithShares(t),
//testGetUserCollateralMixed(t),
testGetUserCollateralEmpty(t),
testGetUserCollateralWithBuyOrders(t),
testGetUserCollateralWithShares(t),
testGetUserCollateralMixed(t),
},
}, testutils.GetTestOptionsWithCache())
}
Expand Down
Loading