diff --git a/backend/.sqlx/query-15938c411b430d9f196b271888bc5ff3543da2769bf6a760a360cff0800f7279.json b/backend/.sqlx/query-15938c411b430d9f196b271888bc5ff3543da2769bf6a760a360cff0800f7279.json new file mode 100644 index 00000000..8bfe3ba8 --- /dev/null +++ b/backend/.sqlx/query-15938c411b430d9f196b271888bc5ff3543da2769bf6a760a360cff0800f7279.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n WITH trade_costs AS (\n SELECT\n market.group_id,\n trade.market_id,\n SUM(CASE WHEN trade.buyer_id = ? THEN CAST(trade.price AS REAL) * CAST(trade.size AS REAL) ELSE 0 END) as buy_cost,\n SUM(CASE WHEN trade.seller_id = ? THEN CAST(trade.price AS REAL) * CAST(trade.size AS REAL) ELSE 0 END) as sell_cost,\n SUM(CASE WHEN trade.buyer_id = ? THEN CAST(trade.size AS REAL) ELSE 0 END) -\n SUM(CASE WHEN trade.seller_id = ? THEN CAST(trade.size AS REAL) ELSE 0 END) as position\n FROM trade\n JOIN market ON trade.market_id = market.id\n WHERE market.group_id IS NOT NULL\n AND market.group_id > 0\n AND (trade.buyer_id = ? OR trade.seller_id = ?)\n GROUP BY market.group_id, trade.market_id\n ),\n market_prices AS (\n SELECT\n market.id as market_id,\n market.group_id,\n COALESCE(\n market.settled_price,\n (SELECT CAST(price AS REAL) FROM trade WHERE trade.market_id = market.id ORDER BY trade.id DESC LIMIT 1)\n ) as current_price\n FROM market\n WHERE market.group_id IS NOT NULL AND market.group_id > 0\n )\n SELECT\n tc.group_id as \"group_id!\",\n CAST(SUM((tc.position * COALESCE(mp.current_price, 0)) - (tc.buy_cost - tc.sell_cost)) AS TEXT) as \"pnl!: Text\"\n FROM trade_costs tc\n LEFT JOIN market_prices mp ON tc.market_id = mp.market_id\n GROUP BY tc.group_id\n ", + "describe": { + "columns": [ + { + "name": "group_id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "pnl!: Text", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 6 + }, + "nullable": [ + true, + false + ] + }, + "hash": "15938c411b430d9f196b271888bc5ff3543da2769bf6a760a360cff0800f7279" +} diff --git a/backend/src/convert.rs b/backend/src/convert.rs index 3cd8e547..f4333067 100644 --- a/backend/src/convert.rs +++ b/backend/src/convert.rs @@ -139,6 +139,7 @@ impl From for websocket_api::MarketGroup { name, description, type_id, + pnl: None, } } } diff --git a/backend/src/db.rs b/backend/src/db.rs index 90e393d3..a7105923 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -1049,6 +1049,59 @@ impl DB { .await } + /// Get PnL for each group for a specific account. + /// Returns a map of group_id -> pnl. + #[instrument(err, skip(self))] + pub async fn get_group_pnls(&self, account_id: i64) -> SqlxResult> { + struct GroupPnLRow { + group_id: i64, + pnl: Text, + } + + let rows = sqlx::query_as!( + GroupPnLRow, + r#" + WITH trade_costs AS ( + SELECT + market.group_id, + trade.market_id, + SUM(CASE WHEN trade.buyer_id = ? THEN CAST(trade.price AS REAL) * CAST(trade.size AS REAL) ELSE 0 END) as buy_cost, + SUM(CASE WHEN trade.seller_id = ? THEN CAST(trade.price AS REAL) * CAST(trade.size AS REAL) ELSE 0 END) as sell_cost, + SUM(CASE WHEN trade.buyer_id = ? THEN CAST(trade.size AS REAL) ELSE 0 END) - + SUM(CASE WHEN trade.seller_id = ? THEN CAST(trade.size AS REAL) ELSE 0 END) as position + FROM trade + JOIN market ON trade.market_id = market.id + WHERE market.group_id IS NOT NULL + AND market.group_id > 0 + AND (trade.buyer_id = ? OR trade.seller_id = ?) + GROUP BY market.group_id, trade.market_id + ), + market_prices AS ( + SELECT + market.id as market_id, + market.group_id, + COALESCE( + market.settled_price, + (SELECT CAST(price AS REAL) FROM trade WHERE trade.market_id = market.id ORDER BY trade.id DESC LIMIT 1) + ) as current_price + FROM market + WHERE market.group_id IS NOT NULL AND market.group_id > 0 + ) + SELECT + tc.group_id as "group_id!", + CAST(SUM((tc.position * COALESCE(mp.current_price, 0)) - (tc.buy_cost - tc.sell_cost)) AS TEXT) as "pnl!: Text" + FROM trade_costs tc + LEFT JOIN market_prices mp ON tc.market_id = mp.market_id + GROUP BY tc.group_id + "#, + account_id, account_id, account_id, account_id, account_id, account_id + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows.into_iter().map(|r| (r.group_id, r.pnl.0)).collect()) + } + #[instrument(err, skip(self))] pub async fn create_market_group( &self, diff --git a/backend/src/handle_socket.rs b/backend/src/handle_socket.rs index 172be5b6..59e4b485 100644 --- a/backend/src/handle_socket.rs +++ b/backend/src/handle_socket.rs @@ -325,12 +325,25 @@ async fn send_initial_public_data( ); socket.send(market_types_msg).await?; - // Send groups + // Send groups with PnL for the acting account let market_groups = db.get_all_market_groups().await?; + let group_pnls = if let Some(&account_id) = owned_accounts.first() { + db.get_group_pnls(account_id).await.unwrap_or_default() + } else { + std::collections::HashMap::new() + }; let market_groups_msg = encode_server_message( String::new(), SM::MarketGroups(MarketGroups { - market_groups: market_groups.into_iter().map(MarketGroup::from).collect(), + market_groups: market_groups + .into_iter() + .map(|g| { + let pnl = group_pnls.get(&g.id).map(|d| (*d).try_into().unwrap()); + let mut mg = MarketGroup::from(g); + mg.pnl = pnl; + mg + }) + .collect(), }), ); socket.send(market_groups_msg).await?; diff --git a/frontend/src/lib/components/forms/redeem.svelte b/frontend/src/lib/components/forms/redeem.svelte index 0c9f2a6a..123e92ef 100644 --- a/frontend/src/lib/components/forms/redeem.svelte +++ b/frontend/src/lib/components/forms/redeem.svelte @@ -42,7 +42,7 @@
- Exchange for {constituentList} + Exchangeable for {constituentList}
diff --git a/frontend/src/lib/components/marketHead.svelte b/frontend/src/lib/components/marketHead.svelte index e60fcb0d..da678f3b 100644 --- a/frontend/src/lib/components/marketHead.svelte +++ b/frontend/src/lib/components/marketHead.svelte @@ -200,7 +200,7 @@ Settle Price: {marketDefinition.closed.settlePrice}

{/if} - {#if isRedeemable} + {#if isRedeemable && !marketDefinition.closed}
diff --git a/frontend/src/lib/portfolioMetrics.ts b/frontend/src/lib/portfolioMetrics.ts index 5ec6f57b..d18e381c 100644 --- a/frontend/src/lib/portfolioMetrics.ts +++ b/frontend/src/lib/portfolioMetrics.ts @@ -150,3 +150,34 @@ export const computePortfolioMetrics = ( } }; }; + +/** + * Calculate subaccount PnL: Current MTM - Net Transfers In + */ +export const calculateSubaccountPnL = ( + accountId: number, + transfers: websocket_api.ITransfer[], + markToMarket: number +): number => { + let netTransfersIn = 0; + for (const transfer of transfers) { + if (transfer.toAccountId === accountId) { + netTransfersIn += transfer.amount ?? 0; + } + if (transfer.fromAccountId === accountId) { + netTransfersIn -= transfer.amount ?? 0; + } + } + return markToMarket - netTransfersIn; +}; + +/** + * Get group PnL from market groups (calculated by backend) + */ +export const getGroupPnL = ( + groupId: number, + marketGroups: Map +): number => { + const group = marketGroups.get(groupId); + return group?.pnl ?? 0; +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 108bfc4d..fdf47d36 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,13 +1,13 @@
@@ -460,26 +462,32 @@ {#each organized as item (item.type === 'group' ? `group-${item.groupId}` : item.key)} {#if item.type === 'group'} + {@const groupPnL = getGroupPnL(item.groupId, serverState.marketGroups)} {@const clock = clocksByName.get(item.groupName)}

{item.groupName}

- {#if clock} -
- - {formatClockTime(clock)} - {#if !clock.is_running} - (paused) - {/if} -
- {/if} +
+ {#if clock} +
+ + {formatClockTime(clock)} + {#if !clock.is_running} + (paused) + {/if} +
+ {/if} + + Round PnL: 📎 {Math.round(groupPnL).toLocaleString()} + +
{#each item.markets as { id, market, starred, pinned } (id)} diff --git a/schema-js/index.d.ts b/schema-js/index.d.ts index 2e8de787..64e72a61 100644 --- a/schema-js/index.d.ts +++ b/schema-js/index.d.ts @@ -2548,6 +2548,9 @@ export namespace websocket_api { /** MarketGroup typeId */ typeId?: (number|Long|null); + + /** MarketGroup pnl */ + pnl?: (number|null); } /** Represents a MarketGroup. */ @@ -2571,6 +2574,12 @@ export namespace websocket_api { /** MarketGroup typeId. */ public typeId: (number|Long); + /** MarketGroup pnl. */ + public pnl?: (number|null); + + /** MarketGroup _pnl. */ + public _pnl?: "pnl"; + /** * Creates a new MarketGroup instance using the specified properties. * @param [properties] Properties to set diff --git a/schema-js/index.js b/schema-js/index.js index 2f05d27a..05334a3a 100644 --- a/schema-js/index.js +++ b/schema-js/index.js @@ -6888,6 +6888,7 @@ $root.websocket_api = (function() { * @property {string|null} [name] MarketGroup name * @property {string|null} [description] MarketGroup description * @property {number|Long|null} [typeId] MarketGroup typeId + * @property {number|null} [pnl] MarketGroup pnl */ /** @@ -6937,6 +6938,28 @@ $root.websocket_api = (function() { */ MarketGroup.prototype.typeId = $util.Long ? $util.Long.fromBits(0,0,false) : 0; + /** + * MarketGroup pnl. + * @member {number|null|undefined} pnl + * @memberof websocket_api.MarketGroup + * @instance + */ + MarketGroup.prototype.pnl = null; + + // OneOf field names bound to virtual getters and setters + var $oneOfFields; + + /** + * MarketGroup _pnl. + * @member {"pnl"|undefined} _pnl + * @memberof websocket_api.MarketGroup + * @instance + */ + Object.defineProperty(MarketGroup.prototype, "_pnl", { + get: $util.oneOfGetter($oneOfFields = ["pnl"]), + set: $util.oneOfSetter($oneOfFields) + }); + /** * Creates a new MarketGroup instance using the specified properties. * @function create @@ -6969,6 +6992,8 @@ $root.websocket_api = (function() { writer.uint32(/* id 3, wireType 2 =*/26).string(message.description); if (message.typeId != null && Object.hasOwnProperty.call(message, "typeId")) writer.uint32(/* id 4, wireType 0 =*/32).int64(message.typeId); + if (message.pnl != null && Object.hasOwnProperty.call(message, "pnl")) + writer.uint32(/* id 5, wireType 1 =*/41).double(message.pnl); return writer; }; @@ -7019,6 +7044,10 @@ $root.websocket_api = (function() { message.typeId = reader.int64(); break; } + case 5: { + message.pnl = reader.double(); + break; + } default: reader.skipType(tag & 7); break; @@ -7054,6 +7083,7 @@ $root.websocket_api = (function() { MarketGroup.verify = function verify(message) { if (typeof message !== "object" || message === null) return "object expected"; + var properties = {}; if (message.id != null && message.hasOwnProperty("id")) if (!$util.isInteger(message.id) && !(message.id && $util.isInteger(message.id.low) && $util.isInteger(message.id.high))) return "id: integer|Long expected"; @@ -7066,6 +7096,11 @@ $root.websocket_api = (function() { if (message.typeId != null && message.hasOwnProperty("typeId")) if (!$util.isInteger(message.typeId) && !(message.typeId && $util.isInteger(message.typeId.low) && $util.isInteger(message.typeId.high))) return "typeId: integer|Long expected"; + if (message.pnl != null && message.hasOwnProperty("pnl")) { + properties._pnl = 1; + if (typeof message.pnl !== "number") + return "pnl: number expected"; + } return null; }; @@ -7103,6 +7138,8 @@ $root.websocket_api = (function() { message.typeId = object.typeId; else if (typeof object.typeId === "object") message.typeId = new $util.LongBits(object.typeId.low >>> 0, object.typeId.high >>> 0).toNumber(); + if (object.pnl != null) + message.pnl = Number(object.pnl); return message; }; @@ -7147,6 +7184,11 @@ $root.websocket_api = (function() { object.typeId = options.longs === String ? String(message.typeId) : message.typeId; else object.typeId = options.longs === String ? $util.Long.prototype.toString.call(message.typeId) : options.longs === Number ? new $util.LongBits(message.typeId.low >>> 0, message.typeId.high >>> 0).toNumber() : message.typeId; + if (message.pnl != null && message.hasOwnProperty("pnl")) { + object.pnl = options.json && !isFinite(message.pnl) ? String(message.pnl) : message.pnl; + if (options.oneofs) + object._pnl = "pnl"; + } return object; }; diff --git a/schema/market-group.proto b/schema/market-group.proto index c2f373d9..53c8b9bc 100644 --- a/schema/market-group.proto +++ b/schema/market-group.proto @@ -6,4 +6,5 @@ message MarketGroup { string name = 2; string description = 3; int64 type_id = 4; + optional double pnl = 5; }