@palmyr/cli 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,36 +96,41 @@ export interface SolanaSell {
96
96
  time: string;
97
97
  tokensIn: string;
98
98
  tokensInRaw: string;
99
- /** Display string. Holds "0.300000 SOL" for native sells, "5.00 USDC" for USDC sells. */
100
- solOut: string;
101
- /** Raw output. Lamports for SOL exits, 6-dec raw for USDC exits. Both fit JS Number for realistic sizes. */
102
- solOutRaw: number;
103
99
  percentRequested: number;
104
- /** Realized PnL in the position's `inputAsset` (SOL or USDC). Field name kept for back-compat. */
105
- realizedSol: number;
106
100
  reason: string;
107
101
  feeLamports?: number;
108
102
  tipLamports?: number;
109
103
  slippageBpsUsed?: number;
110
104
  protectedExec?: boolean;
111
105
  forensics?: FillForensics;
112
- /** USDC-aware: the asset this sell exited to. Missing on legacy sells; treated as 'SOL'. */
113
- outputAsset?: SolanaInputAsset;
114
- /** Realized output (asset-tagged). `solOut` / `solOutRaw` are back-compat aliases. */
115
- output?: AssetAmount;
116
- /** Realized PnL for this sell (asset-tagged). `realizedSol` is a back-compat alias. */
117
- realized?: AssetPnl;
106
+ /** Asset this sell exited to (mirrors entry.inputAsset). */
107
+ outputAsset: SolanaInputAsset;
108
+ /** Realized output (asset-tagged). Holds USDC values on USDC exits — name no longer chain-typed. */
109
+ output: AssetAmount;
110
+ /** Realized PnL for this sell (asset-tagged). */
111
+ realized: AssetPnl;
112
+ /**
113
+ * @deprecated Read `output.display` / `output.raw` / `realized.amount` instead.
114
+ * Optional only so legacy position files on disk (pre-canonical) still parse;
115
+ * new sells no longer set these.
116
+ */
117
+ solOut?: string;
118
+ /** @deprecated Use `output.raw`. */
119
+ solOutRaw?: number;
120
+ /** @deprecated Use `realized.amount`. */
121
+ realizedSol?: number;
118
122
  }
119
123
  export interface SolanaPnl {
120
- /** Realized PnL in the position's input asset (SOL or USDC). Field name kept for back-compat with legacy SOL positions. */
121
- realizedSol: number;
122
- unrealizedSol: number;
123
124
  unrealizedPct: number;
124
125
  lastPricedAt: string | null;
125
126
  /** Realized PnL across all sells (asset-tagged). */
126
- realized?: AssetPnl;
127
+ realized: AssetPnl;
127
128
  /** Mark-to-market unrealized PnL (asset-tagged). */
128
- unrealized?: AssetPnl;
129
+ unrealized: AssetPnl;
130
+ /** @deprecated Use `realized.amount`. */
131
+ realizedSol?: number;
132
+ /** @deprecated Use `unrealized.amount`. */
133
+ unrealizedSol?: number;
129
134
  }
