@nomadamas/k-skill 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/bin/k-skill.js +64 -43
  2. package/package.json +2 -2
  3. package/skills/biz-health-check/instruction.md +2 -0
  4. package/skills/company-analysis/instruction.md +68 -0
  5. package/skills/company-analysis/references/source-contract.md +75 -0
  6. package/skills/company-analysis/scripts/dart-rows.js +52 -0
  7. package/skills/company-analysis/scripts/dart-rows.ts +48 -0
  8. package/skills/company-analysis/scripts/evidence.js +567 -0
  9. package/skills/company-analysis/scripts/evidence.ts +520 -0
  10. package/skills/company-analysis/scripts/fetch-public.js +85 -0
  11. package/skills/company-analysis/scripts/fetch-public.ts +75 -0
  12. package/skills/company-analysis/skill.json +6 -0
  13. package/skills/express-bus-booking/scripts/kobus_express_booking.py +46 -18
  14. package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +53 -3
  15. package/skills/g2b-order-plan-search/instruction.md +17 -0
  16. package/skills/g2b-sanctioned-supplier/instruction.md +10 -1
  17. package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +8 -4
  18. package/skills/g2b-sanctioned-supplier/scripts/test_g2b_sanctioned_supplier.py +19 -0
  19. package/skills/government-bond-analysis/instruction.md +216 -0
  20. package/skills/government-bond-analysis/references/methodology.md +63 -0
  21. package/skills/government-bond-analysis/scripts/cli.mjs +268 -0
  22. package/skills/government-bond-analysis/scripts/collect.mjs +171 -0
  23. package/skills/government-bond-analysis/scripts/core.mjs +224 -0
  24. package/skills/government-bond-analysis/scripts/dual.mjs +178 -0
  25. package/skills/government-bond-analysis/scripts/harness.mjs +406 -0
  26. package/skills/government-bond-analysis/scripts/modes.mjs +136 -0
  27. package/skills/government-bond-analysis/scripts/news.mjs +113 -0
  28. package/skills/government-bond-analysis/scripts/render.mjs +120 -0
  29. package/skills/government-bond-analysis/scripts/schedule.mjs +96 -0
  30. package/skills/government-bond-analysis/scripts/ust.mjs +204 -0
  31. package/skills/government-bond-analysis/skill.json +8 -0
  32. package/skills/korean-bond-search/instruction.md +122 -0
  33. package/skills/korean-bond-search/references/fields.md +48 -0
  34. package/skills/korean-bond-search/references/source-notes.md +11 -0
  35. package/skills/korean-bond-search/scripts/bond_search.py +906 -0
  36. package/skills/korean-bond-search/skill.json +9 -0
  37. package/skills/market-event-impact/instruction.md +150 -0
  38. package/skills/market-event-impact/references/event-taxonomy.md +74 -0
  39. package/skills/market-event-impact/references/methodology.md +98 -0
  40. package/skills/market-event-impact/references/research-contract.md +120 -0
  41. package/skills/market-event-impact/references/source-priority.md +25 -0
  42. package/skills/market-event-impact/scripts/adapters.py +32 -0
  43. package/skills/market-event-impact/scripts/asset_selector.py +120 -0
  44. package/skills/market-event-impact/scripts/calendars.py +179 -0
  45. package/skills/market-event-impact/scripts/event_resolver.py +260 -0
  46. package/skills/market-event-impact/scripts/event_study.py +171 -0
  47. package/skills/market-event-impact/scripts/market_data.py +204 -0
  48. package/skills/market-event-impact/scripts/market_event.py +1065 -0
  49. package/skills/market-event-impact/scripts/research.py +389 -0
  50. package/skills/market-event-impact/skill.json +9 -0
  51. package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
  52. package/src/analytics-policy.json +18 -0
  53. package/src/analytics.js +106 -0
  54. package/src/metrics.js +49 -0
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic event-window, curve, and threshold calculations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from datetime import date, datetime, time, timedelta, timezone
7
+ from typing import Any, Iterable
8
+
9
+ from calendars import TIMEZONE_CLOSES, align_date, window_indices, zone_for_date
10
+
11
+
12
+ DEFAULT_OFFSETS = (-1, 0, 1, 5)
13
+
14
+
15
+ def _change(value: float | None, anchor: float | None, rates: bool, unit: str = "pct") -> float | None:
16
+ if value is None or anchor is None:
17
+ return None
18
+ if rates:
19
+ scale = 1.0 if unit.casefold() == "bp" else 100.0
20
+ return round((value - anchor) * scale, 10)
21
+ if anchor == 0:
22
+ return None
23
+ return round((value / anchor - 1.0) * 100.0, 10)
24
+
25
+
26
+ def analyze_series(series: dict[str, Any], event_date: str, alignment: str = "next",
27
+ offsets: Iterable[int] = DEFAULT_OFFSETS, alignment_date: str | None = None) -> dict[str, Any]:
28
+ observations = series["observations"]
29
+ dates = [row["date"] for row in observations]
30
+ target_date = alignment_date or event_date
31
+ aligned, anchor_index = align_date(target_date, dates, alignment)
32
+ result = {
33
+ "symbol": series["symbol"],
34
+ "region": series.get("region"),
35
+ "asset_class": series["asset_class"],
36
+ "unit": series["unit"],
37
+ "event_date": event_date,
38
+ "alignment_anchor_date": target_date,
39
+ "aligned_event_date": aligned,
40
+ "alignment": alignment,
41
+ "alignment_basis": series.get("alignment_basis", "region_calendar_proxy"),
42
+ "observation_provenance": {
43
+ "observation_timezone": series.get("observation_timezone"),
44
+ "observation_time": series.get("observation_time"),
45
+ "market_timezone": series.get("market_timezone"),
46
+ "price_type": series.get("price_type"),
47
+ },
48
+ "source": series["source"],
49
+ "latest_observation_date": dates[-1] if dates else None,
50
+ "window": {},
51
+ "warnings": [],
52
+ }
53
+ if anchor_index is None:
54
+ result["failure"] = "no_observation_for_alignment"
55
+ return result
56
+ positions = window_indices(anchor_index, len(observations), offsets)
57
+ rates = series["asset_class"].casefold() in {"rates", "policy", "spread", "credit"}
58
+ base_index = positions.get(-1)
59
+ base = observations[base_index]["value"] if base_index is not None else None
60
+ for offset, index in positions.items():
61
+ key = "d0" if offset == 0 else f"d{offset:+d}"
62
+ row = observations[index] if index is not None else None
63
+ result["window"][key] = {
64
+ "date": row["date"] if row else None,
65
+ "value": row["value"] if row else None,
66
+ "change_from_d_minus_1": _change(row["value"] if row else None, base, rates, series["unit"]),
67
+ "change_unit": "bp" if rates else "pct",
68
+ }
69
+ if row is None:
70
+ warning = f"pending_{key}" if offset > 0 and anchor_index + offset >= len(observations) else f"missing_{key}"
71
+ result["warnings"].append(warning)
72
+ if base is None:
73
+ result["warnings"].append("changes_unavailable_without_d_minus_1")
74
+ if not series.get("observation_timezone") or not series.get("observation_time"):
75
+ result["warnings"].append("observation_time_basis_missing")
76
+ return result
77
+
78
+
79
+ def analyze_intraday_reaction(series: dict[str, Any], event: dict[str, Any]) -> dict[str, Any] | None:
80
+ """Measure source-stamped pre-event, +1h, and first-session end observations."""
81
+ rows = series.get("intraday_observations") or []
82
+ if not rows:
83
+ return None
84
+ if not event.get("event_time_local") or not event.get("event_timezone"):
85
+ return {"symbol": series["symbol"], "status": "unavailable", "reason": "exact_event_timestamp_required"}
86
+ event_day = date.fromisoformat(event["event_date"])
87
+ event_local_time = time.fromisoformat(event["event_time_local"])
88
+ event_instant = datetime.combine(
89
+ event_day, event_local_time, zone_for_date(event["event_timezone"], event_day)
90
+ ).astimezone(timezone.utc)
91
+ normalized = []
92
+ for row in rows:
93
+ instant = datetime.fromisoformat(str(row["timestamp"]).replace("Z", "+00:00"))
94
+ normalized.append((instant.astimezone(timezone.utc), instant, float(row["value"])))
95
+ normalized.sort(key=lambda item: item[0])
96
+ before = [row for row in normalized if row[0] < event_instant]
97
+ after = [row for row in normalized if row[0] >= event_instant]
98
+ one_hour_after = [row for row in normalized if row[0] >= event_instant + timedelta(hours=1)]
99
+ market_timezone = series.get("market_timezone") or event["event_timezone"]
100
+ session_end = None
101
+ close_confirmed = False
102
+ if after:
103
+ first_market_date = after[0][1].astimezone(zone_for_date(market_timezone, after[0][1].date())).date()
104
+ session_rows = [row for row in after if row[1].astimezone(zone_for_date(market_timezone, row[1].date())).date() == first_market_date]
105
+ if session_rows:
106
+ session_end = session_rows[-1]
107
+ close_time = TIMEZONE_CLOSES.get(market_timezone)
108
+ close_confirmed = bool(close_time and session_end[1].astimezone(zone_for_date(market_timezone, first_market_date)).time().replace(tzinfo=None) == close_time)
109
+ rates = series["asset_class"].casefold() in {"rates", "policy", "spread", "credit"}
110
+ unit = series["unit"]
111
+ anchor = before[-1] if before else None
112
+
113
+ def pack(row: tuple[datetime, datetime, float] | None) -> dict[str, Any] | None:
114
+ if row is None:
115
+ return None
116
+ return {
117
+ "timestamp": row[1].isoformat(),
118
+ "value": row[2],
119
+ "change_from_pre_event": _change(row[2], anchor[2] if anchor else None, rates, unit),
120
+ "change_unit": "bp" if rates else "pct",
121
+ }
122
+
123
+ return {
124
+ "symbol": series["symbol"],
125
+ "status": "ok",
126
+ "pre_event": pack(anchor),
127
+ "plus_1h": pack(one_hour_after[0] if one_hour_after else None),
128
+ "session_end": pack(session_end),
129
+ "session_end_label": "market_close" if close_confirmed else "last_supplied_bar_not_confirmed_close",
130
+ "warnings": [name for name, missing in (
131
+ ("intraday_missing_pre_event", anchor is None),
132
+ ("intraday_missing_plus_1h", not one_hour_after),
133
+ ("intraday_session_close_unconfirmed", session_end is not None and not close_confirmed),
134
+ ) if missing],
135
+ "source": series["source"],
136
+ }
137
+
138
+
139
+ def classify_curve(short_change_bp: float | None, long_change_bp: float | None) -> dict[str, Any]:
140
+ if short_change_bp is None or long_change_bp is None:
141
+ return {"classification": None, "spread_change_bp": None}
142
+ spread = round(long_change_bp - short_change_bp, 10)
143
+ if short_change_bp > 0 and long_change_bp > 0:
144
+ direction = "Bear"
145
+ elif short_change_bp < 0 and long_change_bp < 0:
146
+ direction = "Bull"
147
+ else:
148
+ return {"classification": "Mixed", "spread_change_bp": spread}
149
+ shape = "Steepening" if spread > 0 else "Flattening" if spread < 0 else "Parallel"
150
+ return {"classification": f"{direction} {shape}", "spread_change_bp": spread}
151
+
152
+
153
+ def threshold_events(series: dict[str, Any], metric: str, direction: str, threshold: float) -> list[dict[str, Any]]:
154
+ if direction not in {"above", "below"}:
155
+ raise ValueError("direction must be above or below")
156
+ rates = metric == "bp_change"
157
+ if metric not in {"bp_change", "pct_change"}:
158
+ raise ValueError("metric must be bp_change or pct_change")
159
+ is_rates = series["asset_class"].casefold() in {"rates", "policy", "spread", "credit"}
160
+ if rates and not is_rates:
161
+ raise ValueError("bp_change requires a rates, policy, spread, or credit series")
162
+ rows = series["observations"]
163
+ matches = []
164
+ for previous, current in zip(rows, rows[1:]):
165
+ value = _change(current["value"], previous["value"], rates, series["unit"])
166
+ if value is None:
167
+ continue
168
+ matched = value >= threshold if direction == "above" else value <= threshold
169
+ if matched:
170
+ matches.append({"date": current["date"], "metric": metric, "value": value, "threshold": threshold})
171
+ return matches
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env python3
2
+ """Strict JSON/CSV market-series loader with source metadata."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import csv
7
+ import json
8
+ import math
9
+ import re
10
+ from datetime import date, datetime, time, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from urllib.parse import urlparse
14
+
15
+ from calendars import valid_timezone, zone_for_date
16
+
17
+
18
+ class SeriesError(ValueError):
19
+ pass
20
+
21
+
22
+ def _finite(value: Any) -> float:
23
+ if isinstance(value, bool):
24
+ raise SeriesError(f"not a number: {value!r}")
25
+ try:
26
+ number = float(value)
27
+ except (TypeError, ValueError) as exc:
28
+ raise SeriesError(f"not a number: {value!r}") from exc
29
+ if not math.isfinite(number):
30
+ raise SeriesError(f"not finite: {value!r}")
31
+ return number
32
+
33
+
34
+ def _normalize_series(item: dict[str, Any]) -> dict[str, Any]:
35
+ symbol = str(item.get("symbol") or "").strip()
36
+ if not symbol:
37
+ raise SeriesError("series symbol is required")
38
+ region = str(item.get("region") or "").strip().upper() or None
39
+ if region is not None and not re.fullmatch(r"[A-Z]{2,6}", region):
40
+ raise SeriesError(f"{symbol}: region must be a 2-6 letter market code")
41
+ asset_class = str(item.get("asset_class") or "").strip().casefold()
42
+ if asset_class not in {"rates", "policy", "spread", "credit", "fx", "equity", "commodity", "volatility", "price"}:
43
+ raise SeriesError(f"{symbol}: unsupported asset_class {asset_class!r}")
44
+ unit = str(item.get("unit") or "").strip()
45
+ if not unit:
46
+ raise SeriesError(f"{symbol}: unit is required")
47
+ unit = unit.casefold()
48
+ if asset_class in {"rates", "policy", "spread", "credit"} and unit not in {"pct", "bp"}:
49
+ raise SeriesError(f"{symbol}: rates, policy, spread, and credit unit must be pct or bp")
50
+ source = item.get("source") if isinstance(item.get("source"), dict) else {}
51
+ if not str(source.get("name") or "").strip() or not str(source.get("url") or "").strip():
52
+ raise SeriesError(f"{symbol}: source.name and source.url are required")
53
+ parsed_url = urlparse(str(source["url"]))
54
+ if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
55
+ raise SeriesError(f"{symbol}: source.url must be an http(s) URL")
56
+ observations = item.get("observations")
57
+ if not isinstance(observations, list) or not observations:
58
+ raise SeriesError(f"{symbol}: observations must be a non-empty array")
59
+ seen: set[str] = set()
60
+ clean = []
61
+ for row in observations:
62
+ if not isinstance(row, dict) or "date" not in row or "value" not in row:
63
+ raise SeriesError(f"{symbol}: each observation needs date and value")
64
+ try:
65
+ observed = date.fromisoformat(str(row["date"])).isoformat()
66
+ except ValueError as exc:
67
+ raise SeriesError(f"{symbol}: invalid ISO date {row['date']!r}") from exc
68
+ if observed in seen:
69
+ raise SeriesError(f"{symbol}: duplicate date {observed}")
70
+ seen.add(observed)
71
+ clean.append({"date": observed, "value": _finite(row["value"])})
72
+ clean.sort(key=lambda row: row["date"])
73
+ observation_timezone = str(item.get("observation_timezone") or "").strip() or None
74
+ observation_time = str(item.get("observation_time") or "").strip() or None
75
+ market_timezone = str(item.get("market_timezone") or "").strip() or None
76
+ if bool(observation_timezone) != bool(observation_time):
77
+ raise SeriesError(f"{symbol}: observation_timezone and observation_time must be provided together")
78
+ for field, timezone_name in (("observation_timezone", observation_timezone), ("market_timezone", market_timezone)):
79
+ if timezone_name and not valid_timezone(timezone_name, date.fromisoformat(clean[0]["date"])):
80
+ raise SeriesError(f"{symbol}: {field} must be a valid IANA timezone")
81
+ if observation_time:
82
+ try:
83
+ parsed_observation_time = time.fromisoformat(observation_time)
84
+ if parsed_observation_time.tzinfo is not None:
85
+ raise ValueError
86
+ observation_time = parsed_observation_time.isoformat(timespec="minutes" if parsed_observation_time.second == 0 else "seconds")
87
+ except ValueError as exc:
88
+ raise SeriesError(f"{symbol}: observation_time must be local HH:MM[:SS] without an offset") from exc
89
+ price_type = str(item.get("price_type") or "").strip() or None
90
+ if price_type and len(price_type) > 64:
91
+ raise SeriesError(f"{symbol}: price_type must be 64 characters or fewer")
92
+ raw_intraday = item.get("intraday_observations") or []
93
+ if not isinstance(raw_intraday, list):
94
+ raise SeriesError(f"{symbol}: intraday_observations must be an array")
95
+ intraday = []
96
+ intraday_seen: set[str] = set()
97
+ for index, row in enumerate(raw_intraday):
98
+ if not isinstance(row, dict) or "timestamp" not in row or "value" not in row:
99
+ raise SeriesError(f"{symbol}: intraday_observations[{index}] needs timestamp and value")
100
+ try:
101
+ timestamp = datetime.fromisoformat(str(row["timestamp"]).replace("Z", "+00:00"))
102
+ except ValueError as exc:
103
+ raise SeriesError(f"{symbol}: invalid intraday timestamp {row.get('timestamp')!r}") from exc
104
+ if timestamp.tzinfo is None:
105
+ raise SeriesError(f"{symbol}: intraday timestamps must include a timezone offset")
106
+ utc_key = timestamp.astimezone(timezone.utc).isoformat()
107
+ if utc_key in intraday_seen:
108
+ raise SeriesError(f"{symbol}: duplicate intraday timestamp {timestamp.isoformat()}")
109
+ intraday_seen.add(utc_key)
110
+ intraday.append({"timestamp": timestamp.isoformat(), "value": _finite(row["value"]), "_utc": utc_key})
111
+ intraday.sort(key=lambda row: row["_utc"])
112
+ for row in intraday:
113
+ row.pop("_utc")
114
+ roles = item.get("roles") or []
115
+ if not isinstance(roles, list) or not all(isinstance(role, str) and role.strip() for role in roles):
116
+ raise SeriesError(f"{symbol}: roles must be non-empty strings")
117
+ priority = item.get("selection_priority", 0)
118
+ if isinstance(priority, bool) or not isinstance(priority, int) or not -100 <= priority <= 100:
119
+ raise SeriesError(f"{symbol}: selection_priority must be an integer from -100 to 100")
120
+ return {
121
+ "symbol": symbol,
122
+ "region": region,
123
+ "asset_class": asset_class,
124
+ "unit": unit,
125
+ "source": {"name": str(source.get("name")).strip(), "url": str(source.get("url")).strip()},
126
+ "observation_timezone": observation_timezone,
127
+ "observation_time": observation_time,
128
+ "market_timezone": market_timezone,
129
+ "price_type": price_type,
130
+ "intraday_observations": intraday,
131
+ "roles": list(dict.fromkeys(role.strip().casefold() for role in roles)),
132
+ "selection_priority": priority,
133
+ "observations": clean,
134
+ }
135
+
136
+
137
+ def _unique_symbols(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
138
+ if not items:
139
+ raise SeriesError("series input must contain at least one series")
140
+ symbols = [item["symbol"].casefold() for item in items]
141
+ duplicates = sorted({symbol for symbol in symbols if symbols.count(symbol) > 1})
142
+ if duplicates:
143
+ raise SeriesError(f"duplicate symbols: {', '.join(duplicates)}")
144
+ return items
145
+
146
+
147
+ def load_series(path_text: str) -> list[dict[str, Any]]:
148
+ path = Path(path_text).expanduser()
149
+ if not path.is_file():
150
+ raise SeriesError(f"series file not found: {path}")
151
+ if path.suffix.casefold() == ".json":
152
+ payload = json.loads(path.read_text(encoding="utf-8-sig"))
153
+ items = payload.get("series") if isinstance(payload, dict) else payload
154
+ if not isinstance(items, list):
155
+ raise SeriesError("JSON must be an array or contain a series array")
156
+ return _unique_symbols([_normalize_series(item) for item in items])
157
+ if path.suffix.casefold() == ".csv":
158
+ groups: dict[str, dict[str, Any]] = {}
159
+ with path.open(encoding="utf-8-sig", newline="") as handle:
160
+ for row in csv.DictReader(handle):
161
+ symbol = (row.get("symbol") or "").strip()
162
+ item = groups.setdefault(symbol, {
163
+ "symbol": symbol,
164
+ "region": row.get("region") or None,
165
+ "asset_class": row.get("asset_class") or "price",
166
+ "unit": row.get("unit") or "index",
167
+ "source": {"name": row.get("source_name"), "url": row.get("source_url")},
168
+ "observation_timezone": row.get("observation_timezone") or None,
169
+ "observation_time": row.get("observation_time") or None,
170
+ "market_timezone": row.get("market_timezone") or None,
171
+ "price_type": row.get("price_type") or None,
172
+ "observations": [],
173
+ })
174
+ item["observations"].append({"date": row.get("date"), "value": row.get("value")})
175
+ return _unique_symbols([_normalize_series(item) for item in groups.values()])
176
+ raise SeriesError("series input must be .json or .csv")
177
+
178
+
179
+ def series_as_of(item: dict[str, Any], observed_at: str) -> dict[str, Any]:
180
+ """Exclude observations that were not yet observable at the analysis cutoff."""
181
+ cutoff = datetime.fromisoformat(observed_at.replace("Z", "+00:00")).astimezone(timezone.utc)
182
+ zone_name = item.get("observation_timezone") or item.get("market_timezone")
183
+ zone = zone_for_date(zone_name, cutoff.date()) if zone_name else timezone.utc
184
+ local_cutoff_date = cutoff.astimezone(zone).date()
185
+ observation_time = item.get("observation_time")
186
+ retained = []
187
+ for row in item["observations"]:
188
+ row_date = date.fromisoformat(row["date"])
189
+ if observation_time and item.get("observation_timezone"):
190
+ observed = datetime.combine(
191
+ row_date, time.fromisoformat(observation_time),
192
+ zone_for_date(item["observation_timezone"], row_date),
193
+ ).astimezone(timezone.utc)
194
+ available = observed <= cutoff
195
+ else:
196
+ # A daily date without a verified clock cannot establish same-day availability.
197
+ available = row_date < local_cutoff_date
198
+ if available:
199
+ retained.append(row)
200
+ intraday = [row for row in item.get("intraday_observations", [])
201
+ if datetime.fromisoformat(row["timestamp"].replace("Z", "+00:00")).astimezone(timezone.utc) <= cutoff]
202
+ return {**item, "observations": retained, "intraday_observations": intraday,
203
+ "asof_excluded_observations": len(item["observations"]) - len(retained),
204
+ "original_first_observation_date": item["observations"][0]["date"]}