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 @@
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;
}