130
135
  export interface SolanaPositionFile {
131
136
  chain: 'solana';
@@ -167,35 +172,40 @@ export interface BaseSell {
167
172
  time: string;
168
173
  tokensIn: string;
169
174
  tokensInRaw: string;
170
- /** Display string: "0.005000 ETH" or "5.00 USDC". Field name kept for back-compat. */
171
- ethOut: string;
172
- /** Raw output. Wei for ETH exits, 6-dec raw for USDC exits. Both kept as string for u256 safety. */
173
- ethOutRawWei: string;
174
175
  percentRequested: number;
175
- /** Realized PnL in the position's input asset (ETH or USDC). Field name kept for back-compat. */
176
- realizedEth: number;
177
176
  reason: string;
178
177
  feeWei?: string;
179
178
  slippageBpsUsed?: number;
180
179
  protectedExec?: boolean;
181
180
  forensics?: FillForensics;
182
- /** USDC-aware: asset this sell exited to. Missing on legacy sells; treated as 'ETH'. */
183
- outputAsset?: BaseInputAsset;
184
- /** Realized output (asset-tagged). `ethOut` / `ethOutRawWei` are back-compat aliases. */
185
- output?: AssetAmount;
186
- /** Realized PnL for this sell (asset-tagged). `realizedEth` is a back-compat alias. */
187
- realized?: AssetPnl;
181
+ /** Asset this sell exited to (mirrors entry.inputAsset). */
182
+ outputAsset: BaseInputAsset;
183
+ /** Realized output (asset-tagged). Holds USDC values on USDC exits — name no longer chain-typed. */
184
+ output: AssetAmount;
185
+ /** Realized PnL for this sell (asset-tagged). */
186
+ realized: AssetPnl;
187
+ /**
188
+ * @deprecated Read `output.display` / `output.raw` / `realized.amount` instead.
189
+ * Optional only so legacy position files on disk (pre-canonical) still parse;
190
+ * new sells no longer set these.
191
+ */
192
+ ethOut?: string;
193
+ /** @deprecated Use `output.raw`. */
194
+ ethOutRawWei?: string;
195
+ /** @deprecated Use `realized.amount`. */
196
+ realizedEth?: number;
188
197
  }
189
198
  export interface BasePnl {
190
- /** Realized PnL in the position's input asset (ETH or USDC). Field name kept for back-compat. */
191
- realizedEth: number;
192
- unrealizedEth: number;
193
199
  unrealizedPct: number;
194
200
  lastPricedAt: string | null;
195
201
  /** Realized PnL across all sells (asset-tagged). */
196
- realized?: AssetPnl;
202
+ realized: AssetPnl;
197
203
  /** Mark-to-market unrealized PnL (asset-tagged). */
198
- unrealized?: AssetPnl;
204
+ unrealized: AssetPnl;
205
+ /** @deprecated Use `realized.amount`. */
206
+ realizedEth?: number;
207
+ /** @deprecated Use `unrealized.amount`. */
208
+ unrealizedEth?: number;
199
209
  }
200
210
  export interface BasePositionFile {
201
211
  chain: 'base';
@@ -265,6 +275,7 @@ export type TradeLogLine = {
265
275
  currentPct: number;
266
276
  thresholdPct?: number;
267
277
  peakPct?: number;
278
+ drawdownPct?: number;
268
279
  thresholdDurationMs?: number;
269
280
  elapsedMs?: number;
270
281
  llmVerdict?: 'yes' | 'no' | 'unclear';
@@ -478,6 +489,8 @@ export interface BuyBaseResult {
478
489
  protectedExec: boolean;
479
490
  rpcUrl: string;
480
491
  inputAsset: BaseInputAsset;
492
+ /** One-line human-readable summary; safe to print directly. */
493
+ summary: string;
481
494
  }
482
495
  export declare function buyBase(opts: BuyBaseOpts): Promise<BuyBaseResult>;
483
496
  export interface SellBaseOpts {
@@ -497,9 +510,6 @@ export interface SellBaseResult {
497
510
  txHash: string;
498
511
  tokensIn: string;
499
512
  tokensInRaw: string;
500
- ethOut: string;
501
- ethOutRawWei: string;
502
- realizedEth: number;
503
513
  positionStatus: 'open' | 'closed';
504
514
  wallet: string;
505
515
  mint: string;
@@ -519,10 +529,12 @@ export interface SellBaseResult {
519
529
  * on partial sells of a drifted position.
520
530
  */
521
531
  reconcileDriftRaw?: string;
522
- /** Canonical, asset-tagged output. Preferred over `ethOut`/`ethOutRawWei`. */
532
+ /** Canonical, asset-tagged output. */
523
533
  output: AssetAmount;
524
- /** Canonical, asset-tagged realized PnL for this sell. Preferred over `realizedEth`. */
534
+ /** Canonical, asset-tagged realized PnL for this sell. */
525
535
  realized: AssetPnl;
536
+ /** One-line human-readable summary; safe to print directly. */
537
+ summary: string;
526
538
  }
527
539
  export declare function sellBase(opts: SellBaseOpts): Promise<SellBaseResult>;
528
540
  export interface SyncBaseOpts {
@@ -587,6 +599,8 @@ export interface BuyResult {
587
599
  protectedExec: boolean;
588
600
  forensics?: FillForensics;
589
601
  inputAsset: SolanaInputAsset;
602
+ /** One-line human-readable summary; safe to print directly. */
603
+ summary: string;
590
604
  }
591
605
  export declare function buy(opts: BuyOpts): Promise<BuyResult>;
592
606
  export interface SellOpts {
@@ -607,9 +621,6 @@ export interface SellResult {
607
621
  txSignature: string;
608
622
  tokensIn: string;
609
623
  tokensInRaw: string;
610
- solOut: string;
611
- solOutRaw: number;
612
- realizedSol: number;
613
624
  positionStatus: 'open' | 'closed';
614
625
  wallet: string;
615
626
  mint: string;
@@ -622,10 +633,12 @@ export interface SellResult {
622
633
  slippageSource: 'user' | 'dexscreener' | 'fallback';
623
634
  protectedExec: boolean;
624
635
  forensics?: FillForensics;
625
- /** Canonical, asset-tagged output. Preferred over `solOut`/`solOutRaw`. */
636
+ /** Canonical, asset-tagged output. */
626
637
  output: AssetAmount;
627
- /** Canonical, asset-tagged realized PnL for this sell. Preferred over `realizedSol`. */
638
+ /** Canonical, asset-tagged realized PnL for this sell. */
628
639
  realized: AssetPnl;
640
+ /** One-line human-readable summary; safe to print directly. */
641
+ summary: string;
629
642
  }
630
643
  export declare function sell(opts: SellOpts): Promise<SellResult>;
631
644
  export interface SyncOpts {
@@ -194,28 +194,37 @@ export function normalizePosition(p) {
194
194
  }
195
195
  for (const s of p.sells) {
196
196
  if (!s.outputAsset)
197
- s.outputAsset = p.entry.inputAsset;
198
- // Back-fill canonical fields for sells written by pre-canonical CLIs.
199
- // Raw output: lamports for SOL, 6-dec raw for USDC.
200
- if (!s.output) {
201
- const outAsset = s.outputAsset ?? p.entry.inputAsset;
197
+ s.outputAsset = p.entry.inputAsset ?? 'SOL';
198
+ // Back-fill canonical fields from legacy `solOut*` / `realizedSol` if the
199
+ // file pre-dates the canonical schema, then strip the legacy fields so
200
+ // they don't get re-emitted on the next write.
201
+ if (!s.output && s.solOutRaw !== undefined && s.solOut !== undefined) {
202
202
  s.output = {
203
- asset: outAsset,
203
+ asset: s.outputAsset,
204
204
  raw: String(s.solOutRaw),
205
205
  display: s.solOut,
206
206
  };
207
207
  }
208
- if (!s.realized) {
209
- s.realized = { asset: s.outputAsset ?? p.entry.inputAsset, amount: s.realizedSol };
208
+ if (!s.realized && s.realizedSol !== undefined) {
209
+ s.realized = { asset: s.outputAsset, amount: s.realizedSol };
210
210
  }
211
+ delete s.solOut;
212
+ delete s.solOutRaw;
213
+ delete s.realizedSol;
211
214
  }
212
- // Back-fill pnl.realized/unrealized for legacy files.
213
- if (!p.pnl.realized) {
215
+ if (!p.pnl.realized && p.pnl.realizedSol !== undefined) {
214
216
  p.pnl.realized = { asset: p.entry.inputAsset, amount: p.pnl.realizedSol };
215
217
  }
216
- if (!p.pnl.unrealized) {
218
+ if (!p.pnl.unrealized && p.pnl.unrealizedSol !== undefined) {
217
219
  p.pnl.unrealized = { asset: p.entry.inputAsset, amount: p.pnl.unrealizedSol };
218
220
  }
221
+ // Guarantee canonical pnl shape exists for downstream readers.
222
+ if (!p.pnl.realized)
223
+ p.pnl.realized = { asset: p.entry.inputAsset, amount: 0 };
224
+ if (!p.pnl.unrealized)
225
+ p.pnl.unrealized = { asset: p.entry.inputAsset, amount: 0 };
226
+ delete p.pnl.realizedSol;
227
+ delete p.pnl.unrealizedSol;
219
228
  }
220
229
  else {
221
230
  if (!p.entry.inputAsset)
@@ -227,24 +236,33 @@ export function normalizePosition(p) {
227
236
  }
228
237
  for (const s of p.sells) {
229
238
  if (!s.outputAsset)
230
- s.outputAsset = p.entry.inputAsset;
231
- if (!s.output) {
239
+ s.outputAsset = p.entry.inputAsset ?? 'ETH';
240
+ if (!s.output && s.ethOutRawWei !== undefined && s.ethOut !== undefined) {
232
241
  s.output = {
233
- asset: s.outputAsset ?? p.entry.inputAsset,
242
+ asset: s.outputAsset,
234
243
  raw: s.ethOutRawWei,
235
244
  display: s.ethOut,
236
245
  };
237
246
  }
238
- if (!s.realized) {
239
- s.realized = { asset: s.outputAsset ?? p.entry.inputAsset, amount: s.realizedEth };
247
+ if (!s.realized && s.realizedEth !== undefined) {
248
+ s.realized = { asset: s.outputAsset, amount: s.realizedEth };
240
249
  }
250
+ delete s.ethOut;
251
+ delete s.ethOutRawWei;
252
+ delete s.realizedEth;
241
253
  }
242
- if (!p.pnl.realized) {
254
+ if (!p.pnl.realized && p.pnl.realizedEth !== undefined) {
243
255
  p.pnl.realized = { asset: p.entry.inputAsset, amount: p.pnl.realizedEth };
244
256
  }
245
- if (!p.pnl.unrealized) {
257
+ if (!p.pnl.unrealized && p.pnl.unrealizedEth !== undefined) {
246
258
  p.pnl.unrealized = { asset: p.entry.inputAsset, amount: p.pnl.unrealizedEth };
247
259
  }
260
+ if (!p.pnl.realized)
261
+ p.pnl.realized = { asset: p.entry.inputAsset, amount: 0 };
262
+ if (!p.pnl.unrealized)
263
+ p.pnl.unrealized = { asset: p.entry.inputAsset, amount: 0 };
264
+ delete p.pnl.realizedEth;
265
+ delete p.pnl.unrealizedEth;
248
266
  }
249
267
  return p;
250
268
  }
@@ -857,8 +875,6 @@ export async function buyBase(opts) {
857
875
  riskFlags: opts.riskFlags ?? [],
858
876
  sells: [],
859
877
  pnl: {
860
- realizedEth: 0,
861
- unrealizedEth: 0,
862
878
  unrealizedPct: 0,
863
879
  lastPricedAt: null,
864
880
  realized: { asset: inputAsset, amount: 0 },
@@ -868,6 +884,7 @@ export async function buyBase(opts) {
868
884
  // Dry-run must be strictly read-only: simulated trades never touch live state.
869
885
  if (!opts.dryRun)
870
886
  writePosition(position);
887
+ const summary = `${opts.dryRun ? '[dry-run] ' : ''}Bought ${tokensOut} ${opts.ca} for ${position.entry.amountIn} on Base.`;
871
888
  return {
872
889
  positionPath: opts.dryRun
873
890
  ? `simulated:${opts.ca}`
@@ -887,6 +904,7 @@ export async function buyBase(opts) {
887
904
  protectedExec: !!opts.protectedExec,
888
905
  rpcUrl,
889
906
  inputAsset,
907
+ summary,
890
908
  };
891
909
  }
892
910
  export async function sellBase(opts) {
@@ -994,7 +1012,7 @@ export async function sellBase(opts) {
994
1012
  // Entry cost = entry amount + gas fee (ETH-only — fee was paid in ETH at swap time;
995
1013
  // for USDC entries we treat gas as small and ignore it in the USDC accounting).
996
1014
  const entryRawStr = position.entry.amountInRaw ?? position.entry.amountInRawWei;
997
- let realizedEth;
1015
+ let realizedAmount;
998
1016
  const inputAsset = position.entry.inputAsset ?? 'ETH';
999
1017
  if (inputAsset === 'ETH' && outputAsset === 'ETH') {
1000
1018
  const entryAmountWei = BigInt(entryRawStr);
@@ -1004,18 +1022,18 @@ export async function sellBase(opts) {
1004
1022
  const sellFeeWei = BigInt(swap.feeWei);
1005
1023
  const proceedsNetWei = BigInt(outRaw) - sellFeeWei;
1006
1024
  const realizedWei = proceedsNetWei - costWei;
1007
- realizedEth = Number(realizedWei) / 1e18;
1025
+ realizedAmount = Number(realizedWei) / 1e18;
1008
1026
  }
1009
1027
  else if (inputAsset === 'USDC' && outputAsset === 'USDC') {
1010
1028
  // USDC in, USDC out — fees were ETH but we don't subtract them from USDC PnL.
1011
1029
  const entryUsdc = BigInt(entryRawStr);
1012
1030
  const costUsdc = (entryUsdc * tokensToSellRaw) / totalRaw;
1013
1031
  const realized6 = BigInt(outRaw) - costUsdc;
1014
- realizedEth = Number(realized6) / 1e6; // realizedEth field reused for USDC value
1032
+ realizedAmount = Number(realized6) / 1e6;
1015
1033
  }
1016
1034
  else {
1017
- // Mixed in/out (rare — shouldn't happen with symmetric exit). Best-effort: native units.
1018
- realizedEth = 0;
1035
+ // Mixed in/out (rare — shouldn't happen with symmetric exit). Best-effort.
1036
+ realizedAmount = 0;
1019
1037
  }
1020
1038
  const nowIso = new Date().toISOString();
1021
1039
  position.sells.push({
@@ -1023,32 +1041,25 @@ export async function sellBase(opts) {
1023
1041
  time: nowIso,
1024
1042
  tokensIn: tokensInDisplay,
1025
1043
  tokensInRaw: tokensToSellRaw.toString(),
1026
- ethOut: outDisplay,
1027
- ethOutRawWei: outRaw,
1028
1044
  percentRequested: opts.percent,
1029
- realizedEth,
1030
1045
  reason: opts.reason.trim(),
1031
1046
  feeWei: swap.feeWei,
1032
1047
  slippageBpsUsed: slippageBps,
1033
1048
  protectedExec: !!opts.protectedExec,
1034
1049
  outputAsset,
1035
- // Canonical, asset-tagged accounting — preferred over legacy `ethOut*` fields.
1036
1050
  output: { asset: outputAsset, raw: outRaw, display: outDisplay },
1037
- realized: { asset: outputAsset, amount: realizedEth },
1051
+ realized: { asset: outputAsset, amount: realizedAmount },
1038
1052
  });
1039
- position.pnl.realizedEth = position.sells.reduce((a, s) => a + s.realizedEth, 0);
1040
- position.pnl.realized = { asset: position.entry.inputAsset ?? 'ETH', amount: position.pnl.realizedEth };
1053
+ const totalRealized = position.sells.reduce((a, s) => a + (s.realized?.amount ?? 0), 0);
1054
+ position.pnl.realized = { asset: position.entry.inputAsset ?? 'ETH', amount: totalRealized };
1041
1055
  // Position closes when (a) the user explicitly sold 100% — even if the
1042
1056
  // on-chain balance was less than the book amount due to drift — or (b) the
1043
- // sum of recorded sells exceeds the book entry amount. Both paths land at
1044
- // status='closed', and we account for any drift between book and chain so
1045
- // partial sells of a drifted balance still close correctly when fully exited.
1057
+ // sum of recorded sells exceeds the book entry amount.
1046
1058
  const newSoldRaw = soldRaw + tokensToSellRaw;
1047
1059
  const fullyExited = opts.percent >= 100 ||
1048
1060
  newSoldRaw + reconcileDriftRaw >= totalRaw;
1049
1061
  if (fullyExited) {
1050
1062
  position.status = 'closed';
1051
- position.pnl.unrealizedEth = 0;
1052
1063
  position.pnl.unrealizedPct = 0;
1053
1064
  position.pnl.unrealized = { asset: position.entry.inputAsset ?? 'ETH', amount: 0 };
1054
1065
  }
@@ -1056,6 +1067,9 @@ export async function sellBase(opts) {
1056
1067
  // position file (otherwise a simulation can "close" a real position).
1057
1068
  if (!opts.dryRun)
1058
1069
  writePosition(position);
1070
+ const realizedSign = realizedAmount >= 0 ? '+' : '';
1071
+ const realizedDigits = outputAsset === 'USDC' ? 6 : 8;
1072
+ const summary = `${opts.dryRun ? '[dry-run] ' : ''}Sold ${tokensInDisplay} ${opts.ca} for ${outDisplay}; realized ${realizedSign}${realizedAmount.toFixed(realizedDigits)} ${outputAsset}; position ${position.status}.`;
1059
1073
  return {
1060
1074
  positionPath: opts.dryRun
1061
1075
  ? `simulated:${opts.ca}`
@@ -1063,9 +1077,6 @@ export async function sellBase(opts) {
1063
1077
  txHash: swap.txHash,
1064
1078
  tokensIn: tokensInDisplay,
1065
1079
  tokensInRaw: tokensToSellRaw.toString(),
1066
- ethOut: outDisplay,
1067
- ethOutRawWei: outRaw,
1068
- realizedEth,
1069
1080
  positionStatus: position.status,
1070
1081
  wallet: signer.address,
1071
1082
  mint: opts.ca,
@@ -1078,7 +1089,8 @@ export async function sellBase(opts) {
1078
1089
  outputAsset,
1079
1090
  reconcileDriftRaw: reconcileDriftRaw === 0n ? undefined : reconcileDriftRaw.toString(),
1080
1091
  output: { asset: outputAsset, raw: outRaw, display: outDisplay },
1081
- realized: { asset: outputAsset, amount: realizedEth },
1092
+ realized: { asset: outputAsset, amount: realizedAmount },
1093
+ summary,
1082
1094
  };
1083
1095
  }
1084
1096
  export async function syncBase(opts = {}) {
@@ -1134,7 +1146,6 @@ export async function syncBase(opts = {}) {
1134
1146
  unrealizedPct = remainingCost > 0n
1135
1147
  ? (Number(diff) / Number(remainingCost)) * 100
1136
1148
  : 0;
1137
- p.pnl.unrealizedEth = unrealizedEth;
1138
1149
  p.pnl.unrealizedPct = unrealizedPct;
1139
1150
  p.pnl.lastPricedAt = nowIso;
1140
1151
  p.pnl.unrealized = { asset: p.entry.inputAsset ?? 'ETH', amount: unrealizedEth };
@@ -1144,7 +1155,6 @@ export async function syncBase(opts = {}) {
1144
1155
  }
1145
1156
  }
1146
1157
  else {
1147
- p.pnl.unrealizedEth = 0;
1148
1158
  p.pnl.unrealizedPct = 0;
1149
1159
  p.pnl.unrealized = { asset: p.entry.inputAsset ?? 'ETH', amount: 0 };
1150
1160
  }
@@ -1303,8 +1313,6 @@ export async function buy(opts) {
1303
1313
  riskFlags: opts.riskFlags ?? [],
1304
1314
  sells: [],
1305
1315
  pnl: {
1306
- realizedSol: 0,
1307
- unrealizedSol: 0,
1308
1316
  unrealizedPct: 0,
1309
1317
  lastPricedAt: null,
1310
1318
  realized: { asset: inputAsset, amount: 0 },
@@ -1356,6 +1364,7 @@ export async function buy(opts) {
1356
1364
  protectedExec: !!opts.protectedExec,
1357
1365
  forensics,
1358
1366
  inputAsset,
1367
+ summary: `${opts.dryRun ? '[dry-run] ' : ''}Bought ${tokensOut} ${opts.ca} for ${position.entry.amountIn} on Solana.`,
1359
1368
  };
1360
1369
  }
1361
1370
  export async function sell(opts) {
@@ -1468,10 +1477,7 @@ export async function sell(opts) {
1468
1477
  time: nowIso,
1469
1478
  tokensIn: tokensInDisplay,
1470
1479
  tokensInRaw: tokensToSellRaw.toString(),
1471
- solOut: solOutDisplay,
1472
- solOutRaw,
1473
1480
  percentRequested: opts.percent,
1474
- realizedSol,
1475
1481
  reason: opts.reason.trim(),
1476
1482
  feeLamports,
1477
1483
  tipLamports,
@@ -1479,16 +1485,14 @@ export async function sell(opts) {
1479
1485
  protectedExec: !!opts.protectedExec,
1480
1486
  forensics,
1481
1487
  outputAsset,
1482
- // Canonical, asset-tagged accounting — preferred over legacy `solOut*` fields.
1483
1488
  output: { asset: outputAsset, raw: String(solOutRaw), display: solOutDisplay },
1484
1489
  realized: { asset: outputAsset, amount: realizedSol },
1485
1490
  });
1486
- position.pnl.realizedSol = position.sells.reduce((a, s) => a + s.realizedSol, 0);
1487
- position.pnl.realized = { asset: position.entry.inputAsset ?? 'SOL', amount: position.pnl.realizedSol };
1491
+ const totalRealized = position.sells.reduce((a, s) => a + (s.realized?.amount ?? 0), 0);
1492
+ position.pnl.realized = { asset: position.entry.inputAsset ?? 'SOL', amount: totalRealized };
1488
1493
  const newSoldRaw = soldRaw + tokensToSellRaw;
1489
1494
  if (newSoldRaw >= totalRaw) {
1490
1495
  position.status = 'closed';
1491
- position.pnl.unrealizedSol = 0;
1492
1496
  position.pnl.unrealizedPct = 0;
1493
1497
  position.pnl.unrealized = { asset: position.entry.inputAsset ?? 'SOL', amount: 0 };
1494
1498
  }
@@ -1514,6 +1518,9 @@ export async function sell(opts) {
1514
1518
  forensics,
1515
1519
  });
1516
1520
  }
1521
+ const sign = realizedSol >= 0 ? '+' : '';
1522
+ const digits = outputAsset === 'USDC' ? 6 : 8;
1523
+ const summary = `${opts.dryRun ? '[dry-run] ' : ''}Sold ${tokensInDisplay} ${opts.ca} for ${solOutDisplay}; realized ${sign}${realizedSol.toFixed(digits)} ${outputAsset}; position ${position.status}.`;
1517
1524
  return {
1518
1525
  positionPath: opts.dryRun
1519
1526
  ? `simulated:${opts.ca}`
@@ -1521,9 +1528,6 @@ export async function sell(opts) {
1521
1528
  txSignature: swap.txSignature,
1522
1529
  tokensIn: tokensInDisplay,
1523
1530
  tokensInRaw: tokensToSellRaw.toString(),
1524
- solOut: solOutDisplay,
1525
- solOutRaw,
1526
- realizedSol,
1527
1531
  positionStatus: position.status,
1528
1532
  wallet: signer.address,
1529
1533
  mint: opts.ca,
@@ -1537,6 +1541,7 @@ export async function sell(opts) {
1537
1541
  outputAsset,
1538
1542
  output: { asset: outputAsset, raw: String(solOutRaw), display: solOutDisplay },
1539
1543
  realized: { asset: outputAsset, amount: realizedSol },
1544
+ summary,
1540
1545
  };
1541
1546
  }
1542
1547
  export async function sync(opts = {}) {
@@ -1594,7 +1599,6 @@ export async function sync(opts = {}) {
1594
1599
  unrealizedSol = usdcOut - remCost; // field name kept; holds USDC realized for USDC positions
1595
1600
  unrealizedPct = remCost > 0 ? (unrealizedSol / remCost) * 100 : 0;
1596
1601
  }
1597
- p.pnl.unrealizedSol = unrealizedSol;
1598
1602
  p.pnl.unrealizedPct = unrealizedPct;
1599
1603
  p.pnl.lastPricedAt = nowIso;
1600
1604
  p.pnl.unrealized = { asset: p.entry.inputAsset ?? 'SOL', amount: unrealizedSol };
@@ -1604,7 +1608,6 @@ export async function sync(opts = {}) {
1604
1608
  }
1605
1609
  }
1606
1610
  else {
1607
- p.pnl.unrealizedSol = 0;
1608
1611
  p.pnl.unrealizedPct = 0;
1609
1612
  p.pnl.unrealized = { asset: p.entry.inputAsset ?? 'SOL', amount: 0 };
1610
1613
  }
@@ -1793,27 +1796,31 @@ export async function computePnl(opts = {}) {
1793
1796
  let usdcRealized = 0, usdcUnrealized = 0, usdcCount = 0;
1794
1797
  for (const p of solanaPositions) {
1795
1798
  const asset = p.entry.inputAsset ?? 'SOL';
1799
+ const realized = p.pnl.realized?.amount ?? 0;
1800
+ const unrealized = p.pnl.unrealized?.amount ?? 0;
1796
1801
  if (asset === 'USDC') {
1797
- usdcRealized += p.pnl.realizedSol; // field name kept for back-compat; value is USDC
1798
- usdcUnrealized += p.pnl.unrealizedSol;
1802
+ usdcRealized += realized;
1803
+ usdcUnrealized += unrealized;
1799
1804
  usdcCount += 1;
1800
1805
  }
1801
1806
  else {
1802
- solRealized += p.pnl.realizedSol;
1803
- solUnrealized += p.pnl.unrealizedSol;
1807
+ solRealized += realized;
1808
+ solUnrealized += unrealized;
1804
1809
  solCount += 1;
1805
1810
  }
1806
1811
  }
1807
1812
  for (const p of basePositions) {
1808
1813
  const asset = p.entry.inputAsset ?? 'ETH';
1814
+ const realized = p.pnl.realized?.amount ?? 0;
1815
+ const unrealized = p.pnl.unrealized?.amount ?? 0;
1809
1816
  if (asset === 'USDC') {
1810
- usdcRealized += p.pnl.realizedEth;
1811
- usdcUnrealized += p.pnl.unrealizedEth;
1817
+ usdcRealized += realized;
1818
+ usdcUnrealized += unrealized;
1812
1819
  usdcCount += 1;
1813
1820
  }
1814
1821
  else {
1815
- ethRealized += p.pnl.realizedEth;
1816
- ethUnrealized += p.pnl.unrealizedEth;
1822
+ ethRealized += realized;
1823
+ ethUnrealized += unrealized;
1817
1824
  ethCount += 1;
1818
1825
  }
1819
1826
  }
@@ -1863,8 +1870,8 @@ export async function computePnl(opts = {}) {
1863
1870
  entry = { key: displayKey, realized: 0, unrealized: 0, count: 0, unit: asset, chain: 'solana' };
1864
1871
  map.set(key, entry);
1865
1872
  }
1866
- entry.realized += p.pnl.realizedSol;
1867
- entry.unrealized += p.pnl.unrealizedSol;
1873
+ entry.realized += p.pnl.realized?.amount ?? 0;
1874
+ entry.unrealized += p.pnl.unrealized?.amount ?? 0;
1868
1875
  entry.count += 1;
1869
1876
  }
1870
1877
  for (const p of basePositions) {
@@ -1876,8 +1883,8 @@ export async function computePnl(opts = {}) {
1876
1883
  entry = { key: displayKey, realized: 0, unrealized: 0, count: 0, unit: asset, chain: 'base' };
1877
1884
  map.set(key, entry);
1878
1885
  }
1879
- entry.realized += p.pnl.realizedEth;
1880
- entry.unrealized += p.pnl.unrealizedEth;
1886
+ entry.realized += p.pnl.realized?.amount ?? 0;
1887
+ entry.unrealized += p.pnl.unrealized?.amount ?? 0;
1881
1888
  entry.count += 1;
1882
1889
  }
1883
1890
  byGroup = Array.from(map.values());