suzi-pnl-card-generator
v1.4.0
Published
Generate PnL and performance cards for trading, prediction markets, and custom products
Maintainers
Readme
suzi-pnl-card-generator
Generate polished PnL and performance card images. Outputs PNG or SVG. Built with Satori + resvg.
The package supports old protocol-specific inputs, a generic performance primitive for unsupported products like sports bets, grid bots, options, spot trades, LP positions, portfolio snapshots, and custom venues, plus dedicated pair_trade and basket_trade cards for multi-leg trades.
Install
bun add suzi-pnl-card-generatorQuick Start
import { generateCard } from "suzi-pnl-card-generator";
import { writeFileSync } from "fs";
const png = await generateCard({
type: "performance",
platform: { name: "Phoenix" },
subject: { title: "SOL Grid Bot", subtitle: "61 day strategy" },
result: { label: "Total PnL", value: "+7.46%", profit: true },
stats: [
{ label: "Grid PnL", value: "+$29.01" },
{ label: "Unrealized", value: "+$112.05" },
{ label: "Entry", value: "$88.80" },
{ label: "Current", value: "$94.07" }
]
});
writeFileSync("card.png", png);API
generateCard(data, options?)
Returns Promise<Buffer> for PNG or Promise<string> for SVG.
Card Types
Supported type values:
"performance" // generic slot-based card
"pair_trade" // strict two-leg long/short trade card
"basket_trade" // two-to-six-leg basket trade card
"perp" // perp convenience wrapper
"prediction" // prediction-market convenience wrapper
"phoenix" // Phoenix convenience alias
"hyperliquid" // legacy compatible
"polymarket" // legacy compatibleThe renderer does not calculate PnL, ratios, hedge weights, or exposure. Consumers should pass already-formatted display values.
Render Options
| Field | Type | Default | Description |
|---|---|---:|---|
| format | "png" \| "svg" | "png" | Output format |
| scale | number | 2 | PNG scale factor |
| hd | boolean | true | Full-resolution bundled assets |
Generic Performance Cards
Use type: "performance" when the consumer owns the product semantics and the package should only render a card.
await generateCard({
type: "performance",
platform: {
name: "Phoenix",
icon: "data:image/png;base64,..." // optional
},
subject: {
title: "SOL Grid Bot",
subtitle: "61 day strategy",
icon: "data:image/png;base64,..." // optional
},
result: {
label: "Total PnL",
value: "+7.46%",
profit: true
},
stats: [
{ label: "Grid PnL", value: "+$29.01" },
{ label: "Unrealized", value: "+$112.05" },
{ label: "Entry", value: "$88.80" },
{ label: "Current", value: "$94.07" }
]
});Sports Bet
await generateCard({
type: "performance",
platform: { name: "Polymarket" },
subject: { title: "Lakers vs Warriors", subtitle: "Lakers ML" },
result: { label: "PnL", value: "+32.80%", profit: true },
stats: [
{ label: "Entry", value: "62c" },
{ label: "Exit", value: "82c" },
{ label: "Stake", value: "$250" },
{ label: "Profit", value: "+$82" }
]
});Options Position
await generateCard({
type: "performance",
platform: { name: "Deribit" },
subject: { title: "BTC 110K Call", subtitle: "Expires Jun 28" },
result: { label: "PnL", value: "-18.40%", profit: false },
stats: [
{ label: "Strike", value: "$110K" },
{ label: "Entry", value: "$2,140" },
{ label: "Mark", value: "$1,746" },
{ label: "Size", value: "0.5 BTC" }
]
});Custom Protocol Logo
import { readFileSync } from "fs";
const platformIcon = `data:image/png;base64,${readFileSync("./drift.png").toString("base64")}`;
const tokenIcon = `data:image/png;base64,${readFileSync("./sol.png").toString("base64")}`;
await generateCard({
type: "performance",
platform: { name: "Drift", icon: platformIcon },
subject: { title: "SOL-PERP", icon: tokenIcon },
result: { label: "PnL", value: "+12.40%", profit: true },
stats: [
{ label: "Entry", value: "$140.10" },
{ label: "Exit", value: "$157.48" },
{ label: "Leverage", value: "10x" }
]
});Pair and Basket Trade Cards
Use type: "pair_trade" when the position is exactly two legs and type: "basket_trade" when it has 2-6 legs. Each leg accepts an optional icon; if it is omitted, the renderer draws a deterministic symbol badge so the card still has asset identity without requiring this package to fetch token images.
Pair Trade
await generateCard({
type: "pair_trade",
platform: { name: "Phoenix", icon: "data:image/png;base64,..." },
title: "SOL / ETH",
subtitle: "Mean reversion",
result: { label: "Net PnL", value: "+8.42%", profit: true },
legs: [
{
side: "long",
symbol: "SOL",
icon: "data:image/png;base64,...",
pnl: "+$312",
profit: true,
weight: "55%",
entry: "$140.10",
exit: "$157.48"
},
{
side: "short",
symbol: "ETH",
icon: "data:image/png;base64,...",
pnl: "+$108",
profit: true,
weight: "45%",
entry: "$3,420",
exit: "$3,280"
}
],
stats: [
{ label: "Entry Ratio", value: "0.0472" },
{ label: "Exit Ratio", value: "0.0511" }
]
});Basket Trade
await generateCard({
type: "basket_trade",
platform: { name: "Phoenix", icon: "data:image/png;base64,..." },
title: "SOL / ETH / BTC",
subtitle: "Market neutral basket",
result: { label: "Net PnL", value: "+6.18%", profit: true },
legs: [
{ side: "long", symbol: "SOL", icon: "data:image/png;base64,...", pnl: "+$312", profit: true, weight: "45%" },
{ side: "short", symbol: "ETH", icon: "data:image/png;base64,...", pnl: "+$108", profit: true, weight: "35%" },
{ side: "long", symbol: "BTC", icon: "data:image/png;base64,...", pnl: "-$54", profit: false, weight: "20%" }
],
stats: [
{ label: "Notional", value: "$15,000" },
{ label: "Net Exposure", value: "$1,240" },
{ label: "Hedge", value: "Delta 0.12" }
]
});Rules:
pair_trade.legsmust contain exactly 2 legs.basket_trade.legsmust contain 2-6 legs.- Each leg requires
side,symbol, andpnl. sidemust be"long"or"short".statsis optional and capped at 4 items.entryandexitare shown only in the roomier two-leg pair layout.
Perp Cards
Use type: "perp" for perp-style PnL input. Built-in protocol presets fill platform name and icon for hyperliquid and phoenix. platformName and platformIcon override presets.
await generateCard({
type: "perp",
protocol: "phoenix",
pair: "SOL-PERP",
pnlPercent: 5.93,
entryPrice: "$88.80",
exitPrice: "$94.07",
leverage: 15,
profit: true
});Custom perp venue:
await generateCard({
type: "perp",
platformName: "Drift",
platformIcon: "data:image/png;base64,...",
pair: "SOL-PERP",
pnlPercent: 12.4,
entryPrice: "$140.10",
exitPrice: "$157.48",
leverage: 10,
profit: true
});Phoenix alias:
await generateCard({
type: "phoenix",
pair: "SOL-PERP",
pnlPercent: 5.93,
entryPrice: "$88.80",
exitPrice: "$94.07",
leverage: 15,
profit: true
});Prediction Cards
Use type: "prediction" for generic prediction-market cards, or keep using type: "polymarket" for the built-in Polymarket preset.
await generateCard({
type: "prediction",
platformName: "Kalshi",
title: "Fed decision in January",
outcome: "Yes",
closesAt: "Closes Jan 16",
pnlPercent: 40.32,
entryPrice: "84c",
exitPrice: "94c",
profit: true
});Legacy Compatibility
Existing inputs still work:
await generateCard({
type: "hyperliquid",
pair: "SOL -USDC",
pnlPercent: 40.32,
entryPrice: "$184.2",
exitPrice: "$192.5",
leverage: 20,
profit: true
});
await generateCard({
type: "polymarket",
title: "Fed decision in January",
outcome: "Yes",
closesAt: "Closes Jan 16",
pnlPercent: 40.32,
entryPrice: "84c",
exitPrice: "94c",
profit: true
});Both legacy types also accept platformName, platformIcon, and tokenIcon overrides.
CLI
suzi-pnl-generator -i card.json -o card.png
suzi-pnl-generator -i card.json --format svgThe CLI accepts the same JSON shapes as the TypeScript API.
JSON Schema
Performance Card
{
"type": "object",
"required": ["type", "platform", "subject", "result", "stats"],
"properties": {
"type": { "const": "performance" },
"platform": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "maxLength": 32 },
"icon": { "type": "string" }
}
},
"subject": {
"type": "object",
"required": ["title"],
"properties": {
"title": { "type": "string", "maxLength": 72 },
"subtitle": { "type": "string", "maxLength": 48 },
"icon": { "type": "string" },
"titleMaxLines": { "type": "number", "default": 2 }
}
},
"result": {
"type": "object",
"required": ["label", "value", "profit"],
"properties": {
"label": { "type": "string", "maxLength": 24 },
"value": { "type": "string", "maxLength": 24 },
"profit": { "type": "boolean" }
}
},
"stats": {
"type": "array",
"maxItems": 6,
"items": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": { "type": "string", "maxLength": 24 },
"value": { "type": "string", "maxLength": 32 }
}
}
}
}
}Pair and Basket Trade Card
{
"type": "object",
"required": ["type", "platform", "title", "result", "legs"],
"properties": {
"type": { "enum": ["pair_trade", "basket_trade"] },
"platform": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "maxLength": 32 },
"icon": { "type": "string" }
}
},
"title": { "type": "string", "maxLength": 72 },
"subtitle": { "type": "string", "maxLength": 48 },
"result": {
"type": "object",
"required": ["label", "value", "profit"],
"properties": {
"label": { "type": "string", "maxLength": 24 },
"value": { "type": "string", "maxLength": 24 },
"profit": { "type": "boolean" }
}
},
"legs": {
"type": "array",
"minItems": 2,
"maxItems": 6,
"items": {
"type": "object",
"required": ["side", "symbol", "pnl"],
"properties": {
"side": { "enum": ["long", "short"] },
"symbol": { "type": "string", "maxLength": 12 },
"icon": { "type": "string" },
"pnl": { "type": "string", "maxLength": 24 },
"profit": { "type": "boolean" },
"weight": { "type": "string", "maxLength": 16 },
"entry": { "type": "string", "maxLength": 24 },
"exit": { "type": "string", "maxLength": 24 }
}
}
},
"stats": {
"type": "array",
"maxItems": 4,
"items": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": { "type": "string", "maxLength": 24 },
"value": { "type": "string", "maxLength": 32 }
}
}
}
}
}Perp Card
{
"type": "object",
"required": ["type", "pair", "pnlPercent", "entryPrice", "exitPrice", "leverage", "profit"],
"properties": {
"type": { "enum": ["perp", "phoenix", "hyperliquid"] },
"protocol": { "enum": ["hyperliquid", "phoenix"] },
"platformName": { "type": "string" },
"platformIcon": { "type": "string" },
"tokenIcon": { "type": "string" },
"pair": { "type": "string" },
"pnlPercent": { "type": "number" },
"entryPrice": { "type": "string" },
"exitPrice": { "type": "string" },
"leverage": { "type": "number" },
"profit": { "type": "boolean" },
"resultLabel": { "type": "string" },
"titleMaxLines": { "type": "number" }
}
}Design
- Card: 512x307px, dark gradient background, subtle border
- Result: green for profit, red for loss
- Subject/title: Inter
- Labels: DM Mono
- Includes Suzi branding and happy/sad mascot based on
profit
