mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
feat(market): add sell orders view and default to 7-day window
- New "Sell orders" tab / sort=sellRecent: items with open asks, newest listing first, showing ask, sell qty, sell value and last listed time. - MarketOverview and item quotes now include sellValue and lastSellAt. - SDK MARKET_SORTS (and so the get_market MCP tool) accept sellRecent. - Market list and API default to days=7 instead of 24h. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
ccf011d91c
commit
002303875c
8 files changed
+104
-33
No files matched your search
+6
-2
@@ -1021,9 +1021,13 @@ interface MarketQuote {
|
||||
/** Coins committed to unfilled bids, independent of the selected period. */
|
||||
buyValue: number;
|
||||
sellQuantity: number;
|
||||
/** Completed item quantity in the selected period (24 hours by default). */
|
||||
/** Coins asked for unfilled sell offers, independent of the selected period. */
|
||||
sellValue: number;
|
||||
/** Newest open sell offer creation time (Unix ms), or null without open asks. */
|
||||
lastSellAt: number | null;
|
||||
/** Completed item quantity in the selected period (7 days by default). */
|
||||
volume: number;
|
||||
/** Gross coins exchanged in the selected period (24 hours by default). */
|
||||
/** Gross coins exchanged in the selected period (7 days by default). */
|
||||
gross: number;
|
||||
tax: number;
|
||||
trades: number;
|
||||
|
||||
+7
-3
@@ -153,14 +153,16 @@ No authentication is required. Responses may be cached for five seconds.
|
||||
| `/api/market/items/{id}` | Canonical, unnoted tradeable item ID | `MarketQuote` |
|
||||
| `/api/market/items/{id}/history` | `from`, `to`: Unix milliseconds; `interval`: 3600000 (hour) or 86400000 (day) | `{ item, from, to, interval, history }` |
|
||||
|
||||
The list and item endpoints also accept `days=1|7|30|90` (default `1`).
|
||||
The list accepts `sort=relevance|name|name-desc|recent|rises|falls|marketCap|price|volume|value|buyValue`
|
||||
The list and item endpoints also accept `days=1|7|30|90` (default `7`).
|
||||
The list accepts `sort=relevance|name|name-desc|recent|rises|falls|marketCap|price|volume|value|buyValue|sellRecent`
|
||||
(default `relevance` when searching, `name` otherwise). Rankings apply before pagination;
|
||||
`rises`/`falls` include only positive/negative changes with trades in the period.
|
||||
`price` ranks by last unit price, `volume` by units traded, and `value` by gross
|
||||
trade value. `buyValue` ranks the current open buy book by the sum of remaining
|
||||
quantity × bid price for each item, including items without completed trades.
|
||||
This value is independent of `days` and excludes filled and cancelled quantities.
|
||||
`sellRecent` lists items with open sell offers, newest listing first; like
|
||||
`buyValue` it describes the current book and ignores `days`.
|
||||
Search filters the chosen ranking. The response also includes
|
||||
`days`, `sort`, `updatedAt`, a market-wide `summary` (`volume`, `gross`,
|
||||
`activeItems`, `totalItems`), and the most-traded `featured` item, or `null`.
|
||||
@@ -194,8 +196,10 @@ are omitted, never filled with invented prices or volume.
|
||||
| `bid`, `ask` | Highest open buy limit / lowest open sell limit; `null` if absent |
|
||||
| `buyQuantity`, `sellQuantity` | Remaining quantities in open offers |
|
||||
| `buyValue` | Total gp committed to unfilled open buy offers, at each offer’s bid price |
|
||||
| `sellValue` | Total gp asked for unfilled open sell offers, at each offer’s ask price |
|
||||
| `lastSellAt` | Creation time of the newest open sell offer (Unix ms), or `null` |
|
||||
| `lastPrice`, `lastTradeAt` | Most recent execution price and Unix milliseconds, or `null` |
|
||||
| `volume` | Actual item units exchanged in the selected period (24 hours by default) |
|
||||
| `volume` | Actual item units exchanged in the selected period (7 days by default) |
|
||||
| `gross`, `tax`, `trades` | Selected-period gross coins, coins burned, and number of matched fills |
|
||||
| `vwap` | Selected-period quantity-weighted average price; `null` without trades |
|
||||
|
||||
|
||||
+7
-3
@@ -1,5 +1,5 @@
|
||||
/** Public, read-only Grand Exchange market data. Trading stays in the game. */
|
||||
export const MARKET_SORTS = ['relevance', 'name', 'name-desc', 'recent', 'rises', 'falls', 'marketCap', 'price', 'volume', 'value', 'buyValue'] as const;
|
||||
export const MARKET_SORTS = ['relevance', 'name', 'name-desc', 'recent', 'rises', 'falls', 'marketCap', 'price', 'volume', 'value', 'buyValue', 'sellRecent'] as const;
|
||||
export type MarketSort = (typeof MARKET_SORTS)[number];
|
||||
export type MarketDays = 1 | 7 | 30 | 90;
|
||||
export interface MarketItemOptions {
|
||||
@@ -26,9 +26,13 @@ export interface MarketQuote {
|
||||
/** Coins committed to unfilled bids, independent of the selected period. */
|
||||
buyValue: number;
|
||||
sellQuantity: number;
|
||||
/** Completed item quantity in the selected period (24 hours by default). */
|
||||
/** Coins asked for unfilled sell offers, independent of the selected period. */
|
||||
sellValue: number;
|
||||
/** Newest open sell offer creation time (Unix ms), or null without open asks. */
|
||||
lastSellAt: number | null;
|
||||
/** Completed item quantity in the selected period (7 days by default). */
|
||||
volume: number;
|
||||
/** Gross coins exchanged in the selected period (24 hours by default). */
|
||||
/** Gross coins exchanged in the selected period (7 days by default). */
|
||||
gross: number;
|
||||
tax: number;
|
||||
trades: number;
|
||||
|
||||
@@ -892,10 +892,11 @@
|
||||
<div class="tabs" id="sort-tabs" role="group" aria-label="Market views">
|
||||
<button data-sort="recent" class="active" aria-pressed="true">Recent trades</button>
|
||||
<button data-sort="buyValue" aria-pressed="false">Buy orders</button>
|
||||
<button data-sort="sellRecent" aria-pressed="false">Sell orders</button>
|
||||
<button data-sort="value" class="metric-tab" aria-pressed="false">
|
||||
<img src="/sprite/item/995-10000.png" alt="" />
|
||||
<span
|
||||
><span class="tab-label">Trade value · <span class="window-label">24h</span></span
|
||||
><span class="tab-label">Trade value · <span class="window-label">7d</span></span
|
||||
><strong id="total-gold">—</strong> <span class="tab-unit">gp</span></span
|
||||
>
|
||||
</button>
|
||||
@@ -903,14 +904,14 @@
|
||||
<button data-sort="volume" class="metric-tab" aria-pressed="false">
|
||||
<img src="/sprite/item/1511.png" alt="" />
|
||||
<span
|
||||
><span class="tab-label">Most traded · <span class="window-label">24h</span></span
|
||||
><span class="tab-label">Most traded · <span class="window-label">7d</span></span
|
||||
><strong id="total-volume">—</strong></span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<div class="periods" id="list-periods" role="group" aria-label="Market time period">
|
||||
<button data-days="1" class="active" aria-pressed="true">24H</button><button data-days="7" aria-pressed="false">7D</button><button data-days="30" aria-pressed="false">30D</button
|
||||
<button data-days="1" aria-pressed="false">24H</button><button data-days="7" class="active" aria-pressed="true">7D</button><button data-days="30" aria-pressed="false">30D</button
|
||||
><button data-days="90" aria-pressed="false">90D</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -922,7 +923,7 @@
|
||||
<th scope="col" id="price-column">Price · gp</th>
|
||||
<th scope="col" id="change-column">Change</th>
|
||||
<th scope="col" id="metric-column">Volume</th>
|
||||
<th scope="col">Last traded</th>
|
||||
<th scope="col" id="time-column">Last traded</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items">
|
||||
@@ -1033,6 +1034,7 @@
|
||||
const titles = {
|
||||
recent: 'Recent trades',
|
||||
buyValue: 'Buy orders',
|
||||
sellRecent: 'Sell orders',
|
||||
value: 'Trade value',
|
||||
price: 'Biggest items',
|
||||
volume: 'Most traded',
|
||||
@@ -1041,7 +1043,7 @@
|
||||
let offset = 0,
|
||||
total = 0,
|
||||
sort = 'recent',
|
||||
days = 1,
|
||||
days = 7,
|
||||
detailDays = 30,
|
||||
selected = null,
|
||||
listSequence = 0,
|
||||
@@ -1105,11 +1107,14 @@
|
||||
$('list-title').textContent = titles[sort];
|
||||
activate('sort-tabs', 'sort', sort);
|
||||
activate('list-periods', 'days', days);
|
||||
const buyOrders = sort === 'buyValue';
|
||||
$('list-periods').parentElement.hidden = buyOrders;
|
||||
$('price-column').textContent = buyOrders ? 'Bid · gp' : 'Price · gp';
|
||||
$('change-column').textContent = buyOrders ? 'Buy qty' : 'Change';
|
||||
$('metric-column').textContent = buyOrders ? 'Buy value · gp' : sort === 'value' ? 'Trade value · gp' : 'Volume';
|
||||
const buyOrders = sort === 'buyValue',
|
||||
sellOrders = sort === 'sellRecent',
|
||||
orders = buyOrders || sellOrders;
|
||||
$('list-periods').parentElement.hidden = orders;
|
||||
$('price-column').textContent = buyOrders ? 'Bid · gp' : sellOrders ? 'Ask · gp' : 'Price · gp';
|
||||
$('change-column').textContent = buyOrders ? 'Buy qty' : sellOrders ? 'Sell qty' : 'Change';
|
||||
$('metric-column').textContent = buyOrders ? 'Buy value · gp' : sellOrders ? 'Sell value · gp' : sort === 'value' ? 'Trade value · gp' : 'Volume';
|
||||
$('time-column').textContent = sellOrders ? 'Last listed' : 'Last traded';
|
||||
try {
|
||||
const d = await api('?' + new URLSearchParams({ q: $('search').value, limit, offset, sort, days }));
|
||||
if (seq !== listSequence) return;
|
||||
@@ -1121,14 +1126,17 @@
|
||||
}
|
||||
$('items').innerHTML = d.items.length
|
||||
? d.items
|
||||
.map(
|
||||
item =>
|
||||
`<tr data-item="${item.id}"><td><a class="itemlink" href="${escape(itemHash(item.id))}">${sprite(item.id)}<span>${escape(item.name)}</span></a></td>
|
||||
<td title="${fmt(buyOrders ? item.bid : item.lastPrice)} gp">${short(buyOrders ? item.bid : item.lastPrice)}</td>
|
||||
<td class="${buyOrders ? '' : 'change ' + tone(item.changePercent)}" title="${buyOrders ? fmt(item.buyQuantity) : ''}">${buyOrders ? short(item.buyQuantity) : changeText(item.changePercent)}</td>
|
||||
<td title="${fmt(buyOrders ? item.buyValue : sort === 'value' ? item.gross : item.volume)}">${short(buyOrders ? item.buyValue : sort === 'value' ? item.gross : item.volume)}</td>
|
||||
<td class="recent" title="${item.lastTradeAt ? new Date(item.lastTradeAt).toLocaleString() : 'No completed trades'}">${relative(item.lastTradeAt)}</td></tr>`
|
||||
)
|
||||
.map(item => {
|
||||
const price = buyOrders ? item.bid : sellOrders ? item.ask : item.lastPrice,
|
||||
quantity = buyOrders ? item.buyQuantity : item.sellQuantity,
|
||||
metric = buyOrders ? item.buyValue : sellOrders ? item.sellValue : sort === 'value' ? item.gross : item.volume,
|
||||
time = sellOrders ? item.lastSellAt : item.lastTradeAt;
|
||||
return `<tr data-item="${item.id}"><td><a class="itemlink" href="${escape(itemHash(item.id))}">${sprite(item.id)}<span>${escape(item.name)}</span></a></td>
|
||||
<td title="${fmt(price)} gp">${short(price)}</td>
|
||||
<td class="${orders ? '' : 'change ' + tone(item.changePercent)}" title="${orders ? fmt(quantity) : ''}">${orders ? short(quantity) : changeText(item.changePercent)}</td>
|
||||
<td title="${fmt(metric)}">${short(metric)}</td>
|
||||
<td class="recent" title="${time != null ? new Date(time).toLocaleString() : 'No completed trades'}">${relative(time)}</td></tr>`;
|
||||
})
|
||||
.join('')
|
||||
: '<tr><td colspan="5" class="empty">No results</td></tr>';
|
||||
bindImages($('items'));
|
||||
@@ -1347,7 +1355,7 @@
|
||||
history.replaceState(null, '', '#' + params);
|
||||
}
|
||||
sort = location.hash === '#browse' ? 'name' : Object.hasOwn(titles, params.get('sort')) ? params.get('sort') : 'recent';
|
||||
days = [1, 7, 30, 90].includes(Number(params.get('days'))) ? Number(params.get('days')) : 1;
|
||||
days = [1, 7, 30, 90].includes(Number(params.get('days'))) ? Number(params.get('days')) : 7;
|
||||
const requestedOffset = Number(params.get('offset'));
|
||||
offset = Number.isFinite(requestedOffset) ? Math.floor(Math.min(100000, Math.max(0, requestedOffset)) / limit) * limit : 0;
|
||||
$('search').value = (params.get('q') || '').slice(0, 80);
|
||||
|
||||
@@ -13,6 +13,8 @@ export type MarketOverview = {
|
||||
buyQuantity: number;
|
||||
buyValue: number;
|
||||
sellQuantity: number;
|
||||
sellValue: number;
|
||||
lastSellAt: number | null;
|
||||
volume: number;
|
||||
gross: number;
|
||||
tax: number;
|
||||
@@ -210,12 +212,14 @@ export class MarketStore {
|
||||
}
|
||||
quote(item: number, since = Date.now() - 86_400_000) {
|
||||
const book = this.db
|
||||
.query<{ bid: number | null; ask: number | null; buyQuantity: number; buyValue: number; sellQuantity: number }, [number]>(
|
||||
.query<{ bid: number | null; ask: number | null; buyQuantity: number; buyValue: number; sellQuantity: number; sellValue: number; lastSellAt: number | null }, [number]>(
|
||||
`SELECT
|
||||
MAX(CASE WHEN side='buy' THEN price END) bid, MIN(CASE WHEN side='sell' THEN price END) ask,
|
||||
COALESCE(SUM(CASE WHEN side='buy' THEN remaining ELSE 0 END),0) buyQuantity,
|
||||
COALESCE(SUM(CASE WHEN side='buy' THEN remaining * price ELSE 0 END),0) buyValue,
|
||||
COALESCE(SUM(CASE WHEN side='sell' THEN remaining ELSE 0 END),0) sellQuantity
|
||||
COALESCE(SUM(CASE WHEN side='sell' THEN remaining ELSE 0 END),0) sellQuantity,
|
||||
COALESCE(SUM(CASE WHEN side='sell' THEN remaining * price ELSE 0 END),0) sellValue,
|
||||
MAX(CASE WHEN side='sell' AND remaining>0 THEN created END) lastSellAt
|
||||
FROM offers WHERE item=? AND state='open'`
|
||||
)
|
||||
.get(item)!;
|
||||
@@ -248,12 +252,15 @@ export class MarketStore {
|
||||
MAX(CASE WHEN side='buy' THEN price END) bid, MIN(CASE WHEN side='sell' THEN price END) ask,
|
||||
SUM(CASE WHEN side='buy' THEN remaining ELSE 0 END) buyQuantity,
|
||||
SUM(CASE WHEN side='buy' THEN remaining * price ELSE 0 END) buyValue,
|
||||
SUM(CASE WHEN side='sell' THEN remaining ELSE 0 END) sellQuantity
|
||||
SUM(CASE WHEN side='sell' THEN remaining ELSE 0 END) sellQuantity,
|
||||
SUM(CASE WHEN side='sell' THEN remaining * price ELSE 0 END) sellValue,
|
||||
MAX(CASE WHEN side='sell' AND remaining>0 THEN created END) lastSellAt
|
||||
FROM offers WHERE state='open' GROUP BY item),
|
||||
period AS (SELECT item,SUM(quantity) volume,SUM(gross) gross,SUM(tax) tax,COUNT(*) trades
|
||||
FROM trades WHERE time>=? AND time<? GROUP BY item)
|
||||
SELECT a.item,b.bid,b.ask,COALESCE(b.buyQuantity,0) buyQuantity,COALESCE(b.sellQuantity,0) sellQuantity,
|
||||
COALESCE(b.buyValue,0) buyValue,
|
||||
COALESCE(b.sellValue,0) sellValue,b.lastSellAt,
|
||||
COALESCE(p.volume,0) volume,COALESCE(p.gross,0) gross,COALESCE(p.tax,0) tax,COALESCE(p.trades,0) trades,
|
||||
(SELECT price FROM trades WHERE item=a.item AND time<? ORDER BY time DESC,id DESC LIMIT 1) lastPrice,
|
||||
(SELECT time FROM trades WHERE item=a.item AND time<? ORDER BY time DESC,id DESC LIMIT 1) lastTradeAt,
|
||||
|
||||
@@ -13,7 +13,7 @@ function snapshot(days: number) {
|
||||
return result;
|
||||
}
|
||||
function period(url: URL) {
|
||||
const days = integer(url, 'days', 1, 1, 90);
|
||||
const days = integer(url, 'days', 7, 1, 90);
|
||||
if (![1, 7, 30, 90].includes(days)) throw new Error('Invalid days: use 1, 7, 30 or 90.');
|
||||
return days;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export function handleMarket(req: Request, url: URL): Response | null {
|
||||
offset = integer(url, 'offset', 0, 0, 100_000);
|
||||
const days = period(url),
|
||||
sort = url.searchParams.get('sort') ?? (q.trim() ? 'relevance' : 'name');
|
||||
if (!['relevance', 'name', 'name-desc', 'recent', 'rises', 'falls', 'marketCap', 'price', 'volume', 'value', 'buyValue'].includes(sort)) throw new Error('Invalid sort.');
|
||||
if (!['relevance', 'name', 'name-desc', 'recent', 'rises', 'falls', 'marketCap', 'price', 'volume', 'value', 'buyValue', 'sellRecent'].includes(sort)) throw new Error('Invalid sort.');
|
||||
const data = snapshot(days),
|
||||
byId = new Map(data.rows.map(row => [row.item, row]));
|
||||
const empty = {
|
||||
@@ -55,6 +55,8 @@ export function handleMarket(req: Request, url: URL): Response | null {
|
||||
buyQuantity: 0,
|
||||
buyValue: 0,
|
||||
sellQuantity: 0,
|
||||
sellValue: 0,
|
||||
lastSellAt: null,
|
||||
volume: 0,
|
||||
gross: 0,
|
||||
tax: 0,
|
||||
@@ -72,10 +74,11 @@ export function handleMarket(req: Request, url: URL): Response | null {
|
||||
if (sort === 'falls') matches = matches.filter(i => i.changePercent !== null && i.changePercent < 0 && i.volume > 0);
|
||||
if (sort === 'marketCap') matches = matches.filter(i => i.marketCap !== null && i.marketCap > 0);
|
||||
if (sort === 'buyValue') matches = matches.filter(i => i.buyValue > 0);
|
||||
if (sort === 'sellRecent') matches = matches.filter(i => i.sellQuantity > 0 && i.lastSellAt !== null);
|
||||
if (sort === 'price') matches = matches.filter(i => i.lastPrice !== null);
|
||||
if (sort === 'recent') matches = matches.filter(i => i.lastTradeAt !== null);
|
||||
if (sort === 'volume' || sort === 'value') matches = matches.filter(i => i.volume > 0);
|
||||
const sortKeys = { recent: 'lastTradeAt', rises: 'changePercent', falls: 'changePercent', marketCap: 'marketCap', price: 'lastPrice', volume: 'volume', value: 'gross', buyValue: 'buyValue' } as const;
|
||||
const sortKeys = { recent: 'lastTradeAt', rises: 'changePercent', falls: 'changePercent', marketCap: 'marketCap', price: 'lastPrice', volume: 'volume', value: 'gross', buyValue: 'buyValue', sellRecent: 'lastSellAt' } as const;
|
||||
const key = sortKeys[sort as keyof typeof sortKeys];
|
||||
if (key) matches.sort((a, b) => ((b[key] ?? -Infinity) - (a[key] ?? -Infinity)) * (sort === 'falls' ? -1 : 1) || a.name.localeCompare(b.name) || a.id - b.id);
|
||||
const eligible = new Set(catalog().map(i => i.id)),
|
||||
|
||||
@@ -42,6 +42,13 @@ test('HTTP rankings, search, pagination and item details use the same period met
|
||||
buyOffer.run(feather, 50, 100, 50, now);
|
||||
buyOffer.run(logs, 10, 70, 10, now);
|
||||
buyOffer.run(iron, 20, 5, 20, now);
|
||||
const sellOffer = marketStore().db.query("INSERT INTO offers(owner,slot,item,side,quantity,price,remaining,created,state) VALUES('private-seller',-1,?,'sell',3,?,3,?,?)");
|
||||
sellOffer.run(logs, 1000, now - 2000, 'open');
|
||||
sellOffer.run(feather, 200, now - 1000, 'open');
|
||||
sellOffer.run(logs, 1200, now - 5000, 'open');
|
||||
sellOffer.run(iron, 100, now + 1000, 'cancelled');
|
||||
sellOffer.run(logs, 900, now + 2000, 'cancelled');
|
||||
sellOffer.run(logs, 800, now + 3000, 'completed');
|
||||
const ranked = async (sort: string, extra = '') => await request('/api/market/items?days=7&sort=' + sort + extra).json();
|
||||
expect((await ranked('recent')).items[0].id).toBe(iron);
|
||||
const buys = await ranked('buyValue');
|
||||
@@ -54,6 +61,19 @@ test('HTTP rankings, search, pagination and item details use the same period met
|
||||
expect((await request('/api/market/items/' + feather + '?days=7').json()).buyValue).toBe(5000);
|
||||
expect(JSON.stringify(buys)).not.toContain('private-buyer');
|
||||
|
||||
const sells = await ranked('sellRecent');
|
||||
expect(sells.items.map((i: { id: number }) => i.id)).toEqual([coal, feather, logs]);
|
||||
expect(sells.items.map((i: { lastSellAt: number }) => i.lastSellAt)).toEqual([now, now - 1000, now - 2000]);
|
||||
expect(sells.items.map((i: { ask: number }) => i.ask)).toEqual([500, 200, 1000]);
|
||||
expect(sells.items[1].lastTradeAt).toBeNull();
|
||||
expect(sells.items[2].sellQuantity).toBe(6);
|
||||
expect(sells.items[2].sellValue).toBe(6600);
|
||||
expect((await ranked('sellRecent', '&limit=1&offset=1')).items[0].id).toBe(feather);
|
||||
expect((await ranked('sellRecent', '&q=logs')).items.map((i: { id: number }) => i.id)).toEqual([logs]);
|
||||
expect((await ranked('sellRecent', '&q=iron')).total).toBe(0);
|
||||
expect((await request('/api/market/items?sort=sellRecent&days=1').json()).items.map((i: { id: number }) => i.id)).toEqual([coal, feather, logs]);
|
||||
expect(JSON.stringify(sells)).not.toContain('private-seller');
|
||||
|
||||
expect((await ranked('rises')).items[0].id).toBe(coal);
|
||||
const falls = await ranked('falls');
|
||||
expect(falls.items.map((i: { id: number }) => i.id)).toEqual([iron]);
|
||||
|
||||
@@ -296,6 +296,27 @@ describe('Grand Exchange conservation and matching', () => {
|
||||
});
|
||||
|
||||
describe('Market overview metrics', () => {
|
||||
test('sell recency and value track remaining open offers through fills and cancellation', () => {
|
||||
const s = store();
|
||||
const first = s.place(new Wallet('older', 0, 5).account(), ITEM, 'sell', 5, 100, COINS);
|
||||
const second = s.place(new Wallet('newer', 0, 3).account(), ITEM, 'sell', 3, 200, COINS);
|
||||
s.db.query('UPDATE offers SET created=? WHERE id=?').run(1000, first.id);
|
||||
s.db.query('UPDATE offers SET created=? WHERE id=?').run(2000, second.id);
|
||||
const row = () => s.overview(0, Date.now() + 1)[0]!;
|
||||
expect(row().lastSellAt).toBe(2000);
|
||||
expect(row().sellValue).toBe(1100);
|
||||
s.place(new Wallet('buyer', 200).account(), ITEM, 'buy', 2, 100, COINS);
|
||||
expect(row().sellQuantity).toBe(6);
|
||||
expect(row().sellValue).toBe(900);
|
||||
expect(row().lastSellAt).toBe(2000);
|
||||
s.cancel('newer', second.id);
|
||||
expect(row().lastSellAt).toBe(1000);
|
||||
expect(row().sellValue).toBe(300);
|
||||
s.place(new Wallet('buyer2', 300).account(), ITEM, 'buy', 3, 100, COINS);
|
||||
expect(row().sellQuantity).toBe(0);
|
||||
expect(row().sellValue).toBe(0);
|
||||
expect(row().lastSellAt).toBeNull();
|
||||
});
|
||||
test('buy value sums unfilled commitments at each bid price and drops after fills and cancellations', () => {
|
||||
const s = store();
|
||||
const a = new Wallet('a', 1000),
|
||||
|
||||
Reference in new issue
Block a user