jebel-quant/jquantstats/portfolio-diagnostics
Claude Code and agent skill by jebel-quant/jquantstats
- community
Checking that the SKILL.md file is still in the repository.
Diagnose wrong, null or surprising jquantstats results — null propagation, a misinferred periods_per_year, a lost date axis, benchmark misalignment, or a raised JQuantStatsError. Use when a metric returns null or NaN, when numbers disagree with QuantStats or another tool, when a Sharpe or volatility looks implausible, or when construction fails.
Install
npx vskill@latest install jebel-quant/jquantstats/portfolio-diagnosticsSKILL.md
Diagnosing a portfolio
Metrics here fail quietly more often than loudly. A wrong number that looks plausible is the failure mode to hunt.
Start with the digest, not the metric
Read ${CLAUDE_PLUGIN_ROOT}/reference/portfolio-context.md, then rebuild and re-read the digest — it already carries the evidence:
check reporting STALE explains a whole class of "the number changed" reports: the inputs moved after the digest was written. Work through the digest's warnings, nulls, nans and shape blocks before touching the metric that was complained about.
The five silent failures
1. Nulls — inconsistent, not uniform
Polars keeps null (missing) and NaN (IEEE-754) distinct, and null_strategy acts on null only. Handling then varies by metric: aggregations skip nulls, which shrinks the effective sample without saying so, while cumulative and rolling paths can propagate them.
Fix by making the sample explicit at construction: Data.from_returns(..., null_strategy="drop"). Options are None (default, pass through), "drop", "forward_fill", "raise". Use "raise" while debugging — turning a silent failure loud is usually the fastest route. When reproducing QuantStats numbers, "drop" is what matches pandas.
NullsInReturnsError is the loud version of this, raised where a null cannot be tolerated at all.
2. A lost date axis — the date naming trap
The Portfolio → Data bridge keeps the date column only when it is named exactly `date` (lowercase). Under any other name the dates are dropped and replaced by a positional integer index. Nothing is raised.
The consequences are not cosmetic: periods_per_year falls back to a default instead of being inferred, so every annualised metric shifts, and date-axis charts lose their x-axis. On identical data, renaming date → Date moved Sharpe from 100.33 to 83.36 in testing.
The plugin's loader normalises Portfolio inputs to date and reports having done so. If a portfolio was built by hand, check this first.
3. periods_per_year inferred wrongly
Annualisation of sharpe, sortino and volatility runs off this number.
The digest also reports periods_per_year_empirical — observations per calendar year measured from the span. A mismatch means the annualisation is wrong by roughly its square root: business-daily should land near 252, weekly near 52, monthly near 12. If the inference is off, pass periods= (or periods_per_year=, depending on the metric — the two kwarg names are not used consistently) explicitly rather than accepting the default.
4. Benchmark misalignment
The benchmark is passed once at construction and aligned to the returns. BenchmarkAlignmentWarning is emitted when that alignment drops rows — do not suppress it. beta, alpha, r_squared, information_ratio and the capture ratios then describe only the overlap, which may be much shorter than the portfolio.
NoBenchmarkError means a benchmark-dependent statistic was requested on a context built without one — rebuild with --benchmark, don't work around it.
5. The right name, different semantics
Same metric name, different convention from QuantStats:
conditional_value_at_riskacceptsconfidence=0.95oralpha=0.05, never
both (that raises ValueError); omitting both gives alpha=0.05.
value_at_risktakesalphaonly — noconfidenceparameter, on the same
object as its CVaR sibling that has one.
information_ratiois raw and non-annualised by default; pass
annualise=True to scale it.
volatilityannualises by default — and spells itannualize, with a z.
When construction raises
The library uses typed domain errors, so the class names the problem:
| Exception | Cause | |
|---|---|---|
MissingDateColumnError | no date column where one is required — check date_col | |
InvalidPricesTypeError, InvalidCashPositionTypeError | a pandas object or Series was passed; both must be pl.DataFrame | |
RowCountMismatchError | prices and positions have different row counts — align before constructing, don't truncate blindly | |
NonPositiveAumError | aum must be strictly positive | |
NoAssetColumnsError | no numeric asset columns — usually the date column was not excluded, or everything parsed as string | |
PositionExprColumnError | a position expression names columns absent from prices | |
NegativeCostBpsError, NegativeAnnualFeeError, InvalidMaxBpsError | invalid cost inputs | |
UncleanSeriesError | a derived series hit null or non-finite values — trace back to the input nulls | |
NonPositiveWindowError, NonPositivePeriodsPerYearError | invalid rolling window or annualisation factor | |
IntegerIndexBoundError | truncate received a non-integer where a row index was expected |
All subclass JQuantStatsError, so except JQuantStatsError catches the domain set without swallowing real bugs.
Reconciling against QuantStats
When numbers disagree with a QuantStats reference, work in this order — the common cause is upstream of the metric:
- Sample.
null_strategy="drop"to match pandas' silentNaNdropping. - Periodicity. Confirm
periods_per_yearmatches what the other tool assumed. - Date axis. Confirm the index is temporal, not positional.
- Convention. Check the tail spelling, the annualisation flag, and whether
the alias you are comparing against maps to a different canonical metric.
- Only then compare the metric itself.
The repo's own tests/test_jquantstats/test_migration/ validates metrics against the quantstats reference implementation and is the authority on which differences are intentional.
Verify names before concluding a metric is broken
A missing name is usually a QuantStats alias that was never ported (cvar, ror, upi, r2, ghpr, win_loss_ratio). Report that rather than wrapping it.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingplugin/skills/portfolio-diagnostics/SKILL.md
Popularity
- Repository stars
- 42
- Repository forks
- 9
- 7-day trend score
- 19
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/jebel-quant/jquantstats/portfolio-diagnostics)