Install
$ agentstack add skill-bencharoenwong-parallax-workflows-parallax-portfolio-builder ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Portfolio Builder
When not to use
- Analyzing existing portfolio → use /parallax-client-review
- Screening without allocation → use /parallax-thematic-screen
- Single stock analysis → use /parallax-should-i-buy
- Rebalancing existing portfolio → use /parallax-rebalance
Gotchas
- JIT-load _parallax/parallax-conventions.md for RIC resolution, parallel execution, and fallback patterns
- JIT-load _parallax/house-view/loader.md FIRST; if active view present, follow §2 (validation), §3 (multipliers), §4 (conflict resolution), §5 (output rendering), §6 (audit)
- When active view is present, use the view-aware disclaimer per loader.md §5; otherwise use the standard disclaimer
- buildstockuniverse returns relevance-ranked results — re-rank by factor scores for quality
- Run redundancy check BEFORE finalizing allocation to avoid sector concentration
- analyze_portfolio responses often exceed 180K chars. Use streaming JSON extraction (Step 6) instead of waiting for full response; stream validation results progressively to user as they arrive.
- Weights should sum to ~1.0 in final output
- Include both the allocation table AND the factor rationale for each pick
- The saved house view never carries Parallax-derived overlays. When the active view is silent on a dimension this portfolio decision needs, EITHER (a) treat as neutral [default — non-blocking, RM-fan-out-safe] OR (b) JIT-augment via --augment-silent flag with provenance tagged per holding [auditable]. Never fold augmentation back into the saved view.
- View-status banner is REQUIRED first thing in output when active view exists — never bury after the holdings table. If execution gets compressed, this is the section that must NOT be dropped. RM uses the banner as the primary signal that the view is active and what's being applied.
- Phase A Parallelization: View load and universe build run in parallel (view load is fast; universe build is the latency bottleneck). Both complete before Phase B begins.
- JIT-load
_parallax/white-label/integration-pattern.mdbefore the Pre-Render step. Loader call isload_visual_branding()(7-key visual subset; voice structurally excluded —branding["voice"]raisesKeyError). Apply §5 (Branding Header) and §7 (About This Report) in Output Format. - Optional
audience=argument:client_safe | internal_analyst; precedence followsparallax-conventions.md§13.1.
Construct a portfolio from a plain-English investment thesis using Parallax MCP tools.
Usage
/parallax-portfolio-builder "defensive dividend-focused Asian equities under $10B market cap"
/parallax-portfolio-builder "US tech companies with strong quality and momentum scores" top_n=10
/parallax-portfolio-builder "ESG leaders in European industrials"
/parallax-portfolio-builder "EM-tilt balanced" --augment-silent # JIT-augment dimensions the active view is silent on (default: off)
/parallax-portfolio-builder "defensive dividend-focused Asian equities" audience=client_safe
Workflow
Call ToolSearch with query "+Parallax" to load the deferred MCP tool schemas before the first mcp__claude_ai_Parallax__* call. Execute using mcp__claude_ai_Parallax__* tools. JIT-load _parallax/parallax-conventions.md for execution mode and fallback patterns. JIT-load _parallax/house-view/loader.md for active-view validation, tilt application, and conflict-resolution rules.
Pre-flight: house-view drift check
JIT-load _parallax/house-view/auto-on-load-judge-pattern.md and follow its protocol. If the protocol surfaces a banner, render it before proceeding to this skill's main workflow.
Skip this pre-flight if invoked with --skip-drift-check or if no active house view exists.
Phase A — Parallel Initialization & Scoring
Execute Steps 0–2 in parallel. All three must complete before Phase B begins. Best-effort error handling: if any step fails (timeout, validation error), flag it, continue with fallback, and proceed to Phase B.
Step 0: Load Active House View (parallel with Step 1 & 2)
- Per
loader.md§1-§2: read view if present, validate hash and expiry, capture tilt vector + excludes + extraction-confidence warnings. If validation fails, run without view per loader.md §2 "Failure handling." If no view present, proceed normally. - Fallback on timeout (rare): log warning, continue with empty view. Non-blocking.
Step 1: Build Universe (parallel with Step 0 & 2)
- Resolve user thesis vs. view per loader.md §4. If view present, prepend tilt context to the query (e.g., "exclude tech, overweight defensive sectors"). Call
build_stock_universe. Force-include any sectors/themes with view tilt = +2 if absent from initial candidates. - Step 1a: Divergence assertion (per loader.md §5 rule 4) — if the tilt-prepended query named N≥2 sectors/themes, compute
max_sector_share / totalin returned candidates. If > 0.6, emit fail-loud warning: "universe collapsed to single sector despite multi-sector request." Do NOT proceed with a collapsed universe. Default action (per loader.md §5 rule 4 post-2026-04-24): re-issue as N parallel per-sectorbuild_stock_universecalls and merge/dedupe by symbol (keep highest-rank hit on collisions). Fall back to refusing to render only if the per-sector re-issue is itself skewed (e.g., some requested sectors return zero results). Known upstream limitation on the single-call path. - Fallback on timeout: retry once with narrower query (e.g., single sector from thesis). If still times out, set
universe = []anduniverse_status = "unavailable", continue to Phase B with the empty candidate list, and flag the unavailable state in output under "Universe Built". Do NOT callcheck_portfolio_redundancyas a placeholder — it requires aholdingsparameter and will fail or return nonsense without one. - Step 1b: JIT augmentation gate (opt-in) — After universe is built, identify dimensions the THESIS depends on but the active view is silent on. Default behavior (non-blocking, RM-fan-out-safe): treat silent dimensions as neutral, render a one-line note in output:
Active view is silent on ; using neutral. Run with --augment-silent to fill from Parallax data for THIS portfolio.Opt-in--augment-silent: JIT-load_parallax/house-view/gap_detect+gap_suggest. Construct a synthetic "draft view" from the active view but with the THESIS-relevant silent dimensions explicitly enumerated. Callgap_detect.detect_gaps()scoped to those dimensions. Callgap_suggest.plan_calls(gaps, available_markets=mcp__claude_ai_Parallax__list_macro_countries()["markets"]). Fire the planned MCP calls in parallel.gap_suggest.fold_responses()→ list of Suggestions. Apply each Suggestion as a tilt only for THIS portfolio decision — do NOT write back to~/.parallax/active-house-view/. Tag each augmented dimension with[parallax_jit, []@]for the output table's "Tilt Source" column. - Step 1b-bis: Augmentation-not-used invitation — If the user did NOT pass
--augment-silentAND Step 1b's gap detection found ≥1 thesis-relevant silent dimension, render this single line in output (between the universe-built section and the Selected Holdings table):ℹ Active view is silent on (relevant to your thesis). Re-run with --augment-silent to fill these from current Parallax data for THIS portfolio (saved view never mutates).Single line, no AskUserQuestion (RM-fan-out-safe — does not block invocation). Skip if no silent dims found OR if --augment-silent was already passed.
Step 2: Score Top Picks (parallel with Step 0 & 1)
- For top N (default 10), fire all N
get_peer_snapshotcalls AND all Nget_company_infocalls in a single tool-call turn — 2N calls dispatching simultaneously. Do NOT iterate one-at-a-time; that is the dominant latency leak in this skill. Both tools are independent per_parallax/parallax-conventions.md§3. - Prefer per-holding
get_peer_snapshotaggregation over batchquick_portfolio_scoresfor portfolio factor profile — the batch tool has a symbol-mapping bug (see parallax-conventions.md §2) that mis-attributes scores to the wrong company for most non-US tickers. Only usequick_portfolio_scoresafter every returnedcompany_namehas been cross-validated againstget_company_info. - Fallback on timeout: if N > 5, retry with top 5 only and flag in output. If single-call timeouts persist, degrade to quickportfolioscores with name validation.
Phase B — Sequential Selection, Validation & Optimization
Begin only after Phase A completes. Steps 3–6 have tight dependencies; execute sequentially.
Empty-universe gate (REQUIRED FIRST CHECK): If universe_status = "unavailable" from Phase A Step 1 (Build Universe), skip Steps 3-6 entirely. Render Output Format with Universe Built flagged as "unavailable" and no Selected Holdings table. Append an audit log entry per loader.md §6 noting the abort. Do NOT call check_portfolio_redundancy, analyze_portfolio, or quick_portfolio_scores — these tools require holdings input and the universe failure means no holdings exist to validate.
Step 3: Rank & Select
- If view present, re-rank by
composite × multiplier(holding's sector/region/theme)per loader.md §3 multiplier tables. Apply factor tilt re-weighting per loader.md §3 "Factor tilts." Drop candidates that matchtilts.excludes(surface block message per loader.md §4 exception). Select top holdings (default 5-8).
Step 4: Redundancy Check
- Call
check_portfolio_redundancywith proposed equal-weight allocation. Sanity-check the response: if the portfolio has >60% concentration in a single sector butsector_concentration: {}is empty and"well-diversified"is returned, the tool's concentration detection has silently failed — compute concentration client-side from per-holding sectors and flag the tool bug in output.
Step 5: Optimize Weights
- Adjust weights based on scores, redundancy flags, sector balance, AND tilt multipliers. Cap any single sector at 2× its neutral exposure when view is +2 (loader.md §3 cap rule). For factor profile verification, aggregate per-holding
get_peer_snapshotscores weighted by portfolio weight (do NOT rely onquick_portfolio_scoresunless all company names validated).
Step 6: Validate (streaming)
- Call
analyze_portfolioon the final allocation withstream=trueto enable streaming JSON extraction. Fallback for large responses: use streaming; if streaming unavailable or times out, fall back tocheck_portfolio_redundancy+quick_portfolio_scoresfor light validation. - Sanity-check
check_portfolio_redundancyresponse in this fallback path (mirrors Step 4): if the portfolio has >60% concentration in a single sector butsector_concentration: {}is empty and"well-diversified"is returned, the tool's concentration detection has silently failed — compute concentration client-side from per-holding sectors and flag the tool bug in output. The Step 4 sanity-check gate must fire on the fallback path as well, not only on the primary call. - Scope of this fallback:
check_portfolio_redundancy+quick_portfolio_scorescovers redundancy and basic factor coverage only.analyze_portfoliofields that are NOT recoverable from the fallback — rolling metrics, drawdown analysis, contribution attribution, performance time series — must be flagged as "unavailable in fallback path" in output. Do not silently omit them; the operator must know what wasn't validated. - Symbol-mapping caveat (mirrors parallax-conventions.md §2):
quick_portfolio_scoreson this fallback path is subject to the same symbol-mapping bug — non-US ticker scores may be mis-attributed. Cross-validate everycompany_namereturned by quickportfolioscores againstget_company_infobefore treating any score as authoritative. Mismatches flagged ⚠ MISMATCH and excluded from validation output per Step 2 / loader.md §5 rule 3. - When
--augment-silentwas applied: the audit entry MUST carryaugmented_dimensions: [{path, source_tool, source_call_args, data_as_of}]so the per-portfolio JIT augmentation provenance is on the audit chain and recoverable for compliance review. When--augment-silentwas NOT applied but silent dimensions existed, logsilent_dimensions_skipped: [...]so the auditor can see what wasn't filled. - Append audit log entry per loader.md §6.
Pre-Render — Load white-label branding
Load _parallax/white-label/integration-pattern.md §2 and compute white_label_active + client_name per that section. Apply §5 (Branding Header) and §7 (About This Report) when composing the Output Format. The loader returns exactly seven keys; any other access (e.g. branding["voice"]) raises KeyError — structurally enforced by loader.py.
Render — deterministic gate (LAST step, mandatory)
Compose the complete report per Output Format below, then run it through the shared render gate in one Bash step before replying. Use a private mktemp file (never a fixed/predictable path — /tmp symlink hazard). The shared gate is _parallax/render_gate.py, a sibling of the directory you loaded this SKILL.md from; pass this skill's key (use the loaded directory's absolute path as ``):
DRAFT="$(mktemp "${TMPDIR:-/tmp}/builder.XXXXXX")"
cat > "$DRAFT"
REPORT
python3 "/../_parallax/render_gate.py" --skill portfolio-builder `. If `--augment-silent` was used, append `JIT-augmented: `. This preamble is the user's primary signal that the view is active and what it's doing. If execution gets token-compressed, KEEP this section — drop other sections first if needed.
- **Branding Header** (only if `white_label_active` AND `client_name != ""`) — single line immediately below the House View Preamble (or at the very top if no view): `**** portfolio construction`. Logo handling per integration-pattern.md §5: empty path → text only; URL → embed; absolute local (`/` or `~`) → skip embed and append `Logo on file: ` to About This Report.
- **Investment Thesis** (restate and refine the user's intent; note any view-vs-thesis conflicts inline per loader.md §4)
- **Universe Built** (how many candidates, key sectors; note force-includes from +2 tilts and excludes applied; surface divergence-assertion result per loader.md §5 rule 4)
- **Why These Holdings**: 2-3 plain-language sentences summarizing why the selected names fit the stated thesis; no factor jargon without the §13.3 gloss. Rendered in both audience modes.
- **Selected Holdings** (table: `input_ticker`, `returned_name` (from scoring-tool response), `expected_name` (from get_company_info), sector, total score, weight, key factor strengths; if view active, include a "Tilt Effect" column showing the multiplier applied to each holding AND a "Tilt Source" column tagging the dimension's source. Under `audience=client_safe`, move the `Tilt Effect` / `Tilt Source` columns out of the primary table into a trailing **Methodology appendix (internal)** section that reproduces the full row × dimension tag map. **Tilt Source enforcement (compliance contract):** EVERY row × dimension cell MUST carry a non-empty tag in `{[house_view: =], [parallax_jit, []@], [neutral, ]}`. If any cell would be empty, that's a runtime bug — emit `⚠ Tilt Source missing for × ; verify before relying on this output` and refuse to claim the output is compliance-ready. This compliance contract applies to the appendix identically — relocation never deletes or weakens it. **Flag any row where `returned_name ≠ expected_name` after normalizing both per conventions §2 step 2 with ⚠ MISMATCH and do not treat that row's scores as authoritative** — per loader.md §5 rule 3. `⚠ MISMATCH` flags stay in the primary table in both modes because data-integrity warnings are non-suppressible per §13.2.)
- **View-Effect Summary** (only when view active AND ≥1 holding's ranking shifted vs raw-score order) — One paragraph: `Without the active view, the top-N ranking wo
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [bencharoenwong](https://github.com/bencharoenwong)
- **Source:** [bencharoenwong/parallax-workflows](https://github.com/bencharoenwong/parallax-workflows)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.