Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions src/engine/fx-fifo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,34 +92,53 @@ export class FxFifoEngine {
const events: FxEvent[] = [];

for (const trade of trades) {
if (trade.currency === "EUR") continue;
if (trade.assetCategory !== "CASH") continue;
if (FxFifoEngine.isFxconv(trade)) continue;

const date = normalizeDate(trade.settlementDate || trade.tradeDate);
const ecbRate = getEcbRate(rateMap, date, trade.currency);
const quantity = new Decimal(trade.quantity).abs();

// Commission increases cost basis (BUY) or reduces proceeds (SELL)
let commissionEur: Decimal | undefined;
const commAbs = new Decimal(trade.commission).abs();
if (commAbs.greaterThan(0)) {
const commCcy = trade.commissionCurrency || trade.currency;
if (commCcy === "EUR") {
commissionEur = commAbs;
if (trade.currency === "EUR") continue;
if (trade.assetCategory === "CASH") {

if (FxFifoEngine.isFxconv(trade)) continue;

// Commission increases cost basis (BUY) or reduces proceeds (SELL)
let commissionEur: Decimal | undefined;
const commAbs = new Decimal(trade.commission).abs();
if (commAbs.greaterThan(0)) {
const commCcy = trade.commissionCurrency || trade.currency;
if (commCcy === "EUR") {
commissionEur = commAbs;
} else {
const commRate = getEcbRate(rateMap, date, commCcy);
commissionEur = commAbs.mul(commRate);
}
}

if (trade.buySell === "BUY") {
events.push({ date, currency: trade.currency, quantity, ecbRate, trigger: "conversion", commissionEur });
} else {
const commRate = getEcbRate(rateMap, date, commCcy);
commissionEur = commAbs.mul(commRate);
events.push({ date, currency: trade.currency, quantity: quantity.negated(), ecbRate, trigger: "conversion", commissionEur });
}
}
} else if (trade.assetCategory !== "WAR") {
// Multi-currency account: securities trade = implicit FX event
const tradeMoney = new Decimal(trade.tradeMoney).abs();
if (tradeMoney.isZero()) continue;

if (trade.buySell === "BUY") {
events.push({ date, currency: trade.currency, quantity, ecbRate, trigger: "conversion", commissionEur });
} else {
events.push({ date, currency: trade.currency, quantity: quantity.negated(), ecbRate, trigger: "conversion", commissionEur });
if (trade.buySell === "BUY") {
events.push({ date, currency: trade.currency, quantity: tradeMoney.negated(), ecbRate, trigger: "stock_purchase" });
} else {
events.push({ date, currency: trade.currency, quantity: tradeMoney, ecbRate, trigger: "stock_sale" });
}

// Commission also consumes FCY (paid in commissionCurrency)
const commission = new Decimal(trade.commission).abs();
if (commission.greaterThan(0) && trade.commissionCurrency !== "EUR") {
const commRate = getEcbRate(rateMap, date, trade.commissionCurrency);
events.push({ date, currency: trade.commissionCurrency, quantity: commission.negated(), ecbRate: commRate, trigger: "commission" });
}
Comment on lines +134 to +139
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing fallback for commissionCurrency when undefined.

CASH trades at line 109 use trade.commissionCurrency || trade.currency as a fallback. Here, trade.commissionCurrency is used directly. If undefined, the condition !== "EUR" passes, then getEcbRate receives undefined and the event gets currency: undefined.

Proposed fix
         // Commission also consumes FCY (paid in commissionCurrency)
         const commission = new Decimal(trade.commission).abs();
-        if (commission.greaterThan(0) && trade.commissionCurrency !== "EUR") {
-          const commRate = getEcbRate(rateMap, date, trade.commissionCurrency);
-          events.push({ date, currency: trade.commissionCurrency, quantity: commission.negated(), ecbRate: commRate, trigger: "commission" });
+        const commCcy = trade.commissionCurrency || trade.currency;
+        if (commission.greaterThan(0) && commCcy !== "EUR") {
+          const commRate = getEcbRate(rateMap, date, commCcy);
+          events.push({ date, currency: commCcy, quantity: commission.negated(), ecbRate: commRate, trigger: "commission" });
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Commission also consumes FCY (paid in commissionCurrency)
const commission = new Decimal(trade.commission).abs();
if (commission.greaterThan(0) && trade.commissionCurrency !== "EUR") {
const commRate = getEcbRate(rateMap, date, trade.commissionCurrency);
events.push({ date, currency: trade.commissionCurrency, quantity: commission.negated(), ecbRate: commRate, trigger: "commission" });
}
// Commission also consumes FCY (paid in commissionCurrency)
const commission = new Decimal(trade.commission).abs();
const commCcy = trade.commissionCurrency || trade.currency;
if (commission.greaterThan(0) && commCcy !== "EUR") {
const commRate = getEcbRate(rateMap, date, commCcy);
events.push({ date, currency: commCcy, quantity: commission.negated(), ecbRate: commRate, trigger: "commission" });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/fx-fifo.ts` around lines 134 - 139, The commission block uses
trade.commissionCurrency directly which can be undefined; create a fallback like
const commCurrency = trade.commissionCurrency || trade.currency and use
commCurrency for the EUR check, for the getEcbRate call (getEcbRate(rateMap,
date, commCurrency)) and as the event.currency so undefined is never passed;
keep the existing commission amount logic and only push the event when
commCurrency !== "EUR".

}
}

return events;
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/tax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export interface FxLot {
}

/** What triggered an FX disposal */
export type FxTrigger = "conversion" | "dividend" | "interest" | "commission";
export type FxTrigger = "conversion" | "dividend" | "interest" | "commission" | "stock_purchase" | "stock_sale";

/** Result of consuming FX lots via FIFO for a currency disposal */
export interface FxDisposal {
Expand Down
Loading