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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:

required_modules = {
"nano/library/__init__.py",
"nano/library/catalog.py",
"nano/library/contribution.py",
}
missing_modules = sorted(required_modules - packaged)
Expand Down Expand Up @@ -101,8 +102,13 @@ jobs:
"from nano.library.contribution import "
"baseline_control_frames, module_control_frame, "
"source_provenance_issues; "
"from nano.library.catalog import "
"catalog_diagnostics, load_catalog; "
"assert baseline_control_frames.__module__ == "
"'nano.library.contribution'"
"'nano.library.contribution'; "
"catalog = load_catalog(); "
"assert catalog['strategyCount'] == 53; "
"assert catalog_diagnostics() == ()"
),
],
cwd=outside_checkout,
Expand Down
2 changes: 1 addition & 1 deletion nano/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
artifact cannot change behavior because a transitive dependency did.
"""

__version__ = "1.0.4"
__version__ = "1.0.5"
125 changes: 125 additions & 0 deletions nano/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
from ..data import FeedError, load_frame, parse_date
from ..indicators.registry import INDICATORS, names as indicator_names
from ..ir.schema import SUPPORTED_IR_VERSIONS, IRValidationError
from ..library.catalog import (
CatalogValidationError,
catalog_diagnostics,
load_catalog,
searchable_text,
strategy_rows,
)
from ..runtime.interpreter import RuntimeError_
from ..runtime.receipt import ReceiptError, build_receipt, canonical_bytes, differences
from ..runtime.risk import RiskGate
Expand Down Expand Up @@ -524,6 +531,123 @@ def command_indicators(args: Any, console: Console) -> int:
return EXIT_OK


# ---------------------------------------------------------------------------
# nano library
# ---------------------------------------------------------------------------


def _library_document(console: Console) -> Optional[dict[str, Any]]:
try:
return load_catalog()
except CatalogValidationError as error:
for diagnostic in error.diagnostics:
console.warn(f"error: {diagnostic.render()}")
return None


def _print_library_rows(rows: Sequence[dict[str, Any]], console: Console) -> None:
console.say("ID\tIR\tHOST SIGNALS")
for row in rows:
signals = ",".join(row.get("requiredHostSignals", []))
console.say(f"{row['id']}\t{row['irVersion']}\t{signals}")


def command_library(args: Any, console: Console) -> int:
"""Browse or verify the generated strategy-library catalog."""

action = args.library_action
if action == "check":
diagnostics = catalog_diagnostics()
if diagnostics:
for diagnostic in diagnostics:
console.warn(f"error: {diagnostic.render()}")
return EXIT_DIAGNOSTICS
document = _library_document(console)
if document is None:
return EXIT_DIAGNOSTICS
counts = document["irMaturityCounts"]
console.say(
f"catalog ok — {document['strategyCount']} strategies, "
f"{len(document['categoryCounts'])} categories, "
f"{counts.get('baseline', 0)} baseline + {counts.get('v1', 0)} v1"
)
return EXIT_OK

document = _library_document(console)
if document is None:
return EXIT_DIAGNOSTICS
rows = strategy_rows(document)

if action == "show":
needle = args.strategy.casefold()
match = next(
(
row
for row in rows
if row.get("id", "").casefold() == needle
or row.get("slug", "").casefold() == needle
),
None,
)
if match is None:
console.warn(f"error: unknown library strategy {args.strategy!r}")
return EXIT_USAGE
console.say(json.dumps(match, indent=2, ensure_ascii=False))
return EXIT_OK

if action == "search":
terms = tuple(term.casefold() for term in args.query.split() if term)
if not terms:
console.warn("error: library search needs a non-empty query")
return EXIT_USAGE
matched = [
row
for row in rows
if all(term in searchable_text(row) for term in terms)
]
_print_library_rows(matched, console)
return EXIT_OK

if action == "filter":
if args.category is None and args.regime is None and args.input is None:
console.warn(
"error: library filter needs --category, --regime, or --input"
)
return EXIT_USAGE
if any(
value is not None and not value.strip()
for value in (args.category, args.regime, args.input)
):
console.warn("error: library filter values must not be empty")
return EXIT_USAGE
matched = rows
if args.category is not None:
category = args.category.strip().casefold()
matched = [
row for row in matched if row.get("category", "").casefold() == category
]
if args.regime is not None:
regime = args.regime.strip().casefold()
matched = [
row for row in matched if regime in row.get("regime", "").casefold()
]
if args.input is not None:
input_name = args.input.strip().casefold()
matched = [
row
for row in matched
if any(
input_name in str(signal).casefold()
for signal in row.get("requiredHostSignals", [])
)
]
_print_library_rows(matched, console)
return EXIT_OK

_print_library_rows(rows, console)
return EXIT_OK


def command_version(args: Any, console: Console) -> int:
"""Print component versions — useful when a host reports a mismatch."""
from .. import __version__
Expand All @@ -546,6 +670,7 @@ def command_version(args: Any, console: Console) -> int:
"command_check",
"command_compile",
"command_indicators",
"command_library",
"command_replay",
"command_version",
"command_visualize",
Expand Down
40 changes: 40 additions & 0 deletions nano/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
command_check,
command_compile,
command_indicators,
command_library,
command_replay,
command_version,
command_visualize,
Expand All @@ -56,6 +57,8 @@
nano compile strategy.nano --emit types
nano replay strategy.nano --data bars.csv --date 2026-01-15 --verify
nano visualize strategy.nano --format mermaid
nano library search trend
nano library show ema_pullback_continuation
"""


Expand Down Expand Up @@ -158,6 +161,43 @@ def build_parser() -> argparse.ArgumentParser:
indicators.add_argument("name", nargs="?", metavar="NAME")
indicators.set_defaults(handler=command_indicators)

library = subcommands.add_parser(
"library", help="browse and verify the packaged strategy catalog"
)
library_actions = library.add_subparsers(
dest="library_action", metavar="ACTION", required=True
)

library_list = library_actions.add_parser(
"list", help="list every strategy in stable ID order"
)
library_list.set_defaults(handler=command_library)

library_show = library_actions.add_parser(
"show", help="show one strategy's complete metadata as JSON"
)
library_show.add_argument("strategy", metavar="ID_OR_SLUG")
library_show.set_defaults(handler=command_library)

library_search = library_actions.add_parser(
"search", help="search all authored and derived metadata"
)
library_search.add_argument("query", metavar="QUERY")
library_search.set_defaults(handler=command_library)

library_filter = library_actions.add_parser(
"filter", help="filter by category, regime text, or host input"
)
library_filter.add_argument("--category", metavar="CATEGORY")
library_filter.add_argument("--regime", metavar="TEXT")
library_filter.add_argument("--input", metavar="SIGNAL")
library_filter.set_defaults(handler=command_library)

library_check = library_actions.add_parser(
"check", help="verify catalogability and byte-identical regeneration"
)
library_check.set_defaults(handler=command_library)

version = subcommands.add_parser("version", help="print component versions")
version.set_defaults(handler=command_version)

Expand Down
Loading
Loading