diff --git a/dashboard/backend/domain/agents/marketplace.py b/dashboard/backend/domain/agents/marketplace.py
index 2a5a4ee2..f4097d30 100644
--- a/dashboard/backend/domain/agents/marketplace.py
+++ b/dashboard/backend/domain/agents/marketplace.py
@@ -1,8 +1,9 @@
"""Open agent templates for the Agent Supermarket.
-Templates are defined in ``dashboard/config/marketplace.json`` so baseline open
-agents can be added without schema migrations. The listing is public; cloning
-creates a user-owned built-in agent with the template's pipeline copied in.
+Templates are defined in ``dashboard/config/marketplace.json`` so competition
+models and hosted agents can be added without schema migrations. The listing
+is public; cloning creates a user-owned built-in agent with the template's
+pipeline copied in.
"""
from __future__ import annotations
@@ -11,14 +12,29 @@
from functools import lru_cache
from typing import Any, Dict, List, Optional
-from dashboard.backend.domain.agents.taxonomy import (
- category_sort_rank,
- normalize_category,
-)
+from dashboard.backend.domain.agents.taxonomy import normalize_category
from dashboard.backend.paths import CONFIG_DIR
_MARKETPLACE_PATH = CONFIG_DIR / "marketplace.json"
+# Community supermarket rows. Declared order is display order: LLMs first,
+# then Agents. Unknown / omitted values fall through ``_normalize_shelf``.
+MARKETPLACE_SHELVES = ("llms", "open")
+
+
+def _normalize_shelf(raw: Dict[str, Any]) -> str:
+ """Return ``llms`` or ``open``.
+
+ Explicit ``shelf`` on the catalog row wins. Otherwise a non-pipeline
+ runtime (today: AI Hedge Fund) is an open agent, so a future hosted
+ project does not have to remember the field to land on the right row.
+ """
+ explicit = str(raw.get("shelf") or "").strip().lower()
+ if explicit in MARKETPLACE_SHELVES:
+ return explicit
+ runtime_type = str(raw.get("runtime_type") or "pipeline")
+ return "open" if runtime_type != "pipeline" else "llms"
+
def _public_template(raw: Dict[str, Any]) -> Dict[str, Any]:
# This "category" and an agent's "category" used to be two different
@@ -47,6 +63,8 @@ def _public_template(raw: Dict[str, Any]) -> Dict[str, Any]:
"author": raw.get("author") or "Community",
"runtime_type": runtime_type,
"step_count": step_count,
+ "shelf": _normalize_shelf(raw),
+ "card_subtitle": str(raw.get("card_subtitle") or "").strip() or None,
"mode": (
"runtime"
if runtime_type != "pipeline"
@@ -82,18 +100,17 @@ def _load_catalog() -> Dict[str, Dict[str, Any]]:
def list_marketplace_templates() -> List[Dict[str, Any]]:
- """Return public marketplace cards grouped by shelf, then sorted by name.
+ """Return public marketplace cards grouped by supermarket shelf.
- Ordered by ``category_sort_rank`` rather than by the slug itself: the slugs
- are not alphabetical in shelf order, so a plain ``sorted`` on the raw value
- leads the Community listing with the A-share shelf. Uncategorized templates
- sort last.
+ ``MARKETPLACE_SHELVES`` declaration order is the page order (LLMs, then
+ Agents). Within a shelf the catalog's insertion order is preserved
+ (stable sort) so Community can list models in the leaderboard roster
+ order without a second sort key.
"""
items = [_public_template(raw) for raw in _load_catalog().values()]
- return sorted(
- items,
- key=lambda t: (category_sort_rank(t.get("category")), str(t.get("name") or "")),
- )
+ # _public_template always sets "shelf" via _normalize_shelf, which only
+ # returns members of MARKETPLACE_SHELVES -- no fallback branch needed.
+ return sorted(items, key=lambda t: MARKETPLACE_SHELVES.index(t["shelf"]))
def get_marketplace_template(template_id: str) -> Optional[Dict[str, Any]]:
diff --git a/dashboard/backend/tests/test_admin_analytics_frontend.py b/dashboard/backend/tests/test_admin_analytics_frontend.py
index 9599048a..9bf878c0 100644
--- a/dashboard/backend/tests/test_admin_analytics_frontend.py
+++ b/dashboard/backend/tests/test_admin_analytics_frontend.py
@@ -190,8 +190,8 @@ def test_app_lifecycle_and_cache_versions_are_wired():
assert "window.AdminAnalytics.syncAuth(user)" in APP_JS
assert "window.AdminAnalytics.onEnter()" in APP_JS
assert "window.AdminAnalytics.refresh()" in APP_JS
- assert 'styles.css?v=130' in APP_HTML
- assert 'app.js?v=125' in APP_HTML
+ assert 'styles.css?v=132' in APP_HTML
+ assert 'app.js?v=127' in APP_HTML
assert 'js/admin-analytics.js?v=2' in APP_HTML
assert 'js/admin-tabs.js?v=3' in APP_HTML
diff --git a/dashboard/backend/tests/test_agent_starter_defaults.py b/dashboard/backend/tests/test_agent_starter_defaults.py
index 79e4cdb1..b447d99c 100644
--- a/dashboard/backend/tests/test_agent_starter_defaults.py
+++ b/dashboard/backend/tests/test_agent_starter_defaults.py
@@ -175,10 +175,30 @@ def test_create_still_returns_the_one_time_api_key(client):
assert body["session_id"]
-def test_marketplace_clone_keeps_its_own_pipeline(client):
- """A template with its own pipeline must not be overwritten by the seed."""
+def test_marketplace_clone_keeps_its_own_pipeline(client, monkeypatch):
+ """A template with its own pipeline must not be overwritten by the seed.
+
+ The shipped catalog is now one-step competition-model cards plus a hosted
+ runtime, so the three-step shape is faked — the guard is the copy, not
+ which template still ships it.
+ """
+ fake_template = {
+ "template_id": "three-step-template",
+ "name": "Three-Step Template",
+ "model_name": "anthropic/claude-haiku-4-5",
+ "pipeline": [
+ {"id": "sub_gather", "presetKey": "info_gather", "prompt": "gather"},
+ {"id": "sub_signal", "presetKey": "info_to_signal", "prompt": "signal"},
+ {"id": "sub_exec", "presetKey": "signal_to_execution", "prompt": "exec"},
+ ],
+ }
+ monkeypatch.setattr(
+ marketplace_module,
+ "get_marketplace_template",
+ lambda template_id: fake_template if template_id == "three-step-template" else None,
+ )
cloned = client.post(
- "/api/v1/agents/marketplace/pipeline-analyst/clone",
+ "/api/v1/agents/marketplace/three-step-template/clone",
json={},
headers={"X-Session-Id": str(uuid.uuid4())},
)
diff --git a/dashboard/backend/tests/test_agents_api.py b/dashboard/backend/tests/test_agents_api.py
index 402d481c..75157822 100644
--- a/dashboard/backend/tests/test_agents_api.py
+++ b/dashboard/backend/tests/test_agents_api.py
@@ -735,7 +735,7 @@ def test_marketplace_listing_and_clone(client):
assert listing.status_code == 200
templates = listing.json()["templates"]
assert templates
- assert any(t["template_id"] == "balanced-starter" for t in templates)
+ assert any(t["template_id"] == "claude-haiku-4-5" for t in templates)
hedge_fund_card = next(
t for t in templates if t["template_id"] == "ai-hedge-fund"
)
@@ -751,13 +751,13 @@ def test_marketplace_listing_and_clone(client):
browser_session = str(uuid.uuid4())
headers = {"X-Session-Id": browser_session}
cloned = client.post(
- "/api/v1/agents/marketplace/balanced-starter/clone",
+ "/api/v1/agents/marketplace/claude-haiku-4-5/clone",
json={},
headers=headers,
)
assert cloned.status_code == 200
agent = cloned.json()["agent"]
- assert agent["name"] == "Balanced Starter"
+ assert agent["name"] == "Claude Haiku 4.5"
assert agent["agent_type"] == "builtin"
assert agent.get("pipeline")
assert agent["pipeline"][0]["presetKey"] == "simple_instruction"
@@ -825,43 +825,38 @@ def test_marketplace_catalog_shape():
)
# "Pipeline" is banned product-copy vocabulary (glossary: pipeline ->
- # "multi-step strategy"); the template_id stays "pipeline-analyst" since
- # it's an API identifier baked into clone URLs, but the display name --
- # the card's largest text -- must not carry the word.
+ # "multi-step strategy"). The hosted card and the competition-model cards
+ # must not put that word in the display name -- the card's largest text.
names = {t["template_id"]: t["name"] for t in templates}
- assert names["pipeline-analyst"] == "Three-Step Analyst"
- assert "Pipeline Analyst" not in names.values()
+ assert names["ai-hedge-fund"] == "AI Hedge Fund"
+ assert "Pipeline" not in " ".join(names.values())
def test_marketplace_listing_is_ordered_by_shelf_not_by_slug():
- """Community cards group by market in *declaration* order, not slug order.
-
- The recategorization onto slugs quietly changed which card leads the page:
- ``sorted`` on the raw value orders cn_ashares < us_stocks, so the A-share
- template became card #1 on a U.S.-focused product. Nothing caught it because
- no test asserted order. ``category_sort_rank`` keys on the AgentCategory
- Literal's declaration order instead, which is also the order MARKET_LABELS
- renders the market chips in, so the two surfaces agree.
+ """Community cards group by supermarket shelf, LLMs then Agents.
+
+ Within a shelf the catalog's insertion order is preserved so the LLM
+ row can follow the leaderboard roster instead of alphabetical names.
"""
import dashboard.backend.domain.agents.marketplace as marketplace_mod
- from dashboard.backend.domain.agents.taxonomy import (
- AGENT_CATEGORY_ORDER,
- category_sort_rank,
- )
marketplace_mod.reload_marketplace_catalog()
templates = marketplace_mod.list_marketplace_templates()
- ranks = [category_sort_rank(t.get("category")) for t in templates]
- assert ranks == sorted(ranks), "templates are not grouped in shelf order"
-
- # The U.S. market leads; uncategorized templates never do.
- assert templates[0]["category"] == AGENT_CATEGORY_ORDER[0] == "us_stocks"
- assert templates[-1]["category"] == "cn_ashares"
-
- # Within a shelf, still by name.
- us_stocks = [t["name"] for t in templates if t["category"] == "us_stocks"]
- assert us_stocks == sorted(us_stocks)
+ shelves = [t.get("shelf") for t in templates]
+ assert set(shelves) <= set(marketplace_mod.MARKETPLACE_SHELVES)
+ llms = [t for t in templates if t["shelf"] == "llms"]
+ opens = [t for t in templates if t["shelf"] == "open"]
+ assert llms, "the LLM shelf is empty"
+ assert opens, "the Agents shelf is empty"
+ assert templates[0]["shelf"] == "llms"
+ assert templates[-1]["shelf"] == "open"
+ assert [t["name"] for t in opens][0] == "AI Hedge Fund"
+ assert {t["template_id"] for t in opens} >= {
+ "ai-hedge-fund",
+ "balanced-starter",
+ "ashare-momentum-t1",
+ }
def test_uncategorized_templates_sort_last_and_carry_no_fake_shelf():
@@ -1416,7 +1411,7 @@ def test_builtin_listing_echoes_category(client):
def test_clone_honours_a_model_name_override(client):
"""Community's "Choose model" affordance clones a template onto another model."""
cloned = client.post(
- "/api/v1/agents/marketplace/balanced-starter/clone",
+ "/api/v1/agents/marketplace/claude-haiku-4-5/clone",
json={"model_name": "deepseek/deepseek-v4-pro"},
headers={"X-Session-Id": str(uuid.uuid4())},
)
@@ -1429,7 +1424,7 @@ def test_clone_falls_back_to_the_template_model(client, blank):
"""Omitted or blank means "use the template's model", not "use empty"."""
body = {} if blank is None else {"model_name": blank}
cloned = client.post(
- "/api/v1/agents/marketplace/balanced-starter/clone",
+ "/api/v1/agents/marketplace/claude-haiku-4-5/clone",
json=body,
headers={"X-Session-Id": str(uuid.uuid4())},
)
@@ -1441,7 +1436,7 @@ def test_clone_does_not_validate_the_model_name(client):
"""No whitelist here: POST /agents and PATCH /agents/{id} don't have one either,
and a Literal would drag in the openapi enum deploy gate #313 discharged."""
cloned = client.post(
- "/api/v1/agents/marketplace/balanced-starter/clone",
+ "/api/v1/agents/marketplace/claude-haiku-4-5/clone",
json={"model_name": "some/unreleased-model"},
headers={"X-Session-Id": str(uuid.uuid4())},
)
diff --git a/dashboard/backend/tests/test_analytics_frontend.py b/dashboard/backend/tests/test_analytics_frontend.py
index b6f61650..71e443d9 100644
--- a/dashboard/backend/tests/test_analytics_frontend.py
+++ b/dashboard/backend/tests/test_analytics_frontend.py
@@ -41,7 +41,7 @@ def _function_body(source: str, signature: str) -> str:
def test_analytics_script_loads_between_app_and_page_scripts():
- app_at = APP_HTML.index('')
+ app_at = APP_HTML.index('')
analytics_at = APP_HTML.index(
''
)
diff --git a/dashboard/backend/tests/test_app_copy_register.py b/dashboard/backend/tests/test_app_copy_register.py
index 16fcff56..51021165 100644
--- a/dashboard/backend/tests/test_app_copy_register.py
+++ b/dashboard/backend/tests/test_app_copy_register.py
@@ -403,12 +403,12 @@ def test_marketplace_mode_chip_labels_avoid_banned_words():
`renderMarketplaceGrid` so this doesn't over-match an unrelated string
elsewhere in app.js.
"""
- body = fn_body("function renderMarketplaceGrid")
+ body = fn_body("function buildMarketplaceCardHtml")
assert "'Hosted runtime'" not in body
assert "'Multi-step pipeline'" not in body
- assert "'Hosted'" in body
- assert "'Multi-step strategy'" in body
- assert "'Simple instruction'" in body
+ assert "Open Source" in body
+ assert "'Simple instruction'" not in body
+ assert "'Multi-step strategy'" not in body
def test_each_agent_gets_its_own_trading_session_is_gone():
diff --git a/dashboard/backend/tests/test_backtest_comparison_frontend.py b/dashboard/backend/tests/test_backtest_comparison_frontend.py
index 818dffbb..6d2bb002 100644
--- a/dashboard/backend/tests/test_backtest_comparison_frontend.py
+++ b/dashboard/backend/tests/test_backtest_comparison_frontend.py
@@ -191,8 +191,8 @@ def test_exact_raw_ties_mark_every_tied_series_best():
def test_comparison_script_and_semantic_table_ship_before_app():
helper = ''
- app = ''
- assert 'href="styles.css?v=130"' in APP_HTML
+ app = ''
+ assert 'href="styles.css?v=132"' in APP_HTML
assert APP_HTML.index(helper) < APP_HTML.index(app)
for element_id in (
"performanceLegend",
diff --git a/dashboard/backend/tests/test_frontend_fast_boot.py b/dashboard/backend/tests/test_frontend_fast_boot.py
index 91b77efc..338470c3 100644
--- a/dashboard/backend/tests/test_frontend_fast_boot.py
+++ b/dashboard/backend/tests/test_frontend_fast_boot.py
@@ -191,9 +191,9 @@ def test_cache_busters_bumped():
# the next bump, so the exact one looks like the broken guard and gets
# "fixed" by loosening it. That collision has already cost this repo one
# round of follow-ups (#347/#348).
- assert "app.js?v=125" in APP_HTML
+ assert "app.js?v=127" in APP_HTML
assert "js/agent-editor.js?v=30" in APP_HTML
- assert "styles.css?v=130" in APP_HTML
+ assert "styles.css?v=132" in APP_HTML
assert "js/leaderboard.js?v=32" in APP_HTML
assert "home-page.js?v=50" in APP_HTML
assert "js/credit-format.js?v=1" in APP_HTML
diff --git a/dashboard/backend/tests/test_frontend_model_facets.py b/dashboard/backend/tests/test_frontend_model_facets.py
index 51d78316..cd035f5b 100644
--- a/dashboard/backend/tests/test_frontend_model_facets.py
+++ b/dashboard/backend/tests/test_frontend_model_facets.py
@@ -1,10 +1,10 @@
-"""Guards for the Community model-vendor facet.
+"""Guards for the Community model-vendor table.
The vendor axis is a pure derivation from `model_name` -- no column, no
-migration. MODEL_VENDORS is its single source of truth: chip order, display
-label and open/closed licence all come from one table, so a badge cannot drift
-from the vendor it describes. A wrong badge is a factual claim about someone
-else's product.
+migration. MODEL_VENDORS is its single source of truth: the pinned key/label
+rows, the LLM tile's company submeta and the (currently unrendered) licence
+metadata all come from one table, so an entry cannot drift from the vendor it
+describes. A wrong entry is a factual claim about someone else's product.
"""
import json
@@ -89,35 +89,6 @@ def test_supported_model_vendor_fields_agree_with_the_vendor_table():
assert vendor == expected, f"{slug} is tagged {vendor!r}, table says {expected!r}"
-@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
-def test_provider_label_output_is_unchanged():
- """These six strings ship on cards today. The refactor must not touch them."""
- script = f"""
-{js_const("MODEL_VENDORS")}
-{fn_body("function modelVendorKey")}
-{fn_body("function formatModelProviderLabel")}
-const cases = ['anthropic/claude-haiku-4-5', 'nvidia/nemotron-3-nano-30b-a3b',
- 'deepseek/deepseek-v4-pro', 'openai/gpt-5.5',
- 'google/gemini-3.1-pro-preview', 'qwen/qwen3.7-plus',
- 'totally/unknown', ''];
-console.log(JSON.stringify(cases.map(formatModelProviderLabel)));
-"""
- result = subprocess.run(
- ["node", "-e", script], capture_output=True, text=True, timeout=30
- )
- assert result.returncode == 0, result.stderr
- assert json.loads(result.stdout) == [
- "Powered by Claude",
- "Powered by NVIDIA Nemotron",
- "Powered by DeepSeek",
- "Powered by GPT",
- "Powered by Gemini",
- "Powered by Qwen",
- "AI-powered",
- "AI-powered",
- ]
-
-
@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
def test_unknown_vendor_resolves_to_empty_string():
"""Same contract as agentMarketKey: unknown stays visible under All and is
@@ -143,38 +114,34 @@ def test_unknown_vendor_resolves_to_empty_string():
def test_vendor_chip_container_exists_in_the_community_view():
+ """Model-vendor chips were removed: the LLM shelf already names each model."""
community = APP_HTML[
APP_HTML.index('
marketplaceMarketChips(templates).map((c) => c.key);
+const labels = (templates) => marketplaceMarketChips(templates).map((c) => c.label);
+console.log(JSON.stringify({{
+ usOnly: keys([{{category: 'us_stocks'}}, {{category: 'us_stocks'}}]),
+ mixed: keys([{{category: 'cn_ashares'}}, {{category: 'us_stocks'}}]),
+ empty: keys([]),
+ unknown: keys([{{category: 'crypto'}}]),
+ usLabels: labels([{{category: 'us_stocks'}}]),
+}}));
+"""
+ result = subprocess.run(
+ ["node", "-e", script], capture_output=True, text=True, timeout=30
+ )
+ assert result.returncode == 0, result.stderr
+ data = json.loads(result.stdout)
+ assert data["usOnly"] == ["all", "us_stocks"]
+ assert data["mixed"] == ["all", "us_stocks", "cn_ashares"]
+ assert data["empty"] == ["all"]
+ assert data["unknown"] == ["all"]
+ assert data["usLabels"] == ["All", "U.S."]
+
+
@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
def test_shipped_filter_ands_market_and_vendor_not_or():
- """Lifts the REAL getFilteredMarketplaceTemplates (not a reimplementation) --
- the six chip/empty-state tests above never execute this function, so a
- regression that drops the vendor filter or ORs it with the market filter
- passed the whole suite until this test existed."""
+ """Vendor chips are gone; the market filter still applies to both shelves."""
script = f"""
-{js_const("MODEL_VENDORS")}
-{fn_body("function modelVendorKey")}
const document = {{ getElementById: () => null }};
const marketplaceTemplates = [
{{ template_id: 't1', category: 'us_stocks', model_name: 'anthropic/claude-haiku-4-5' }},
@@ -245,25 +234,18 @@ def test_shipped_filter_ands_market_and_vendor_not_or():
{{ template_id: 't5', category: 'us_stocks', model_name: 'totally/unknown' }},
];
let marketplaceCategoryFilter = 'all';
-let marketplaceVendorFilter = 'all';
{fn_body("function getFilteredMarketplaceTemplates")}
function ids() {{ return getFilteredMarketplaceTemplates().map((t) => t.template_id); }}
const results = {{}};
-marketplaceCategoryFilter = 'us_stocks'; marketplaceVendorFilter = 'all';
-results.marketOnly = ids();
-
-marketplaceCategoryFilter = 'all'; marketplaceVendorFilter = 'qwen';
-results.vendorOnly = ids();
-
-marketplaceCategoryFilter = 'us_stocks'; marketplaceVendorFilter = 'qwen';
-results.both = ids();
-
-marketplaceCategoryFilter = 'all'; marketplaceVendorFilter = 'anthropic';
-results.vendorExplicit = ids();
-
+marketplaceCategoryFilter = 'us_stocks';
+results.marketUs = ids();
+marketplaceCategoryFilter = 'cn_ashares';
+results.marketCn = ids();
+marketplaceCategoryFilter = 'all';
+results.marketAll = ids();
console.log(JSON.stringify(results));
"""
result = subprocess.run(
@@ -271,233 +253,172 @@ def test_shipped_filter_ands_market_and_vendor_not_or():
)
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
- # Market filter alone: both known-vendor templates plus the unknown-vendor
- # one -- unknown must stay visible when no vendor chip narrows it.
- assert set(data["marketOnly"]) == {"t1", "t2", "t5"}
- # Vendor filter alone: both markets, only the matching vendor.
- assert set(data["vendorOnly"]) == {"t2", "t4"}
- # Both together must be the INTERSECTION (t2 only), not the union
- # (which would also include t1, t4, t5).
- assert data["both"] == ["t2"], "market+vendor must AND, not OR"
- # An explicit vendor chip excludes the unknown-vendor template -- it is
- # visible only under vendor 'all'.
- assert set(data["vendorExplicit"]) == {"t1", "t3"}
-
-
-@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
-def test_shipped_vendor_chip_order_follows_model_vendors_not_catalog_order():
- """Lifts the REAL renderMarketplaceVendorChips against a synthetic DOM and a
- catalog whose insertion order is deliberately scrambled relative to
- MODEL_VENDORS. test_vendor_chips_are_derived_not_hardcoded only substring
- -checks the function's source text, so it never actually executes this and
- can't tell catalog order from MODEL_VENDORS order."""
- script = f"""
-function escapeHtml(s) {{ return String(s); }}
-{js_const("MODEL_VENDORS")}
-{fn_body("function modelVendorKey")}
-
-function makeContainer() {{
- let buttons = [];
- return {{
- querySelectorAll() {{ return buttons; }},
- set innerHTML(html) {{
- buttons = [];
- const re = /data-marketplace-vendor="([^"]*)"/g;
- let m;
- while ((m = re.exec(html))) {{
- buttons.push({{
- dataset: {{ marketplaceVendor: m[1] }},
- classList: {{ toggle() {{}} }},
- setAttribute() {{}},
- }});
- }}
- }},
- }};
-}}
-const container = makeContainer();
-const document = {{ getElementById: (id) => (id === 'marketplaceVendorChips' ? container : null) }};
-
-// MODEL_VENDORS order is anthropic, openai, google, deepseek, qwen, nvidia,
-// meta, xai. This catalog is inserted qwen, anthropic, deepseek -- scrambled
-// on purpose -- plus one unknown-vendor template and no openai template.
-const marketplaceTemplates = [
- {{ template_id: 'a', model_name: 'qwen/qwen3.7-plus' }},
- {{ template_id: 'b', model_name: 'anthropic/claude-haiku-4-5' }},
- {{ template_id: 'c', model_name: 'deepseek/deepseek-v4-pro' }},
- {{ template_id: 'd', model_name: 'totally/unknown' }},
-];
-let marketplaceVendorFilter = 'all';
-
-{fn_body("function renderMarketplaceVendorChips")}
-renderMarketplaceVendorChips();
-
-console.log(JSON.stringify(container.querySelectorAll().map((b) => b.dataset.marketplaceVendor)));
-"""
- result = subprocess.run(
- ["node", "-e", script], capture_output=True, text=True, timeout=30
- )
- assert result.returncode == 0, result.stderr
- keys = json.loads(result.stdout)
- # 'openai' has no template so it must not get a chip; order must follow
- # MODEL_VENDORS (anthropic, deepseek, qwen), not the catalog's insertion
- # order (qwen, anthropic, deepseek).
- assert keys == ["all", "anthropic", "deepseek", "qwen"]
+ assert set(data["marketUs"]) == {"t1", "t2", "t5"}
+ assert set(data["marketCn"]) == {"t3", "t4"}
+ assert set(data["marketAll"]) == {"t1", "t2", "t3", "t4", "t5"}
def test_only_open_weight_models_get_a_badge():
- """Closed models get NOTHING. A "Closed" label reads as a warning about
- someone else's product; absence is not a negative claim."""
- grid = fn_body("function renderMarketplaceGrid")
- assert "modelVendorLicence" in grid
- assert "Open-source model" in grid
+ """LLM tiles no longer claim licence. Hosted repo cards use an explicit
+ Open Source mark; closed models and strategy templates get nothing."""
+ card = fn_body("function buildMarketplaceCardHtml")
+ assert "Open Source" in card
+ assert "Open-source model" not in card
+ assert ">Open" not in card
assert "Closed-source" not in APP_JS
assert "Proprietary" not in APP_JS
-def test_licence_badge_has_a_style_rule():
- from dashboard.backend.tests._frontend_source import css_blocks
-
- assert css_blocks(".marketplace-licence-badge"), "badge has no styles.css rule"
-
-
-@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
-def test_shipped_grid_badges_only_open_weight_cards():
- """Lifts the REAL renderMarketplaceGrid (not a reimplementation) against a
- synthetic catalog with one open-weight, one closed-weight and one
- unknown-vendor template. The two tests above only substring-check
- renderMarketplaceGrid's source text, so a polarity bug (badging closed
- models instead of open ones) or a "computed but never rendered" bug
- (licenceBadge assigned but not interpolated into the tag row) would both
- pass them silently. This test executes the shipped card template and
- checks the actual HTML each template produces."""
- script = f"""
+_CARD_HELPERS = f"""
{fn_body("function escapeHtml")}
{fn_body("function agentRobotIcon")}
const MARKET_LABELS = {{ us_stocks: 'U.S.', cn_ashares: 'China A-Share' }};
{js_const("MODEL_VENDORS")}
{fn_body("function modelVendorKey")}
-{fn_body("function modelVendorLicence")}
-{fn_body("function formatModelProviderLabel")}
-
-// Collaborators renderMarketplaceGrid calls that render other UI regions --
-// stubbed as no-ops so this test stays scoped to the card template.
-function renderMarketplaceCategoryChips() {{}}
-function renderMarketplaceVendorChips() {{}}
-
-function getFilteredMarketplaceTemplates() {{
- return [
- {{ template_id: 'open1', category: 'us_stocks', name: 'Open Template',
- model_name: 'deepseek/deepseek-v4-pro', tags: [], author: 'Community' }},
- {{ template_id: 'closed1', category: 'us_stocks', name: 'Closed Template',
- model_name: 'anthropic/claude-haiku-4-5', tags: [], author: 'Community' }},
- {{ template_id: 'unknown1', category: 'us_stocks', name: 'Unknown Template',
- model_name: 'totally/unknown', tags: [], author: 'Community' }},
- ];
-}}
-
-const cardHtml = [];
-const grid = {{
- innerHTML: '',
- appendChild(card) {{ cardHtml.push(card.innerHTML); }},
- querySelectorAll() {{ return []; }},
-}};
-const document = {{
- getElementById(id) {{ return id === 'marketplaceGrid' ? grid : null; }},
- createElement() {{ return {{ className: '', innerHTML: '' }}; }},
-}};
+{fn_body("function formatModelCompanyLabel")}
+let marketplaceLeaderboardEntries = [];
+let marketplaceContestMeta = {{ start_date: null, end_date: null, display_capital: null, total_entries: null }};
+{fn_body("function templateMarketplaceShelf")}
+{fn_body("function findMarketplaceLeaderboardEntry")}
+{js_const("MARKETPLACE_MONTHS")}
+{fn_body("function marketplaceBenchmarkEntry")}
+{fn_body("function downsampleMarketplaceCurve")}
+{fn_body("function marketplaceIndexedPctSeries")}
+{fn_body("function formatMarketplaceMd")}
+{fn_body("function formatMarketplaceWindowRange")}
+{fn_body("function formatMarketplaceCapital")}
+{fn_body("function marketplaceNicePctTicks")}
+{fn_body("function marketplaceLinePath")}
+{fn_body("function buildMarketplaceCompareChartHtml")}
+{fn_body("function marketplacePerformanceFor")}
+{fn_body("function formatMarketplaceReturnPct")}
+{fn_body("function marketplaceRepoLabel")}
+{fn_body("function compareMarketplaceTemplatesByRank")}
+{fn_body("function buildMarketplaceCardHtml")}
+"""
-{fn_body("function renderMarketplaceGrid")}
-renderMarketplaceGrid();
-console.log(JSON.stringify(cardHtml.map((html) => html.includes('marketplace-licence-badge'))));
+@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
+def test_shipped_grid_badges_only_open_weight_cards():
+ """Open Source is a repo-card mark, not an Agents-shelf or open-weight claim."""
+ script = f"""
+{_CARD_HELPERS}
+const cards = [
+ buildMarketplaceCardHtml({{ template_id: 'llm', shelf: 'llms', category: 'us_stocks',
+ name: 'DeepSeek V4 Pro', model_name: 'deepseek/deepseek-v4-pro' }}),
+ buildMarketplaceCardHtml({{ template_id: 'llm2', shelf: 'llms', category: 'us_stocks',
+ name: 'Claude Haiku 4.5', model_name: 'anthropic/claude-haiku-4-5' }}),
+ buildMarketplaceCardHtml({{ template_id: 'ai-hedge-fund', shelf: 'open', category: 'us_stocks',
+ name: 'AI Hedge Fund', model_name: 'nvidia/nemotron-3-nano-30b-a3b',
+ card_subtitle: 'Open-source multi-agent system',
+ repo_url: 'https://github.com/virattt/ai-hedge-fund' }}),
+ buildMarketplaceCardHtml({{ template_id: 'balanced-starter', shelf: 'open', category: 'us_stocks',
+ name: 'Balanced Starter', model_name: 'anthropic/claude-haiku-4-5',
+ description: 'A simple starter agent that diversifies across strong stocks.' }}),
+];
+console.log(JSON.stringify(cards.map((html) => html.includes('Open Source'))));
"""
result = subprocess.run(
["node", "-e", script], capture_output=True, text=True, timeout=30
)
assert result.returncode == 0, result.stderr
- # In catalog order: open-weight DeepSeek, closed-weight Claude, unknown vendor.
- assert json.loads(result.stdout) == [True, False, False]
+ assert json.loads(result.stdout) == [False, False, True, False]
@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
def test_closed_card_differs_from_open_card_by_exactly_the_badge():
- """"Absence is not a negative claim" is a stronger promise than "no badge
- with this exact class and text" -- the two tests above enumerate two
- guessed forbidden strings ("Closed-source", "Proprietary"), which cannot
- rule out a differently-worded, differently-classed marker on closed cards
- (e.g. a `.marketplace-vendor-note` span reading "Vendor-locked model").
-
- This renders the REAL renderMarketplaceGrid twice, over two templates that
- are identical except for `model_name` (one open-weight, one closed-weight),
- and diffs the emitted HTML directly: stripping the open-source badge out of
- the open card's HTML must yield the closed card's HTML exactly, modulo the
- one legitimately-differing "Powered by X" label. Any extra marker on the
- closed card -- of any name or wording -- breaks that equality.
- """
+ """Two LLM cards that differ only by model_name must differ only by company."""
script = f"""
-{fn_body("function escapeHtml")}
-{fn_body("function agentRobotIcon")}
-const MARKET_LABELS = {{ us_stocks: 'U.S.', cn_ashares: 'China A-Share' }};
-{js_const("MODEL_VENDORS")}
-{fn_body("function modelVendorKey")}
-{fn_body("function modelVendorLicence")}
-{fn_body("function formatModelProviderLabel")}
-
-function renderMarketplaceCategoryChips() {{}}
-function renderMarketplaceVendorChips() {{}}
-
-let currentTemplates;
-function getFilteredMarketplaceTemplates() {{ return currentTemplates; }}
-let document;
-
-{fn_body("function renderMarketplaceGrid")}
-
+{_CARD_HELPERS}
function renderOneCard(modelName) {{
- const cardHtml = [];
- const grid = {{
- innerHTML: '',
- appendChild(card) {{ cardHtml.push(card.innerHTML); }},
- querySelectorAll() {{ return []; }},
- }};
- document = {{
- getElementById(id) {{ return id === 'marketplaceGrid' ? grid : null; }},
- createElement() {{ return {{ className: '', innerHTML: '' }}; }},
- }};
- // Identical in every field except model_name -- the only legitimate
- // difference between the two cards is the licence and the model label.
- // A shared tag keeps the tag-row wrapper div present on BOTH cards, so the
- // only thing that can differ inside it is the badge span itself -- with no
- // tags, the wrapper disappears entirely on the closed card (no badge, no
- // tags -> nothing to wrap) and that wrapper's absence would itself count as
- // a "difference", masking whether an extra marker was also added.
- currentTemplates = [
- {{ template_id: 'x', category: 'us_stocks', name: 'Same Template',
- model_name: modelName, tags: ['sample'], author: 'Community' }},
- ];
- renderMarketplaceGrid();
- return cardHtml[0];
+ return buildMarketplaceCardHtml({{
+ template_id: 'x', shelf: 'llms', category: 'us_stocks',
+ name: 'Same Template', model_name: modelName,
+ }});
}}
-
const openHtml = renderOneCard('deepseek/deepseek-v4-pro');
const closedHtml = renderOneCard('anthropic/claude-haiku-4-5');
+const openNorm = openHtml.split('DeepSeek').join('COMPANY');
+const closedNorm = closedHtml.split('Anthropic').join('COMPANY');
+console.log(JSON.stringify({{
+ openHasBadge: openHtml.includes('Open Source'),
+ closedHasBadge: closedHtml.includes('Open Source'),
+ equalAfterNormalizing: openNorm === closedNorm,
+}}));
+"""
+ result = subprocess.run(
+ ["node", "-e", script], capture_output=True, text=True, timeout=30
+ )
+ assert result.returncode == 0, result.stderr
+ data = json.loads(result.stdout)
+ assert data["openHasBadge"] is False
+ assert data["closedHasBadge"] is False
+ assert data["equalAfterNormalizing"] is True
-const BADGE = 'Open-source model';
-const openHasBadge = openHtml.includes(BADGE);
-const closedHasBadge = closedHtml.includes('marketplace-licence-badge');
-// Strip only the exact badge span (proves it was actually there) and
-// normalise only the one known-legitimate difference (the model label) --
-// a blanket strip would hide any other marker instead of catching it.
-// Global replace: the label is in both the submeta title attribute and the
-// visible text; String.replace(string) would only hit the first.
-const openWithoutBadge = openHtml.split(BADGE).join('')
- .split('Powered by DeepSeek').join('POWERED_BY_MODEL');
-const closedNormalized = closedHtml
- .split('Powered by Claude').join('POWERED_BY_MODEL');
+@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
+def test_card_uses_overall_rank_and_return_without_placeholders():
+ """Contest stats: trophy rank, Return, model vs DJIA cumulative-return chart."""
+ script = f"""
+{_CARD_HELPERS}
+marketplaceLeaderboardEntries = [
+ {{ is_model: true, model: 'Claude Haiku 4.5', entry_id: 'claude_haiku_4_5',
+ rank: 2, cumulative_return: 0.084,
+ equity_curve: [
+ {{ timestamp: '2026-04-15T00:00:00+00:00', equity: 10000 }},
+ {{ timestamp: '2026-04-30T00:00:00+00:00', equity: 10400 }},
+ {{ timestamp: '2026-05-15T00:00:00+00:00', equity: 10840 }}
+ ] }},
+ {{ is_model: false, model: 'DJIA', entry_id: 'djia_index', rank: 8,
+ cumulative_return: 0.022,
+ equity_curve: [
+ {{ timestamp: '2026-04-15T00:00:00+00:00', equity: 10000 }},
+ {{ timestamp: '2026-04-30T00:00:00+00:00', equity: 10100 }},
+ {{ timestamp: '2026-05-15T00:00:00+00:00', equity: 10224 }}
+ ] }},
+];
+marketplaceContestMeta = {{
+ start_date: '2026-04-15', end_date: '2026-05-15',
+ display_capital: 10000, total_entries: 12,
+}};
+const ranked = buildMarketplaceCardHtml({{
+ template_id: 'claude-haiku-4-5', shelf: 'llms', category: 'us_stocks',
+ name: 'Claude Haiku 4.5', model_name: 'anthropic/claude-haiku-4-5',
+}});
+const unranked = buildMarketplaceCardHtml({{
+ template_id: 'ai-hedge-fund', shelf: 'open', category: 'us_stocks',
+ name: 'AI Hedge Fund', model_name: 'nvidia/nemotron-3-nano-30b-a3b',
+ card_subtitle: 'Open-source multi-agent system',
+ description: 'A team of AI investors that analyzes the market, develops trading ideas, and tests them through backtesting.',
+ repo_url: 'https://github.com/virattt/ai-hedge-fund',
+}});
console.log(JSON.stringify({{
- openHasBadge,
- closedHasBadge,
- equalAfterNormalizing: openWithoutBadge === closedNormalized,
+ rankedHasMedian: ranked.includes('Median Return'),
+ rankedHasLeaderboardReturn: ranked.includes('Leaderboard Return'),
+ rankedHasOverallRank: ranked.includes('Overall Rank'),
+ rankedHasCompetition: ranked.includes('Competition result'),
+ rankedHasTotalReturn: ranked.includes('Total Return'),
+ rankedHasReturnLabel: ranked.includes('>Return<'),
+ rankedHasHashTwo: ranked.includes('#2 of 12'),
+ rankedHasPct: ranked.includes('+8.4%'),
+ rankedHasChart: ranked.includes('mp-compare-chart'),
+ rankedHasAgentLegend: ranked.includes('Claude Haiku 4.5'),
+ rankedHasDjiaLegend: ranked.includes('>DJIA<') || ranked.includes('DJIA'),
+ rankedHasDjiaBenchmark: ranked.includes('DJIA Benchmark'),
+ rankedHasAgentPortfolio: ranked.includes('Agent Portfolio'),
+ rankedHasMeta: ranked.includes('DJIA 30'),
+ rankedHasWindow: ranked.includes('Apr 15–May 15'),
+ rankedHasCapital: ranked.includes('$10K'),
+ rankedHasHead: ranked.includes('mp-competition-head'),
+ rankedMetaBeforeBody: ranked.indexOf('DJIA 30') < ranked.indexOf('mp-competition-body')
+ && ranked.indexOf('Competition result') < ranked.indexOf('mp-competition-body'),
+ rankedHasComingSoon: ranked.includes('Performance data coming soon'),
+ unrankedHasCompetition: unranked.includes('Competition result'),
+ unrankedHasSpark: unranked.includes('mp-compare-chart'),
+ unrankedHasRepo: unranked.includes('virattt/ai-hedge-fund'),
+ unrankedHasSubtitle: unranked.includes('Open-source multi-agent system'),
+ unrankedHasOverallRank: unranked.includes('Overall Rank'),
+ unrankedHasDescription: unranked.includes('A team of AI investors that analyzes the market'),
}}));
"""
result = subprocess.run(
@@ -505,27 +426,92 @@ def test_closed_card_differs_from_open_card_by_exactly_the_badge():
)
assert result.returncode == 0, result.stderr
data = json.loads(result.stdout)
- assert data["openHasBadge"] is True
- assert data["closedHasBadge"] is False
- assert data["equalAfterNormalizing"] is True, (
- "closed card must be byte-identical to the open card minus the badge "
- "(modulo the model label) -- any other marker on the closed card is a "
- "negative claim about someone else's product"
+ assert data["rankedHasMedian"] is False
+ assert data["rankedHasLeaderboardReturn"] is False
+ assert data["rankedHasOverallRank"] is False
+ assert data["rankedHasCompetition"] is True
+ assert data["rankedHasTotalReturn"] is False
+ assert data["rankedHasReturnLabel"] is True
+ assert data["rankedHasHashTwo"] is True
+ assert data["rankedHasPct"] is True
+ assert data["rankedHasChart"] is True
+ assert data["rankedHasAgentLegend"] is True
+ assert data["rankedHasDjiaLegend"] is True
+ assert data["rankedHasDjiaBenchmark"] is False
+ assert data["rankedHasAgentPortfolio"] is False
+ assert data["rankedHasMeta"] is True
+ assert data["rankedHasWindow"] is True
+ assert data["rankedHasCapital"] is True
+ assert data["rankedHasHead"] is True
+ assert data["rankedMetaBeforeBody"] is True
+ assert data["rankedHasComingSoon"] is False
+ assert data["unrankedHasCompetition"] is False
+ assert data["unrankedHasSpark"] is False
+ assert data["unrankedHasRepo"] is True
+ assert data["unrankedHasSubtitle"] is True
+ assert data["unrankedHasOverallRank"] is False
+ assert data["unrankedHasDescription"] is True
+
+
+@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
+def test_llm_shelf_sorts_by_leaderboard_rank():
+ script = f"""
+{_CARD_HELPERS}
+marketplaceLeaderboardEntries = [
+ {{ is_model: true, model: 'Claude Haiku 4.5', rank: 11, cumulative_return: 0.0 }},
+ {{ is_model: true, model: 'DeepSeek V4 Pro', rank: 1, cumulative_return: 0.07 }},
+ {{ is_model: true, model: 'Qwen3.7 Plus', rank: 5, cumulative_return: 0.02 }},
+];
+const cards = [
+ {{ template_id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }},
+ {{ template_id: 'deepseek-v4-pro', name: 'DeepSeek V4 Pro' }},
+ {{ template_id: 'qwen3-7-plus', name: 'Qwen3.7 Plus' }},
+ {{ template_id: 'unknown-llm', name: 'Unlisted' }},
+];
+const sorted = cards.slice().sort(compareMarketplaceTemplatesByRank).map((t) => t.template_id);
+console.log(JSON.stringify(sorted));
+"""
+ result = subprocess.run(
+ ["node", "-e", script], capture_output=True, text=True, timeout=30
)
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout) == [
+ "deepseek-v4-pro",
+ "qwen3-7-plus",
+ "claude-haiku-4-5",
+ "unknown-llm",
+ ]
+
+
+def test_marketplace_card_has_no_fixed_min_height():
+ from dashboard.backend.tests._frontend_source import css_blocks
+
+ blocks = css_blocks(".marketplace-card")
+ assert blocks, "marketplace-card has no styles.css rule"
+ assert all("min-height" not in block for block in blocks)
+
+
+def test_llm_grid_sorts_by_leaderboard_rank():
+ grid = fn_body("function renderMarketplaceGrid")
+ assert "compareMarketplaceTemplatesByRank" in grid
+ assert "shelf.key === 'llms'" in grid
def test_primary_clone_cta_is_unchanged():
"""The conversion click keeps its label and its one-click behaviour."""
- grid = fn_body("function renderMarketplaceGrid")
- assert "const cloneLabel = 'Add to My Agents';" in grid
- assert "marketplace-clone-btn" in grid
+ card = fn_body("function buildMarketplaceCardHtml")
+ assert "const cloneLabel = 'Add to My Agents';" in card
+ assert "marketplace-clone-btn" in card
-def test_model_choice_is_a_secondary_affordance():
+def test_model_choice_is_not_on_the_card():
+ """Tiles are the model. Switching models happens in Configure after Add."""
+ card = fn_body("function buildMarketplaceCardHtml")
grid = fn_body("function renderMarketplaceGrid")
- assert "marketplace-clone-model-btn" in grid
- assert "Choose model" in grid
- assert "SUPPORTED_MODELS" in grid
+ for body in (card, grid):
+ assert "marketplace-clone-model-btn" not in body
+ assert "Choose model" not in body
+ assert "marketplace-model-menu" not in body
def test_clone_sends_the_chosen_model():
@@ -533,71 +519,22 @@ def test_clone_sends_the_chosen_model():
assert "model_name" in body
-def test_clone_menu_changes_only_the_model():
- """A second half-Configure inside a clone menu is how two editing surfaces
- start drifting apart. Name, capital and pipeline stay in Configure."""
- grid = fn_body("function renderMarketplaceGrid")
- menu_start = grid.index("marketplace-model-menu")
- menu = grid[menu_start : menu_start + 800]
- for forbidden in ("cash_allocation", "backtest_allocation", "pipeline", "rename"):
- assert forbidden not in menu
-
-
@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
def test_model_picker_gated_on_runtime_type_not_truthiness():
- """`runtime_type` is always present and always truthy -- server-defaulted
- to 'pipeline' for every ordinary template (marketplace.py:
- `str(raw.get("runtime_type") or "pipeline")`). A gate written as
- `template.runtime_type ? '' : (...)` is therefore false for EVERY
- template and would hide the picker everywhere while looking correct on a
- substring-only test. The AI Hedge Fund runtime hardcodes its own model
- (infrastructure/ai_hedge_fund/adapter.py) and never reads the stored
- value, so offering a picker there would let a user "choose" a model that
- is silently ignored.
-
- This executes the REAL renderMarketplaceGrid over one ordinary
- (runtime_type: 'pipeline') template and one hosted (runtime_type:
- 'ai_hedge_fund') template and checks which cards actually get a picker."""
+ """Neither shelf card offers Choose model; both keep Add to My Agents."""
script = f"""
-{fn_body("function escapeHtml")}
-{fn_body("function agentRobotIcon")}
-const MARKET_LABELS = {{ us_stocks: 'U.S.' }};
-{js_const("MODEL_VENDORS")}
-{js_const("SUPPORTED_MODELS")}
-{fn_body("function modelVendorKey")}
-{fn_body("function modelVendorLicence")}
-{fn_body("function formatModelProviderLabel")}
-{fn_body("function normalizeBacktestModelId")}
-
-function renderMarketplaceCategoryChips() {{}}
-function renderMarketplaceVendorChips() {{}}
-
-const marketplaceTemplates = [
- {{ template_id: 'ordinary', category: 'us_stocks', name: 'Ordinary Template',
- model_name: 'anthropic/claude-haiku-4-5', tags: [], author: 'Community',
- runtime_type: 'pipeline' }},
- {{ template_id: 'hosted', category: 'us_stocks', name: 'Hosted Template',
- model_name: 'nvidia/nemotron-3-nano-30b-a3b', tags: [], author: 'Community',
- runtime_type: 'ai_hedge_fund' }},
-];
-function getFilteredMarketplaceTemplates() {{ return marketplaceTemplates; }}
-let marketplaceCloneInFlight = false;
-
-const cardHtml = [];
-const grid = {{
- innerHTML: '',
- appendChild(card) {{ cardHtml.push(card.innerHTML); }},
- querySelectorAll() {{ return []; }},
-}};
-const document = {{
- getElementById(id) {{ return id === 'marketplaceGrid' ? grid : null; }},
- createElement() {{ return {{ className: '', innerHTML: '' }}; }},
-}};
-
-{fn_body("function renderMarketplaceGrid")}
-renderMarketplaceGrid();
-
-console.log(JSON.stringify(cardHtml.map((html) => ({{
+{_CARD_HELPERS}
+const ordinary = buildMarketplaceCardHtml({{
+ template_id: 'ordinary', shelf: 'llms', category: 'us_stocks',
+ name: 'Ordinary Template', model_name: 'anthropic/claude-haiku-4-5',
+ runtime_type: 'pipeline',
+}});
+const hosted = buildMarketplaceCardHtml({{
+ template_id: 'hosted', shelf: 'open', category: 'us_stocks',
+ name: 'Hosted Template', model_name: 'nvidia/nemotron-3-nano-30b-a3b',
+ runtime_type: 'ai_hedge_fund',
+}});
+console.log(JSON.stringify([ordinary, hosted].map((html) => ({{
hasModelBtn: html.includes('marketplace-clone-model-btn'),
hasModelMenu: html.includes('marketplace-model-menu'),
hasPrimaryBtn: html.includes('marketplace-clone-btn'),
@@ -608,13 +545,9 @@ def test_model_picker_gated_on_runtime_type_not_truthiness():
)
assert result.returncode == 0, result.stderr
ordinary, hosted = json.loads(result.stdout)
- # Ordinary template: both the picker button AND its menu render.
- assert ordinary["hasModelBtn"] is True
- assert ordinary["hasModelMenu"] is True
+ assert ordinary["hasModelBtn"] is False
+ assert ordinary["hasModelMenu"] is False
assert ordinary["hasPrimaryBtn"] is True
- # Hosted template: the primary "Add to My Agents" CTA still renders, but
- # neither the picker button nor its menu markup does -- a hidden button
- # with live menu markup is dead weight, not a fix.
assert hosted["hasModelBtn"] is False
assert hosted["hasModelMenu"] is False
assert hosted["hasPrimaryBtn"] is True
@@ -780,17 +713,12 @@ def test_duplicate_does_not_start_a_backtest():
assert forbidden not in body
-def test_entering_community_resets_the_vendor_filter():
- """A vendor left selected on one visit must not leak into the next.
-
- `marketplaceCategoryFilter` already resets here, under a comment explaining
- exactly this hazard. The vendor filter was added later and initially did not,
- so returning to Community via the nav tab stayed filtered -- and the My Agents
- empty-shelf deep link (which rides in with a category) then ANDed against the
- stale vendor and landed the user on an empty grid.
+def test_entering_community_resets_the_category_filter():
+ """A chip left selected on one visit must not leak into the next.
- Scoped to the `page === 'community'` branch on purpose: a reset anywhere else
- in the function would not fix the leak, so it must not satisfy this guard.
+ Scoped to the `page === 'community'` branch on purpose: a reset anywhere
+ else in the function would not fix the leak, so it must not satisfy this
+ guard. (The vendor filter that used to reset here left with the chip row.)
"""
body = fn_body("function navigateToPage")
start = body.index("page === 'community'")
@@ -798,11 +726,6 @@ def test_entering_community_resets_the_vendor_filter():
assert re.search(r"marketplaceCategoryFilter\s*=", branch), (
"the category reset vanished from the community branch"
)
- assert re.search(r"marketplaceVendorFilter\s*=\s*'all'", branch), (
- "entering Community must reset marketplaceVendorFilter to 'all'; "
- "without it the vendor chip leaks across visits and strands the "
- "empty-shelf deep links on an empty grid"
- )
@pytest.mark.skipif(shutil.which("node") is None, reason="node is not installed")
@@ -829,13 +752,3 @@ def test_duplicate_name_never_exceeds_the_backend_cap():
assert length <= 100, f"generated a {length}-char name; backend caps at 100"
assert ends_with_vendor, "the vendor suffix was trimmed instead of the base name"
-
-def test_model_picker_accessible_name_contains_its_visible_label():
- """WCAG 2.5.3 Label in Name (Level A): a voice-control user saying
- "click Choose model" must be able to activate the button."""
- grid = fn_body("function renderMarketplaceGrid")
- match = re.search(r'aria-label="([^"]*)"[^>]*>Choose model', grid)
- assert match, "the model-picker button lost its aria-label or its visible text"
- assert match.group(1).lower().startswith("choose model"), (
- f"accessible name {match.group(1)!r} does not contain the visible label 'Choose model'"
- )
diff --git a/dashboard/backend/tests/test_frontend_shelves.py b/dashboard/backend/tests/test_frontend_shelves.py
index cf0ca2f5..a8619e16 100644
--- a/dashboard/backend/tests/test_frontend_shelves.py
+++ b/dashboard/backend/tests/test_frontend_shelves.py
@@ -248,18 +248,34 @@ def test_my_agents_card_submeta_drops_duplicate_model_and_hosted_ai():
def test_render_marketplace_category_chips_is_built_from_the_shared_label_map():
- """The chip row is built from MARKET_LABELS rather than a second hardcoded
- list, plus an 'all' chip that isn't a category at all. It is no longer built
- from AGENT_SHELVES: Community filters templates by *market*, and
- Prompted Models holds both markets, so the shelf list and the chip
- list are different things -- built from AGENT_SHELVES this row would emit a
- single, meaningless "Prompted Models" chip that matches no template.
- """
- body = _strip_js_comments(fn_body("function renderMarketplaceCategoryChips()"))
- assert "MARKET_LABELS" in body
- assert "'all'" in body
+ """The chip row reads labels from MARKET_LABELS rather than a second
+ hardcoded list, plus an 'all' chip that isn't a category at all. Membership
+ is catalog-derived (marketplaceMarketChips) so an empty-market chip cannot
+ ship. It is no longer built from AGENT_SHELVES: Community filters templates
+ by *market*, and Prompted Models holds both markets, so the shelf list and
+ the chip list are different things -- built from AGENT_SHELVES this row
+ would emit a single, meaningless "Prompted Models" chip that matches no
+ template.
+ """
+ renderer = _strip_js_comments(fn_body("function renderMarketplaceCategoryChips()"))
+ helper = _strip_js_comments(fn_body("function marketplaceMarketChips("))
+ assert "marketplaceMarketChips(marketplaceTemplates)" in renderer
+ assert "MARKET_LABELS" in helper
+ assert "'all'" in helper
+ assert "present.has(key)" in helper
for label in ("U.S.", "China A-Share"):
- assert label not in body, f"{label!r} hardcoded instead of read from MARKET_LABELS"
+ assert label not in helper, f"{label!r} hardcoded instead of read from MARKET_LABELS"
+ assert label not in renderer, f"{label!r} hardcoded instead of read from MARKET_LABELS"
+
+
+def test_supermarket_second_shelf_is_titled_agents():
+ """Community's second row is Agents, not Open Agents. My Agents keeps its
+ own Open Agents title -- that surface is hosted runtimes only.
+ """
+ decl = js_const("MARKETPLACE_SHELVES")
+ assert "title: 'Agents'" in decl
+ assert "title: 'Open Agents'" not in decl
+ assert "Ready-made trading agents" in decl
def test_navigate_to_page_resets_chip_filter_on_plain_community_entry():
@@ -382,7 +398,7 @@ def test_copy_to_my_agents_cta_is_gone():
PR #253 made "Add to My Agents" canonical everywhere else, so this one
holdout ternary must go, not gain a permanent sibling.
"""
- body = _strip_js_comments(fn_body(_MARKETPLACE_RENDER_FN))
+ body = _strip_js_comments(fn_body("function buildMarketplaceCardHtml"))
assert "Copy to My Agents" not in body
@@ -392,7 +408,7 @@ def test_add_to_my_agents_cta_is_a_single_unconditional_string():
apart again tomorrow. Assert the direct, unconditional assignment and
that the now-dead branch variable is gone with it.
"""
- body = _strip_js_comments(fn_body(_MARKETPLACE_RENDER_FN))
+ body = _strip_js_comments(fn_body("function buildMarketplaceCardHtml"))
assert "cloneLabel = 'Add to My Agents'" in body
assert "isAiHedgeFundTemplate" not in body
@@ -405,7 +421,7 @@ def test_marketplace_submeta_never_renders_a_raw_category_or_model_slug():
'anthropic/'/'nvidia/' prefix strings elsewhere in the function, so a
whole-function check would false-positive on the table doing its job.
"""
- body = _strip_js_comments(fn_body(_MARKETPLACE_RENDER_FN))
+ body = _strip_js_comments(fn_body("function buildMarketplaceCardHtml"))
submeta_line = next(line for line in body.splitlines() if "agent-card-submeta" in line)
assert "template.category" not in submeta_line
assert "template.model_name" not in submeta_line
@@ -413,9 +429,12 @@ def test_marketplace_submeta_never_renders_a_raw_category_or_model_slug():
def test_fallback_description_copy_is_updated():
- body = _strip_js_comments(fn_body(_MARKETPLACE_RENDER_FN))
+ """LLM tiles stay description-free. Open Agents may show catalog copy.
+ Old fallback strings must stay gone."""
+ body = _strip_js_comments(fn_body("function buildMarketplaceCardHtml"))
assert "Open agent template." not in body
- assert "No description provided yet." in body
+ assert "No description provided yet." not in body
+ assert "marketplace-card-description" in body
def test_marketplace_category_chip_container_is_present_in_community_view():
@@ -458,7 +477,12 @@ def test_community_page_carries_the_no_real_money_sentence_once():
the brief says "once per page").
"""
community_html = _community_view_html()
- assert community_html.count(_CANONICAL_NO_REAL_MONEY_SENTENCE) == 1
+ shortened = (
+ "Performance is based on simulated tests and may vary between runs. "
+ "Live trading requires an explicitly connected brokerage account."
+ )
+ assert community_html.count(shortened) == 1
+ assert _CANONICAL_NO_REAL_MONEY_SENTENCE not in community_html
def _strip_js_comments_from(source: str) -> str:
diff --git a/dashboard/backend/tests/test_marketplace_catalog_models.py b/dashboard/backend/tests/test_marketplace_catalog_models.py
index 8e04a3cf..9baec802 100644
--- a/dashboard/backend/tests/test_marketplace_catalog_models.py
+++ b/dashboard/backend/tests/test_marketplace_catalog_models.py
@@ -21,32 +21,143 @@
_SUPPORTED_SLUGS = set(re.findall(r"slug:\s*'([^']+)'", js_const("SUPPORTED_MODELS")))
-_EXPECTED_NEW = {
- "contrarian-dip-buyer": ("openai/gpt-5.5", "us_stocks"),
- "sector-rotator": ("google/gemini-3.1-pro-preview", "us_stocks"),
- "volatility-guard": ("deepseek/deepseek-v4-pro", "us_stocks"),
- "ashare-momentum-t1": ("qwen/qwen3.7-plus", "cn_ashares"),
+# Nemotron is on the Competition Leaderboard via OpenRouter but is not in
+# SUPPORTED_MODELS (the user-facing picker). The supermarket still ships a
+# card for it so the catalog matches the board.
+_LEADERBOARD_ONLY_SLUGS = {"nvidia/nemotron-3-nano-30b-a3b"}
+
+_EXPECTED_MODELS = {
+ "claude-haiku-4-5": ("anthropic/claude-haiku-4-5", "us_stocks"),
+ "claude-sonnet-4-6": ("anthropic/claude-sonnet-4-6", "us_stocks"),
+ "gpt-5-5": ("openai/gpt-5.5", "us_stocks"),
+ "gemini-3-1-pro-preview": ("google/gemini-3.1-pro-preview", "us_stocks"),
+ "deepseek-v4-pro": ("deepseek/deepseek-v4-pro", "us_stocks"),
+ "qwen3-7-plus": ("qwen/qwen3.7-plus", "us_stocks"),
+ "nemotron-3-nano-30b": ("nvidia/nemotron-3-nano-30b-a3b", "us_stocks"),
}
+_LEADERBOARD = json.loads(
+ (Path(__file__).resolve().parents[3] / "dashboard/config/leaderboard.json").read_text(
+ encoding="utf-8"
+ )
+)
+
@pytest.mark.parametrize("template", _CATALOG, ids=lambda t: t["template_id"])
def test_every_template_runs_a_supported_or_hosted_model(template):
if template.get("runtime_type"):
return # hosted runtime: its model is not user-selectable
- assert template["model_name"] in _SUPPORTED_SLUGS, (
+ assert template["model_name"] in (_SUPPORTED_SLUGS | _LEADERBOARD_ONLY_SLUGS), (
f"{template['template_id']} runs {template['model_name']!r}, "
- "which is not in SUPPORTED_MODELS"
+ "which is not in SUPPORTED_MODELS or the leaderboard"
)
-@pytest.mark.parametrize("template_id,expected", sorted(_EXPECTED_NEW.items()))
-def test_new_templates_are_present_with_their_pairings(template_id, expected):
+@pytest.mark.parametrize("template_id,expected", sorted(_EXPECTED_MODELS.items()))
+def test_leaderboard_model_cards_are_present_with_their_pairings(template_id, expected):
found = next((t for t in _CATALOG if t["template_id"] == template_id), None)
assert found is not None, f"{template_id} missing from marketplace.json"
assert (found["model_name"], found["category"]) == expected
+def test_every_leaderboard_llm_has_a_supermarket_card():
+ """The supermarket model cards are the board's llm_agent roster, by name.
+
+ A new competition model that ships on the board without a card (or a card
+ that outlives its board entry) is otherwise invisible until someone notices.
+ AI Hedge Fund is a hosted runtime, not a board entry, and is excluded.
+ """
+ board_names = {
+ entry["name"]
+ for entry in _LEADERBOARD["strategies"]
+ if entry.get("strategy") == "llm_agent"
+ }
+ catalog_names = {
+ template["name"]
+ for template in _CATALOG
+ if template.get("shelf") == "llms"
+ }
+ assert catalog_names == board_names
+ # The API payload exposes each entry's *model* string (service.py builds
+ # entries with "model", never "name"), and app.js joins cards to entries on
+ # it -- pin the join key itself, not just the config's display name.
+ board_models = {
+ entry["model"]
+ for entry in _LEADERBOARD["strategies"]
+ if entry.get("strategy") == "llm_agent"
+ }
+ assert catalog_names == board_models
+
+
+def test_llm_template_ids_map_to_leaderboard_entry_ids():
+ """app.js's fallback join is template_id with '-' -> '_' against entry_id.
+
+ The primary join is the name/model string; this fallback exists for the day
+ those drift, so it must actually match -- 'gemini-3-1-pro' silently missed
+ 'gemini_3_1_pro_preview' until the template_id was renamed.
+ """
+ board_ids = {
+ entry["id"]
+ for entry in _LEADERBOARD["strategies"]
+ if entry.get("strategy") == "llm_agent"
+ }
+ catalog_ids = {
+ template["template_id"].replace("-", "_")
+ for template in _CATALOG
+ if template.get("shelf") == "llms"
+ }
+ assert catalog_ids == board_ids
+
+
+def test_llm_card_prompts_pin_the_default_starter_instruction():
+ """The 7 LLM cards ship the starter instruction verbatim -- a third copy of
+ DEFAULT_STARTER_INSTRUCTION (defaults.py already mirrors it to app.js under
+ a pin test). Pin this copy too, so tuning the constant cannot silently
+ strand the cards on stale wording."""
+ from dashboard.backend.domain.agents.defaults import DEFAULT_STARTER_INSTRUCTION
+
+ for template in _CATALOG:
+ if template.get("shelf") != "llms":
+ continue
+ [step] = template["pipeline"]
+ assert step["prompt"] == DEFAULT_STARTER_INSTRUCTION, template["template_id"]
+
+
+def test_restored_strategy_templates_sit_on_the_agents_shelf():
+ restored = {
+ "balanced-starter",
+ "momentum-scout",
+ "pipeline-analyst",
+ "blue-chip-steady",
+ "even-split-dow",
+ "ashare-steady-t1",
+ "contrarian-dip-buyer",
+ "sector-rotator",
+ "volatility-guard",
+ "ashare-momentum-t1",
+ }
+ by_id = {template["template_id"]: template for template in _CATALOG}
+ assert restored <= set(by_id)
+ for template_id in restored:
+ assert by_id[template_id].get("shelf") == "open", template_id
+
+
+def test_catalog_rows_declare_a_supermarket_shelf():
+ shelves = {template["template_id"]: template.get("shelf") for template in _CATALOG}
+ assert shelves["ai-hedge-fund"] == "open"
+ for template_id in _EXPECTED_MODELS:
+ assert shelves[template_id] == "llms", template_id
+
+
def test_catalog_covers_every_pickable_vendor():
"""The facet is decorative if most of its chips are empty."""
vendors = {t["model_name"].split("/", 1)[0] for t in _CATALOG}
assert {"anthropic", "openai", "google", "deepseek", "qwen"} <= vendors
+
+
+def test_catalog_includes_both_markets():
+ """Community chips follow the catalog. A-share templates are back on the
+ Agents shelf, so the China A-Share chip ships again without a hardcoded
+ chip list.
+ """
+ assert {t.get("category") for t in _CATALOG} == {"us_stocks", "cn_ashares"}
diff --git a/dashboard/config/marketplace.json b/dashboard/config/marketplace.json
index 11e207b0..be95d9cb 100644
--- a/dashboard/config/marketplace.json
+++ b/dashboard/config/marketplace.json
@@ -1,12 +1,195 @@
{
"templates": [
+ {
+ "template_id": "claude-haiku-4-5",
+ "shelf": "llms",
+ "name": "Claude Haiku 4.5",
+ "model_name": "anthropic/claude-haiku-4-5",
+ "description": "The Competition Leaderboard's Claude Haiku 4.5 agent. Same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_claude_haiku",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "claude-sonnet-4-6",
+ "shelf": "llms",
+ "name": "Claude Sonnet 4.6",
+ "model_name": "anthropic/claude-sonnet-4-6",
+ "description": "The Competition Leaderboard's Claude Sonnet 4.6 agent. Same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_claude_sonnet",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "deepseek-v4-pro",
+ "shelf": "llms",
+ "name": "DeepSeek V4 Pro",
+ "model_name": "deepseek/deepseek-v4-pro",
+ "description": "The Competition Leaderboard's DeepSeek V4 Pro agent — the only model that beat the passive baselines in the April 2026 contest window. Same long-only DJIA hourly backtest; add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_deepseek_v4",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "gpt-5-5",
+ "shelf": "llms",
+ "name": "GPT-5.5",
+ "model_name": "openai/gpt-5.5",
+ "description": "The Competition Leaderboard's GPT-5.5 agent. Same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_gpt_5_5",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "gemini-3-1-pro-preview",
+ "shelf": "llms",
+ "name": "Gemini 3.1 Pro Preview",
+ "model_name": "google/gemini-3.1-pro-preview",
+ "description": "The Competition Leaderboard's Gemini 3.1 Pro Preview agent. Same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_gemini_pro",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "nemotron-3-nano-30b",
+ "shelf": "llms",
+ "name": "Nemotron 3 Nano 30B",
+ "model_name": "nvidia/nemotron-3-nano-30b-a3b",
+ "description": "The Competition Leaderboard's NVIDIA Nemotron 3 Nano 30B agent. An open-weight model on the same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_nemotron_nano",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "qwen3-7-plus",
+ "shelf": "llms",
+ "name": "Qwen3.7 Plus",
+ "model_name": "qwen/qwen3.7-plus",
+ "description": "The Competition Leaderboard's Qwen3.7 Plus agent. Same long-only DJIA hourly backtest as the rest of the board — add it and edit the instruction to try this model yourself.",
+ "category": "us_stocks",
+ "tags": [
+ "competition model",
+ "DJIA"
+ ],
+ "author": "Agentic Trading Lab",
+ "pipeline": [
+ {
+ "id": "sub_qwen3_7",
+ "presetKey": "simple_instruction",
+ "label": "Trading instruction",
+ "prompt": "Spread the money across a few of the strongest available stocks. Buy on meaningful dips, take profits after strong run-ups, and never put everything into one stock.",
+ "outputFormat": "JSON: { \"orders\": [{ \"symbol\": \"...\", \"side\": \"buy|sell|hold\", \"qty\": number, \"order_type\": \"market|limit\", \"limit_price\": number|null, \"reason\": \"...\" }] }"
+ }
+ ]
+ },
+ {
+ "template_id": "ai-hedge-fund",
+ "shelf": "open",
+ "card_subtitle": "Open-source multi-agent system",
+ "name": "AI Hedge Fund",
+ "model_name": "nvidia/nemotron-3-nano-30b-a3b",
+ "description": "A team of AI investors that analyzes the market, develops trading ideas, and tests them through backtesting.",
+ "category": "us_stocks",
+ "tags": [
+ "analyst team",
+ "fundamentals",
+ "official template"
+ ],
+ "author": "virattt / Agentic Trading Lab",
+ "repo_url": "https://github.com/virattt/ai-hedge-fund",
+ "runtime_type": "ai_hedge_fund",
+ "runtime_config": {
+ "analysts": [
+ "fundamentals_analyst",
+ "technical_analyst",
+ "sentiment_analyst",
+ "valuation_analyst"
+ ]
+ }
+ },
{
"template_id": "balanced-starter",
+ "shelf": "open",
"name": "Balanced Starter",
"model_name": "anthropic/claude-haiku-4-5",
"description": "A simple starter agent that diversifies across strong stocks, buys dips, and takes profits after run-ups.",
"category": "us_stocks",
- "tags": ["starter", "diversified"],
+ "tags": [
+ "starter",
+ "diversified"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -20,11 +203,15 @@
},
{
"template_id": "momentum-scout",
+ "shelf": "open",
"name": "Momentum Scout",
"model_name": "anthropic/claude-haiku-4-5",
"description": "Focus on recent price strength and volume. Favor leaders with positive momentum and trim laggards quickly.",
"category": "us_stocks",
- "tags": ["momentum", "trend"],
+ "tags": [
+ "momentum",
+ "trend"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -38,11 +225,15 @@
},
{
"template_id": "pipeline-analyst",
+ "shelf": "open",
"name": "Three-Step Analyst",
"model_name": "anthropic/claude-sonnet-4-6",
"description": "A three-step strategy: gather market facts, convert them into signals, then produce executable orders.",
"category": "us_stocks",
- "tags": ["multi-step strategy", "official template"],
+ "tags": [
+ "multi-step strategy",
+ "official template"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -68,32 +259,17 @@
}
]
},
- {
- "template_id": "ai-hedge-fund",
- "name": "AI Hedge Fund",
- "model_name": "nvidia/nemotron-3-nano-30b-a3b",
- "description": "A hosted panel of AI analysts that weigh in on every trade, run through Agentic Trading Lab's long-only backtest engine. Based on the open-source AI Hedge Fund project by virattt.",
- "category": "us_stocks",
- "tags": ["analyst team", "fundamentals", "official template"],
- "author": "virattt / Agentic Trading Lab",
- "repo_url": "https://github.com/virattt/ai-hedge-fund",
- "runtime_type": "ai_hedge_fund",
- "runtime_config": {
- "analysts": [
- "fundamentals_analyst",
- "technical_analyst",
- "sentiment_analyst",
- "valuation_analyst"
- ]
- }
- },
{
"template_id": "blue-chip-steady",
+ "shelf": "open",
"name": "Blue-Chip Steady",
"model_name": "anthropic/claude-haiku-4-5",
"description": "Buy and hold a handful of the strongest Dow companies, selling only when a position deteriorates badly. Mirrors the buy-and-hold benchmark on our leaderboard.",
"category": "us_stocks",
- "tags": ["buy and hold", "blue chips"],
+ "tags": [
+ "buy and hold",
+ "blue chips"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -107,11 +283,15 @@
},
{
"template_id": "even-split-dow",
+ "shelf": "open",
"name": "Even-Split Dow",
"model_name": "anthropic/claude-haiku-4-5",
"description": "Spread the money evenly across all available Dow stocks and keep the split even. Mirrors the equal-weight benchmark on our leaderboard.",
"category": "us_stocks",
- "tags": ["equal weight", "diversified"],
+ "tags": [
+ "equal weight",
+ "diversified"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -125,11 +305,15 @@
},
{
"template_id": "ashare-steady-t1",
+ "shelf": "open",
"name": "A-Share Steady (T+1)",
"model_name": "anthropic/claude-haiku-4-5",
"description": "A patient strategy for Chinese A-shares, built for that market's rule that shares bought today cannot be sold until the next trading day.",
"category": "cn_ashares",
- "tags": ["a-shares", "patient"],
+ "tags": [
+ "a-shares",
+ "patient"
+ ],
"author": "Agentic Trading Lab",
"pipeline": [
{
@@ -143,6 +327,7 @@
},
{
"template_id": "contrarian-dip-buyer",
+ "shelf": "open",
"name": "Contrarian Dip Buyer",
"model_name": "openai/gpt-5.5",
"description": "Buys stocks that have sold off hard and trims them back once they have recovered. The opposite instinct to a momentum strategy.",
@@ -164,6 +349,7 @@
},
{
"template_id": "sector-rotator",
+ "shelf": "open",
"name": "Sector Rotator",
"model_name": "google/gemini-3.1-pro-preview",
"description": "Concentrates into whichever part of the market is leading, and moves on when leadership changes.",
@@ -185,6 +371,7 @@
},
{
"template_id": "volatility-guard",
+ "shelf": "open",
"name": "Volatility Guard",
"model_name": "deepseek/deepseek-v4-pro",
"description": "Holds a steady portfolio in calm markets and cuts exposure when prices start swinging. Runs on the only model that has beaten the passive baselines on our leaderboard.",
@@ -206,6 +393,7 @@
},
{
"template_id": "ashare-momentum-t1",
+ "shelf": "open",
"name": "A-Share Momentum (T+1)",
"model_name": "qwen/qwen3.7-plus",
"description": "Rides the strongest Chinese A-shares while respecting that market's rule that shares bought today cannot be sold until the next trading day.",
diff --git a/dashboard/frontend/app.html b/dashboard/frontend/app.html
index a87a5218..4e04517a 100644
--- a/dashboard/frontend/app.html
+++ b/dashboard/frontend/app.html
@@ -13,7 +13,7 @@
because every API call is a CORS request. -->
-
+
@@ -1740,17 +1740,16 @@
Community
Agent Supermarket
-
Browse open agent templates and add them to My Agents to customize and backtest.
+
Discover models and open agents tested by the Agentic Trading Lab community. Add one to your workspace and test it yourself.
-
Every test here uses simulated money. Real money is involved only if you explicitly connect a brokerage account and turn on live trading.
+
Performance is based on simulated tests and may vary between runs. Live trading requires an explicitly connected brokerage account.
-
-
+
Couldn't load the marketplace. Check your connection and try again.
@@ -2491,7 +2490,7 @@
Refund Credits purchase
-
+
diff --git a/dashboard/frontend/app.js b/dashboard/frontend/app.js
index b485dc80..e6ee3b9a 100644
--- a/dashboard/frontend/app.js
+++ b/dashboard/frontend/app.js
@@ -505,8 +505,9 @@ const LEGACY_RUNTIME_MARKET = { ai_hedge_fund: 'us_stocks' };
/** Category slug -> market display name. The single place these strings are
* written: the Prompted Models shelf's market chips, the Community category
- * chips, the agent-card submeta and the Configure picker all read this map, so
- * renaming a market is one edit. Key order is chip order and mirrors the
+ * chips (labels only -- that row also filters to markets present in the
+ * catalog), the agent-card submeta and the Configure picker all read this map,
+ * so renaming a market is one edit. Key order is chip order and mirrors the
* AgentCategory Literal's declaration order in
* dashboard/backend/domain/agents/taxonomy.py.
*
@@ -2191,41 +2192,51 @@ async function loadAgentsNow() {
let marketplaceTemplates = [];
let marketplaceCloneInFlight = false;
let marketplaceLoadInFlight = null;
+/** null = contest board not fetched yet, or the last fetch failed (the next
+ * Community visit retries). [] = fetched and genuinely empty — never
+ * invent ranks. */
+let marketplaceLeaderboardEntries = null;
+let marketplaceLeaderboardLoadInFlight = null;
+/** Window / capital / field count from the same contest payload. */
+let marketplaceContestMeta = {
+ start_date: null,
+ end_date: null,
+ display_capital: null,
+ total_entries: null,
+};
+
+/** Community supermarket rows. Map-rendered; adding a shelf is a new entry
+ * here plus ``shelf`` on the catalog row, not a one-off card layout. */
+const MARKETPLACE_SHELVES = [
+ { key: 'llms', title: 'LLMs', sub: 'LLMs tested on the ATL leaderboard' },
+ { key: 'open', title: 'Agents', sub: 'Ready-made trading agents' },
+];
/** 'all' or one of MARKET_LABELS' keys. Set by the chip row and by the Prompted
* Models shelf's empty-state Community button (via navigateToPage's options). */
let marketplaceCategoryFilter = 'all';
-/** 'all' or one of MODEL_VENDORS' keys. ANDs with marketplaceCategoryFilter. */
-let marketplaceVendorFilter = 'all';
/** The model-vendor axis: who makes a model, and how it is licensed.
*
- * Promoted from a submeta label lookup into the source of truth for the whole
- * axis -- Community's vendor chips, the open-source badge and the card submeta
- * all derive from this one table, so a badge cannot drift from the vendor it
- * describes. A wrong badge is a factual claim about someone else's product.
+ * Single source of truth for vendor identity: `company` feeds the LLM tile
+ * submeta (formatModelCompanyLabel), `key`/`label`/`licence` are pinned by the
+ * facet tests. Nothing renders `licence` since the open-source badge retired
+ * with the vendor chip row (PR #427); it stays as vendor metadata because a
+ * wrong entry is a factual claim about someone else's product.
*
* Matched by PREFIX, not exact slug, so a new model version under a known
- * vendor needs no entry here. Declaration order is chip order, mirroring how
- * MARKET_LABELS' key order mirrors the AgentCategory Literal.
- *
- * All eight are listed even though only six are pickable: a card whose model
- * matches nothing renders as the generic "AI-powered" with no chip and no
- * badge, which is invisible until someone notices. The chip ROW is still
- * derived from what the loaded catalog actually contains (see
- * renderMarketplaceVendorChips), so listing a vendor here never ships an
- * empty chip. */
+ * vendor needs no entry here. */
const MODEL_VENDORS = [
- { key: 'anthropic', prefix: 'anthropic/', label: 'Claude', licence: 'closed' },
- { key: 'openai', prefix: 'openai/', label: 'GPT', licence: 'closed' },
- { key: 'google', prefix: 'google/', label: 'Gemini', licence: 'closed' },
- { key: 'deepseek', prefix: 'deepseek/', label: 'DeepSeek', licence: 'open' },
- { key: 'qwen', prefix: 'qwen/', label: 'Qwen', licence: 'open' },
- // "NVIDIA Nemotron", not "Nemotron": this label also feeds
- // formatModelProviderLabel, whose shipped output must not change.
- { key: 'nvidia', prefix: 'nvidia/nemotron', label: 'NVIDIA Nemotron', licence: 'open' },
- { key: 'meta', prefix: 'meta-llama/', label: 'Llama', licence: 'open' },
- { key: 'xai', prefix: 'x-ai/', label: 'Grok', licence: 'closed' },
+ { key: 'anthropic', prefix: 'anthropic/', label: 'Claude', licence: 'closed', company: 'Anthropic' },
+ { key: 'openai', prefix: 'openai/', label: 'GPT', licence: 'closed', company: 'OpenAI' },
+ { key: 'google', prefix: 'google/', label: 'Gemini', licence: 'closed', company: 'Google' },
+ { key: 'deepseek', prefix: 'deepseek/', label: 'DeepSeek', licence: 'open', company: 'DeepSeek' },
+ { key: 'qwen', prefix: 'qwen/', label: 'Qwen', licence: 'open', company: 'Alibaba' },
+ // "NVIDIA Nemotron", not "Nemotron": EXPECTED_VENDORS pins this label,
+ // and `company` carries the tile-subtitle name.
+ { key: 'nvidia', prefix: 'nvidia/nemotron', label: 'NVIDIA Nemotron', licence: 'open', company: 'NVIDIA' },
+ { key: 'meta', prefix: 'meta-llama/', label: 'Llama', licence: 'open', company: 'Meta' },
+ { key: 'xai', prefix: 'x-ai/', label: 'Grok', licence: 'closed', company: 'xAI' },
];
/** Vendor key for a model slug, or '' when the platform genuinely doesn't know.
@@ -2250,10 +2261,11 @@ function modelVendorLicence(modelName) {
return (MODEL_VENDORS.find((vendor) => vendor.key === key) || {}).licence || '';
}
-function formatModelProviderLabel(modelName) {
+/** Company name for a tile subtitle (Anthropic, NVIDIA). */
+function formatModelCompanyLabel(modelName) {
const key = modelVendorKey(modelName);
const vendor = MODEL_VENDORS.find((entry) => entry.key === key);
- return vendor ? `Powered by ${vendor.label}` : 'AI-powered';
+ return vendor ? (vendor.company || vendor.label) : '';
}
/** Select a Community category chip and re-render, without a route or API
@@ -2271,27 +2283,33 @@ function setMarketplaceCategoryFilter(category) {
renderMarketplaceGrid();
}
-/** Select a vendor chip and re-render. Mirrors setMarketplaceCategoryFilter,
- * including the reset-to-'all' fallback for an unrecognized key. */
-function setMarketplaceVendorFilter(vendorKey) {
- marketplaceVendorFilter = MODEL_VENDORS.some((vendor) => vendor.key === vendorKey)
- ? vendorKey
- : 'all';
- renderMarketplaceGrid();
+/** Chip keys/labels for the Community market row. Pure so the catalog-filter
+ * contract can run under node: 'All' plus one chip per MARKET_LABELS key that
+ * the loaded catalog actually contains. Shipping every enum key put a China
+ * A-Share chip on a 100% us_stocks catalog -- a permanent empty state. Adding
+ * an A-share template brings the chip back; do not hardcode it. Order still
+ * comes from MARKET_LABELS so the row does not reshuffle with catalog order. */
+function marketplaceMarketChips(templates) {
+ const present = new Set(
+ (templates || []).map((t) => String(t.category || '').toLowerCase()),
+ );
+ return [
+ { key: 'all', label: 'All' },
+ ...Object.entries(MARKET_LABELS)
+ .filter(([key]) => present.has(key))
+ .map(([key, label]) => ({ key, label })),
+ ];
}
-/** Chip row above the marketplace grid: 'All' plus one chip per market, built
- * from MARKET_LABELS rather than a second hardcoded list. Built from the label
- * map rather than AGENT_SHELVES because Community filters templates by
- * *market*, and Prompted Models holds both markets -- the shelf
- * list and the chip list are different things. */
+/** Chip row above the marketplace grid. Labels come from MARKET_LABELS;
+ * membership comes from the loaded catalog (see marketplaceMarketChips).
+ * Built from the label map rather than AGENT_SHELVES because Community
+ * filters templates by *market*, and Prompted Models holds both markets --
+ * the shelf list and the chip list are different things. */
function renderMarketplaceCategoryChips() {
const container = document.getElementById('marketplaceCategoryChips');
if (!container) return;
- const chips = [
- { key: 'all', label: 'All' },
- ...Object.entries(MARKET_LABELS).map(([key, label]) => ({ key, label })),
- ];
+ const chips = marketplaceMarketChips(marketplaceTemplates);
// Build once, then only toggle state. This runs from renderMarketplaceGrid,
// which is bound to the search box's `input` event -- rebuilding innerHTML
// per keystroke would blow away the focused chip on every character typed.
@@ -2308,68 +2326,32 @@ function renderMarketplaceCategoryChips() {
});
}
-/** Second chip row: 'All' plus one chip per vendor PRESENT IN THE CATALOG.
- *
- * Deliberately asymmetric with the market row, which is hardcoded from
- * MARKET_LABELS: markets are a closed, backend-validated enum, vendors are
- * open-ended. Hardcoding all of MODEL_VENDORS would ship chips that can never
- * match anything. Order still comes from MODEL_VENDORS, not from catalog order,
- * so the row does not reshuffle when a template is added. */
-function renderMarketplaceVendorChips() {
- const container = document.getElementById('marketplaceVendorChips');
- if (!container) return;
- const present = new Set(marketplaceTemplates.map((t) => modelVendorKey(t.model_name)));
- const chips = [
- { key: 'all', label: 'All models' },
- ...MODEL_VENDORS.filter((vendor) => present.has(vendor.key)).map((vendor) => ({
- key: vendor.key,
- label: vendor.label,
- })),
- ];
- // Build once, then only toggle state -- same reason as the market row: this
- // runs from renderMarketplaceGrid, which is bound to the search box's `input`.
- const existing = container.querySelectorAll('[data-marketplace-vendor]');
- if (existing.length !== chips.length) {
- container.innerHTML = chips
- .map((chip) => ``)
- .join('');
- }
- container.querySelectorAll('[data-marketplace-vendor]').forEach((button) => {
- const active = button.dataset.marketplaceVendor === marketplaceVendorFilter;
- button.classList.toggle('active', active);
- button.setAttribute('aria-pressed', String(active));
- });
-}
-
-/** Empty-state copy. Three cases, deliberately worded apart -- the same concern
- * promptedEmptyHtml records for My Agents.
- *
- * A typed query wins over the facet case: when a search is what emptied the
- * grid, offering "Clear filters" sends the user to fix the wrong thing. */
-function marketplaceEmptyHtml({ searching, categoryFilter, vendorFilter }) {
+/** Empty-state copy. Two cases, deliberately worded apart -- the same concern
+ * promptedEmptyHtml records for My Agents. A typed query wins over the market
+ * chip: when a search is what emptied the grid, "No X templates yet" would
+ * send the user to fix the wrong thing. */
+function marketplaceEmptyHtml({ searching, categoryFilter }) {
if (searching) return 'No templates match your search.';
- if (categoryFilter !== 'all' && vendorFilter !== 'all') {
- return `No templates match both filters. `;
- }
if (categoryFilter !== 'all') {
return `No ${escapeHtml(MARKET_LABELS[categoryFilter] || '')} templates yet.`;
}
- if (vendorFilter !== 'all') {
- const vendor = MODEL_VENDORS.find((entry) => entry.key === vendorFilter);
- return `No ${escapeHtml(vendor?.label || '')} templates yet.`;
- }
return 'No templates match your search.';
}
+function templateMarketplaceShelf(template) {
+ const explicit = String(template?.shelf || '').toLowerCase();
+ if (explicit === 'llms' || explicit === 'open') return explicit;
+ // Mirrors the backend's _normalize_shelf fallback: any hosted runtime is an
+ // open agent, with no per-runtime special case to remember.
+ return template?.mode === 'runtime' ? 'open' : 'llms';
+}
+
function getFilteredMarketplaceTemplates() {
const query = (document.getElementById('marketplaceSearchInput')?.value || '').trim().toLowerCase();
let list = marketplaceTemplates.slice();
if (marketplaceCategoryFilter !== 'all') {
list = list.filter((template) => String(template.category || '').toLowerCase() === marketplaceCategoryFilter);
}
- if (marketplaceVendorFilter !== 'all') {
- list = list.filter((template) => modelVendorKey(template.model_name) === marketplaceVendorFilter);
- }
if (query) {
list = list.filter((template) => {
const haystack = [
@@ -2377,6 +2359,7 @@ function getFilteredMarketplaceTemplates() {
template.description,
template.category,
template.author,
+ template.card_subtitle,
...(template.tags || []),
template.model_name,
]
@@ -2389,6 +2372,324 @@ function getFilteredMarketplaceTemplates() {
return list;
}
+function findMarketplaceLeaderboardEntry(template) {
+ const entries = marketplaceLeaderboardEntries;
+ if (!Array.isArray(entries)) return null;
+ const name = String(template?.name || '').trim().toLowerCase();
+ const id = String(template?.template_id || '').replace(/-/g, '_');
+ return entries.find((entry) => {
+ if (!entry || !entry.is_model) return false;
+ if (name && String(entry.model || '').trim().toLowerCase() === name) return true;
+ return Boolean(id) && String(entry.entry_id || '') === id;
+ }) || null;
+}
+
+/** Contest-board stats for a supermarket card.
+ *
+ * Wired to GET /api/v1/leaderboard?period=contest (same payload as the
+ * Competition Leaderboard, window 2026-04-15 → 2026-05-15). That payload
+ * has a single-window ``cumulative_return``, official ``rank``, and
+ * hourly ``equity_curve``. Do not invent values.
+ */
+const MARKETPLACE_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+function applyMarketplaceLeaderboardPayload(data) {
+ const entries = Array.isArray(data?.entries) ? data.entries : [];
+ marketplaceLeaderboardEntries = entries;
+ marketplaceContestMeta = {
+ start_date: data?.window?.start_date || null,
+ end_date: data?.window?.end_date || null,
+ display_capital: data?.display_capital ?? null,
+ total_entries: Number(data?.total_entries) || entries.length || null,
+ };
+}
+
+function marketplaceBenchmarkEntry() {
+ const entries = marketplaceLeaderboardEntries;
+ if (!Array.isArray(entries)) return null;
+ return entries.find((entry) => (
+ entry?.entry_id === 'djia_index' || String(entry?.model || '').toUpperCase() === 'DJIA'
+ )) || null;
+}
+
+function downsampleMarketplaceCurve(curve, maxPoints = 48) {
+ if (!Array.isArray(curve) || curve.length <= maxPoints) return curve || [];
+ const out = [];
+ const last = curve.length - 1;
+ for (let i = 0; i < maxPoints; i += 1) {
+ out.push(curve[Math.round((i / (maxPoints - 1)) * last)]);
+ }
+ return out;
+}
+
+function marketplaceIndexedPctSeries(curve) {
+ // Cumulative return from the first equity point, in percent (0 = start).
+ if (!Array.isArray(curve) || curve.length < 2) return null;
+ const points = [];
+ for (const point of curve) {
+ const equity = Number(point?.equity);
+ if (!Number.isFinite(equity)) continue;
+ points.push({ t: point.timestamp, equity });
+ }
+ if (points.length < 2) return null;
+ const initial = points[0].equity;
+ if (!initial) return null;
+ return points.map((point) => ({ t: point.t, pct: ((point.equity / initial) - 1) * 100 }));
+}
+
+function formatMarketplaceMd(iso) {
+ const match = String(iso || '').match(/^(\d{4})-(\d{2})-(\d{2})/);
+ if (!match) return '';
+ return `${MARKETPLACE_MONTHS[Number(match[2]) - 1]} ${Number(match[3])}`;
+}
+
+function formatMarketplaceWindowRange(start, end) {
+ const from = formatMarketplaceMd(start);
+ const to = formatMarketplaceMd(end);
+ if (from && to) return `${from}–${to}`;
+ return from || to || '';
+}
+
+function formatMarketplaceCapital(value) {
+ const n = Number(value);
+ if (!Number.isFinite(n)) return '';
+ if (n >= 1000 && n % 1000 === 0) return `$${(n / 1000).toFixed(0)}K`;
+ return `$${Math.round(n).toLocaleString('en-US')}`;
+}
+
+function marketplaceNicePctTicks(min, max) {
+ const lo = Math.min(0, Math.floor(min / 5) * 5);
+ const hi = Math.max(0, Math.ceil(max / 5) * 5);
+ const ticks = [];
+ for (let v = lo; v <= hi; v += 5) ticks.push(v);
+ if (ticks.length < 2) ticks.push(lo + 5);
+ return ticks;
+}
+
+function marketplaceLinePath(series, xOf, yOf) {
+ return series.map((point, i) => {
+ const cmd = i === 0 ? 'M' : 'L';
+ return `${cmd}${xOf(i).toFixed(1)},${yOf(point.pct).toFixed(1)}`;
+ }).join(' ');
+}
+
+/** Agent vs DJIA comparison chart from real contest equity_curve points. */
+function buildMarketplaceCompareChartHtml(agentCurve, benchmarkCurve, { positive = true, modelName = 'Model' } = {}) {
+ const agent = marketplaceIndexedPctSeries(downsampleMarketplaceCurve(agentCurve));
+ if (!agent) return '';
+ const bench = marketplaceIndexedPctSeries(downsampleMarketplaceCurve(benchmarkCurve));
+ const agentColor = positive ? '#4ade80' : '#f87171';
+ const benchColor = '#94a3b8';
+ const pcts = agent.map((p) => p.pct).concat(bench ? bench.map((p) => p.pct) : []);
+ const ticks = marketplaceNicePctTicks(Math.min(...pcts), Math.max(...pcts));
+ const yMin = ticks[0];
+ const yMax = ticks[ticks.length - 1];
+ const yRange = yMax - yMin || 1;
+ const w = 220;
+ const left = 32;
+ const right = 6;
+ const top = 6;
+ const plotBottom = 78;
+ const plotW = w - left - right;
+ const plotH = plotBottom - top;
+ const n = agent.length;
+ // Each series is scaled by ITS OWN length: the two curves are downsampled
+ // independently, and plotting the benchmark against the agent's point count
+ // draws it past the plot edge (or squashed) whenever the lengths differ.
+ const xAt = (i, len) => left + (len <= 1 ? 0 : (i / (len - 1)) * plotW);
+ const xOf = (i) => xAt(i, n);
+ const yOf = (pct) => top + (1 - (pct - yMin) / yRange) * plotH;
+ const xTicks = [0, Math.round((n - 1) / 2), n - 1].filter((v, i, arr) => arr.indexOf(v) === i);
+ const yLines = ticks.map((tick) => {
+ const y = yOf(tick);
+ return `
+ ${tick}%`;
+ }).join('');
+ const xLabels = xTicks.map((i) => {
+ const raw = String(agent[i]?.t || '');
+ const label = formatMarketplaceMd(raw.slice(0, 10));
+ if (!label) return '';
+ return `${escapeHtml(label)}`;
+ }).join('');
+ const agentPath = marketplaceLinePath(agent, xOf, yOf);
+ const benchPath = bench && bench.length >= 2
+ ? marketplaceLinePath(bench, (i) => xAt(i, bench.length), yOf)
+ : '';
+ return `
+