Skip to content

Commit b0f81aa

Browse files
authored
Include traded_market_ids in Portfolio for reliable history loading (#372)
1 parent d20316c commit b0f81aa

File tree

10 files changed

+147
-6
lines changed

10 files changed

+147
-6
lines changed

backend/.sqlx/query-f56b5ce821696068969577978efcb1df79079b8dd5c3dab459a3cf79387787f4.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/src/convert.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ impl From<db::Portfolio> for websocket_api::Portfolio {
1515
available_balance,
1616
market_exposures,
1717
owner_credits,
18+
traded_market_ids,
1819
}: db::Portfolio,
1920
) -> Self {
2021
Self {
@@ -39,6 +40,7 @@ impl From<db::Portfolio> for websocket_api::Portfolio {
3940
credit: credit.credit.0.try_into().unwrap(),
4041
})
4142
.collect(),
43+
traded_market_ids,
4244
}
4345
}
4446
}

backend/src/db.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,29 @@ impl DB {
862862
}
863863
}
864864

865+
/// Get all market IDs an account has ever traded in or redeemed from.
866+
///
867+
/// # Errors
868+
/// Returns an error if the database query fails.
869+
pub async fn get_traded_market_ids(&self, account_id: i64) -> SqlxResult<Vec<i64>> {
870+
let rows = sqlx::query_scalar!(
871+
r#"
872+
SELECT DISTINCT market_id as "market_id!"
873+
FROM (
874+
SELECT market_id FROM trade WHERE buyer_id = ?1
875+
UNION
876+
SELECT market_id FROM trade WHERE seller_id = ?1
877+
UNION
878+
SELECT fund_id AS market_id FROM redemption WHERE redeemer_id = ?1
879+
)
880+
"#,
881+
account_id
882+
)
883+
.fetch_all(&self.pool)
884+
.await?;
885+
Ok(rows)
886+
}
887+
865888
#[must_use]
866889
pub fn get_all_accounts(&self) -> BoxStream<'_, SqlxResult<Account>> {
867890
sqlx::query_as!(
@@ -3633,6 +3656,7 @@ async fn get_portfolio(
36333656
available_balance,
36343657
market_exposures,
36353658
owner_credits: vec![],
3659+
traded_market_ids: vec![],
36363660
}))
36373661
}
36383662

@@ -4061,6 +4085,7 @@ pub struct Portfolio {
40614085
pub available_balance: Decimal,
40624086
pub market_exposures: Vec<MarketExposure>,
40634087
pub owner_credits: Vec<OwnerCredit>,
4088+
pub traded_market_ids: Vec<i64>,
40644089
}
40654090

40664091
#[derive(Debug)]

