From 2e3e58668a802cc766a3bbf188f3ce44825bdcf5 Mon Sep 17 00:00:00 2001 From: sesha456 Date: Tue, 15 Sep 2026 23:09:10 -0500 Subject: [PATCH] Document current X-Ray rule-based logic Read through the existing X-Ray rules engine and wrote up notes on how the 17 rules/8 categories work today, since there's no scoring layer yet to build the Portfolio Health Score on top of. --- docs/sesha-notes/01-xray-current-logic.md | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/sesha-notes/01-xray-current-logic.md diff --git a/docs/sesha-notes/01-xray-current-logic.md b/docs/sesha-notes/01-xray-current-logic.md new file mode 100644 index 0000000000..1413a9333b --- /dev/null +++ b/docs/sesha-notes/01-xray-current-logic.md @@ -0,0 +1,133 @@ +# X-Ray Feature Notes — What Exists Today + +**Author:** Sesha Siva Sankar (Member 1) +**Date:** Tue, Sep 15 +**Task:** Read existing Ghostfolio X-Ray code and document the current rule-based logic + +--- + +## Why I'm writing this + +Before I can design the new Portfolio Health Score, I need to understand what X-Ray already +does. Turns out it's basically a checklist of 17 independent "rules" that each say pass/fail +about one thing (too much cash, too little diversification, high fees, etc). Below is how it +all fits together, explained the way I'd explain it to a teammate over coffee, not a spec doc. + +--- + +## 1. The basic building block: a "Rule" + +- Every check in X-Ray (e.g. "is your cash too low?", "are your fees too high?") is its own + little class. +- They all follow the same shape, because they extend one shared base class + (`apps/api/src/models/rule.ts`). +- Every rule has to answer 4 questions: + - What's your name? (`getName()`) + - What settings do you need? (`getSettings()`) + - What's the actual check logic? (`evaluate()`) + - Are you configurable, and what's your default threshold? (`getConfiguration()`) +- The output of a check is dead simple — just: + - `value`: true or false (did it pass?) + - `evaluation`: a human-readable sentence explaining why +- There's no "warning" or "medium risk" state right now. It's binary — pass or fail. That's + one of the first things that'll need to change for a 0-100 health score. + +## 2. Where the rules actually get run + +- There's a small helper (`rules.service.ts`) whose whole job is: "given a list of rule + objects, run each one and hand back the results." It doesn't decide which rules to run — + it just executes them. +- The real wiring happens in `portfolio.service.ts`, in a big function called `getReport()`. + This is where someone manually created each rule object (currency risk rule, fee rule, + emergency fund rule, etc.) and grouped them into 8 categories. +- Nothing here is dynamic or plug-and-play — if we want to add a new rule, we'd have to edit + this file directly and add it to a category by hand. Worth flagging for later. + +## 3. The 8 categories and their 17 rules + +Here's the full list, grouped the way the app groups them: + +- **Liquidity** + - *Buying Power* — checks if your uninvested cash is below a minimum threshold (default: + just checks it's not zero / not too low). Fully configurable. + +- **Emergency Fund** + - *Setup* — checks if you've even set an emergency fund target. No threshold, just + "did you set one or not." + - *Coverage* — checks if your tagged emergency-fund holdings + cash actually cover your + target. Can fail three ways: no target set, holdings alone overshoot the target + (weird case), or holdings+cash undershoot it. Only runs if you've set a target > 0. + +- **Currency Cluster Risk** (are you too concentrated in one currency?) + - *Current Investment* — fails if your single biggest currency is more than 50% of your + portfolio value (configurable %). + - *Base Currency Investment* — fails if your biggest currency bucket (by money invested) + isn't your home currency. No threshold to tune, it's just yes/no. + +- **Asset Class Cluster Risk** (stocks vs bonds balance) + - *Equity* — fails if equities are outside a 78%–82% band. + - *Fixed Income* — fails if bonds are outside an 18%–22% band. + - Interesting detail: these are *band* checks (too high OR too low both fail), not simple + ceilings like most of the others. + +- **Account Cluster Risk** + - *Current Investment* — fails if one account holds more than 50% of your total money. + - *Single Account* — fails if you only have one account at all (or zero, which is invalid). + +- **Economic Market Cluster Risk** (developed vs emerging market split) + - *Developed Markets* — band check, must be within 68%–72%. + - *Emerging Markets* — band check, must be within 28%–32%. + +- **Regional Market Cluster Risk** (geographic spread) + - Five separate rules, one per region: Asia Pacific, Emerging Markets, Europe, Japan, + North America. Each is a band check against a hardcoded "ideal" range for that region + (e.g. North America should be 65%–69% of your advanced-market exposure). + +- **Fees** + - *Fee Ratio* — fails if total fees paid are more than 1% of your total trading volume + (buys + sells combined). Configurable. + +## 4. Can users customize any of this? + +- Yes — every rule reads its settings from `user.settings.xRayRules`, so a user can turn + individual rules on/off, or override the threshold numbers, from their account settings. +- If a user hasn't set anything, it falls back to hardcoded defaults (the percentages listed + above). + +## 5. Is there already a "score"? (Important — this is basically our starting point) + +- Short answer: **not really.** There's no 0-100 number anywhere today. +- The only "aggregation" that exists is a simple count: + - How many rules are turned on (`rulesActiveCount`) + - How many of those passed (`rulesFulfilledCount`) +- The UI just shows this as a sentence like "7 out of 9 rules align with your portfolio," + with a green check if everything passed, or a warning icon if not. No weighting, no + per-category breakdown, no severity levels. +- This tells me the Health Score feature is a genuinely new layer on top — we're not + reshaping something that already exists, we're building the scoring/weighting logic + from scratch on top of these 17 pass/fail checks. + +## 6. Key files, if I (or anyone else) need to go back and look + +- `apps/api/src/models/rule.ts` — the shared base class every rule extends +- `apps/api/src/models/interfaces/rule.interface.ts` — the contract every rule follows +- `apps/api/src/models/interfaces/evaluation-result.interface.ts` — the pass/fail + message + shape +- `apps/api/src/app/portfolio/rules.service.ts` — runs whatever rules it's handed +- `apps/api/src/app/portfolio/portfolio.service.ts` — `getReport()` (around line 1272) wires + up all 17 rules into categories; `getReportStatistics()` (around line 2005) does the + simple pass-count +- `apps/api/src/models/rules/**` — the actual 17 rule implementations, one folder per + category +- `libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts` — per-user rule + settings shape +- `apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html` — where the + "N out of M rules" summary is rendered today + +## 7. What this means for tomorrow's task + +Tomorrow I need to define the Portfolio Health Score (0-100) data model — inputs, weights, +output schema. Based on today's reading, the natural "inputs" are these 17 existing +pass/fail evaluations (or the underlying ratios they're computed from), and the main design +question is how to turn "17 independent yes/no checks across 8 categories" into one +weighted 0-100 number instead of a flat count.