backend/src/handle_socket.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,11 @@ async fn send_initial_private_data(
278278
let mut transfers = Vec::new();
279279
let mut portfolios = Vec::new();
280280
for &account_id in accounts {
281-
let Some(portfolio) = db.get_portfolio(account_id).await? else {
281+
let Some(mut portfolio) = db.get_portfolio(account_id).await? else {
282282
tracing::warn!("Account {account_id} not found");
283283
continue;
284284
};
285+
portfolio.traded_market_ids = db.get_traded_market_ids(account_id).await?;
285286
portfolios.push(Portfolio::from(portfolio));
286287
transfers.extend(
287288
db.get_transfers(account_id)

frontend/src/lib/api.svelte.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const serverState = $state({
6363
marketGroups: new SvelteMap<number, websocket_api.IMarketGroup>(),
6464
auctions: new SvelteMap<number, websocket_api.IAuction>(),
6565
universes: new SvelteMap<number, websocket_api.IUniverse>(),
66+
tradedMarketIds: new SvelteMap<number, Set<number>>(),
6667
lastKnownTransactionId: 0,
6768
arborPixieAccountId: undefined as number | undefined
6869
});
@@ -258,9 +259,16 @@ socket.onmessage = (event: MessageEvent) => {
258259
if (msg.portfolios) {
259260
if (!msg.portfolios.areNewOwnerships) {
260261
serverState.portfolios.clear();
262+
serverState.tradedMarketIds.clear();
261263
}
262264
for (const p of msg.portfolios.portfolios || []) {
263265
serverState.portfolios.set(p.accountId, p);
266+
if (p.tradedMarketIds?.length) {
267+
serverState.tradedMarketIds.set(
268+
p.accountId as number,
269+
new Set(p.tradedMarketIds.map(Number))
270+
);
271+
}
264272
if (p.accountId == serverState.actingAs) {
265273
serverState.portfolio = p;
266274
}

frontend/src/lib/pnlMetrics.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,10 @@ export function computePnLOverTime(
357357
if (!cashFlowByMarket.has(cid)) cashFlowByMarket.set(cid, 0);
358358
}
359359

360-
// Track redeem fee
361-
totalRedeemFees += event.redeemFee * amount;
360+
// Track redeem fee (attributed to the fund market)
361+
if (!filterMarketIds || filterMarketIds.has(fundId)) {
362+
totalRedeemFees += event.redeemFee * amount;
363+
}
362364

363365
const { totalCash, totalMtM } = computePnLAtPoint();
364366
dataPoints.push({
@@ -466,12 +468,25 @@ export function computePnLOverTime(
466468
export function getMarketsNeedingHistory(
467469
accountId: number,
468470
markets: Map<number, MarketData>,
469-
portfolio: websocket_api.IPortfolio | undefined
471+
portfolio: websocket_api.IPortfolio | undefined,
472+
tradedMarketIds: Set<number> | undefined
470473
): number[] {
471474
const needed: number[] = [];
472475
const seen = new Set<number>();
473476

474-
// Markets with current exposure
477+
// Markets the backend told us this account has traded/redeemed
478+
if (tradedMarketIds) {
479+
for (const marketId of tradedMarketIds) {
480+
const marketData = markets.get(marketId);
481+
if (marketData && !marketData.hasFullTradeHistory && !seen.has(marketId)) {
482+
seen.add(marketId);
483+
needed.push(marketId);
484+
}
485+
}
486+
}
487+
488+
// Markets with current exposure (covers markets not yet in traded_market_ids,
489+
// e.g. from redemption constituents the account never directly traded)
475490
for (const exposure of portfolio?.marketExposures || []) {
476491
const marketId = Number(exposure.marketId ?? 0);
477492
const marketData = markets.get(marketId);

frontend/src/routes/performance/+page.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,13 @@
145145
}
146146
147147
const portfolio = serverState.portfolios.get(accountId);
148-
const needed = getMarketsNeedingHistory(accountId, serverState.markets, portfolio);
148+
const tradedMarketIds = serverState.tradedMarketIds.get(accountId);
149+
const needed = getMarketsNeedingHistory(
150+
accountId,
151+
serverState.markets,
152+
portfolio,
153+
tradedMarketIds
154+
);
149155
150156
let delay = 0;
151157
for (const marketId of needed) {

schema-js/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,9 @@ export namespace websocket_api {
15671567

15681568
/** Portfolio ownerCredits */
15691569
ownerCredits?: (websocket_api.Portfolio.IOwnerCredit[]|null);
1570+
1571+
/** Portfolio tradedMarketIds */
1572+
tradedMarketIds?: ((number|Long)[]|null);
15701573
}
15711574

15721575
/** Represents a Portfolio. */
@@ -1593,6 +1596,9 @@ export namespace websocket_api {
15931596
/** Portfolio ownerCredits. */
15941597
public ownerCredits: websocket_api.Portfolio.IOwnerCredit[];
15951598

1599+
/** Portfolio tradedMarketIds. */
1600+
public tradedMarketIds: (number|Long)[];
1601+
15961602
/**
15971603
* Creates a new Portfolio instance using the specified properties.
15981604
* @param [properties] Properties to set

schema-js/index.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4159,6 +4159,7 @@ $root.websocket_api = (function() {
41594159
* @property {number|null} [availableBalance] Portfolio availableBalance
41604160
* @property {Array.<websocket_api.Portfolio.IMarketExposure>|null} [marketExposures] Portfolio marketExposures
41614161
* @property {Array.<websocket_api.Portfolio.IOwnerCredit>|null} [ownerCredits] Portfolio ownerCredits
4162+
* @property {Array.<number|Long>|null} [tradedMarketIds] Portfolio tradedMarketIds
41624163
*/
41634164

41644165
/**
@@ -4172,6 +4173,7 @@ $root.websocket_api = (function() {
41724173
function Portfolio(properties) {
41734174
this.marketExposures = [];
41744175
this.ownerCredits = [];
4176+
this.tradedMarketIds = [];
41754177
if (properties)
41764178
for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i)
41774179
if (properties[keys[i]] != null)
@@ -4218,6 +4220,14 @@ $root.websocket_api = (function() {
42184220
*/
42194221
Portfolio.prototype.ownerCredits = $util.emptyArray;
42204222

4223+
/**
4224+
* Portfolio tradedMarketIds.
4225+
* @member {Array.<number|Long>} tradedMarketIds
4226+
* @memberof websocket_api.Portfolio
4227+
* @instance
4228+
*/
4229+
Portfolio.prototype.tradedMarketIds = $util.emptyArray;
4230+
42214231
/**
42224232
* Creates a new Portfolio instance using the specified properties.
42234233
* @function create
@@ -4254,6 +4264,12 @@ $root.websocket_api = (function() {
42544264
if (message.ownerCredits != null && message.ownerCredits.length)
42554265
for (var i = 0; i < message.ownerCredits.length; ++i)
42564266
$root.websocket_api.Portfolio.OwnerCredit.encode(message.ownerCredits[i], writer.uint32(/* id 5, wireType 2 =*/42).fork()).ldelim();
4267+
if (message.tradedMarketIds != null && message.tradedMarketIds.length) {
4268+
writer.uint32(/* id 6, wireType 2 =*/50).fork();
4269+
for (var i = 0; i < message.tradedMarketIds.length; ++i)
4270+
writer.int64(message.tradedMarketIds[i]);
4271+
writer.ldelim();
4272+
}
42574273
return writer;
42584274
};
42594275

@@ -4312,6 +4328,17 @@ $root.websocket_api = (function() {
43124328
message.ownerCredits.push($root.websocket_api.Portfolio.OwnerCredit.decode(reader, reader.uint32()));
43134329
break;
43144330
}
4331+
case 6: {
4332+
if (!(message.tradedMarketIds && message.tradedMarketIds.length))
4333+
message.tradedMarketIds = [];
4334+
if ((tag & 7) === 2) {
4335+
var end2 = reader.uint32() + reader.pos;
4336+
while (reader.pos < end2)
4337+
message.tradedMarketIds.push(reader.int64());
4338+
} else
4339+
message.tradedMarketIds.push(reader.int64());
4340+
break;
4341+
}
43154342
default:
43164343
reader.skipType(tag & 7);
43174344
break;
@@ -4374,6 +4401,13 @@ $root.websocket_api = (function() {
43744401
return "ownerCredits." + error;
43754402
}
43764403
}
4404+
if (message.tradedMarketIds != null && message.hasOwnProperty("tradedMarketIds")) {
4405+
if (!Array.isArray(message.tradedMarketIds))
4406+
return "tradedMarketIds: array expected";
4407+
for (var i = 0; i < message.tradedMarketIds.length; ++i)
4408+
if (!$util.isInteger(message.tradedMarketIds[i]) && !(message.tradedMarketIds[i] && $util.isInteger(message.tradedMarketIds[i].low) && $util.isInteger(message.tradedMarketIds[i].high)))
4409+
return "tradedMarketIds: integer|Long[] expected";
4410+
}
43774411
return null;
43784412
};
43794413

@@ -4422,6 +4456,20 @@ $root.websocket_api = (function() {
44224456
message.ownerCredits[i] = $root.websocket_api.Portfolio.OwnerCredit.fromObject(object.ownerCredits[i]);
44234457
}
44244458
}
4459+
if (object.tradedMarketIds) {
4460+
if (!Array.isArray(object.tradedMarketIds))
4461+
throw TypeError(".websocket_api.Portfolio.tradedMarketIds: array expected");
4462+
message.tradedMarketIds = [];
4463+
for (var i = 0; i < object.tradedMarketIds.length; ++i)
4464+
if ($util.Long)
4465+
(message.tradedMarketIds[i] = $util.Long.fromValue(object.tradedMarketIds[i])).unsigned = false;
4466+
else if (typeof object.tradedMarketIds[i] === "string")
4467+
message.tradedMarketIds[i] = parseInt(object.tradedMarketIds[i], 10);
4468+
else if (typeof object.tradedMarketIds[i] === "number")
4469+
message.tradedMarketIds[i] = object.tradedMarketIds[i];
4470+
else if (typeof object.tradedMarketIds[i] === "object")
4471+
message.tradedMarketIds[i] = new $util.LongBits(object.tradedMarketIds[i].low >>> 0, object.tradedMarketIds[i].high >>> 0).toNumber();
4472+
}
44254473
return message;
44264474
};
44274475

@@ -4441,6 +4489,7 @@ $root.websocket_api = (function() {
44414489
if (options.arrays || options.defaults) {
44424490
object.marketExposures = [];
44434491
object.ownerCredits = [];
4492+
object.tradedMarketIds = [];
44444493
}
44454494
if (options.defaults) {
44464495
if ($util.Long) {
@@ -4470,6 +4519,14 @@ $root.websocket_api = (function() {
44704519
for (var j = 0; j < message.ownerCredits.length; ++j)
44714520
object.ownerCredits[j] = $root.websocket_api.Portfolio.OwnerCredit.toObject(message.ownerCredits[j], options);
44724521
}
4522+
if (message.tradedMarketIds && message.tradedMarketIds.length) {
4523+
object.tradedMarketIds = [];
4524+
for (var j = 0; j < message.tradedMarketIds.length; ++j)
4525+
if (typeof message.tradedMarketIds[j] === "number")
4526+
object.tradedMarketIds[j] = options.longs === String ? String(message.tradedMarketIds[j]) : message.tradedMarketIds[j];
4527+
else
4528+
object.tradedMarketIds[j] = options.longs === String ? $util.Long.prototype.toString.call(message.tradedMarketIds[j]) : options.longs === Number ? new $util.LongBits(message.tradedMarketIds[j].low >>> 0, message.tradedMarketIds[j].high >>> 0).toNumber() : message.tradedMarketIds[j];
4529+
}
44734530
return object;
44744531
};
44754532

schema/portfolio.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ message Portfolio {
77
double available_balance = 3;
88
repeated MarketExposure market_exposures = 4;
99
repeated OwnerCredit owner_credits = 5;
10+
repeated int64 traded_market_ids = 6;
1011

1112
message MarketExposure {
1213
int64 market_id = 1;

0 commit comments

Comments
 (0)