hypgraph 0.7.0__tar.gz → 0.7.1__tar.gz
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.
- {hypgraph-0.7.0 → hypgraph-0.7.1}/CHANGELOG.md +23 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/PKG-INFO +1 -1
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/__init__.py +1 -1
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/coord.py +9 -2
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/derive.py +24 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/language.py +3 -1
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/profiles.py +16 -2
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/workflow.py +54 -5
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_measure_questions.py +1 -1
- hypgraph-0.7.1/tests/test_opposed_answers.py +99 -0
- hypgraph-0.7.1/tests/test_rerunnable.py +75 -0
- hypgraph-0.7.1/tests/test_stats_not_backtests.py +68 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_workflows.py +7 -7
- {hypgraph-0.7.0 → hypgraph-0.7.1}/.gitignore +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/AGENTS.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/CITATION.cff +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/LICENSE +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/NOTICE +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/README.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/PLAN.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/decisions/ADR-001-token-bound-authors.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/decisions/ADR-002-governance-profiles.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/profile-workflows-plan.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/review-2026-10-24.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/site-plan.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/spec.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/ui-plan.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/README.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/knowledge.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/optimization.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/scientific.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/software.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/docs/workflows/trading.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/pyproject.toml +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/__main__.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/bundle.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/checks.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/cli.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/demos/__init__.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/demos/broad_street.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/graph.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/mcp.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/plan.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/policy.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/refusals.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/report_check.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/schema.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/server.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/skill/SKILL.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/stats.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/store.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/__init__.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/atlas.css +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/atlas.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.css +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/hypgraph.svg +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/index.html +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/LICENSES.md +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cose-base.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cytoscape-fcose.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/cytoscape.min.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/src/hypgraph/ui/vendor/layout-base.js +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_bundle_telemetry.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_cancel_releases_lease.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_coord.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_d10.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_engine.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_engine_fixes.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_incremental.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_log_and_check.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_order_detail.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_profiles.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_profiles_041.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_questions_not_assertions.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_refusals.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_report_answers.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_restates_scale.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_server_auth.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_stats_values.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_surfaces.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_telemetry_stream.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_ui_app.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_ui_engine.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_unnamed_inputs.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_versions_not_figures.py +0 -0
- {hypgraph-0.7.0 → hypgraph-0.7.1}/tests/test_vocabulary_friction.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1 — 2026-10-27: a computed answer needs its code, and opposed answers keep a question open
|
|
4
|
+
|
|
5
|
+
- **A computation nobody can re-run doesn't settle a computational question.** The optimization workflow's
|
|
6
|
+
`solved`, trading's `backtested` and the scientific workflow's evidenced stage (`result_exists` under a
|
|
7
|
+
scientific profile) count only results something can re-run: the run that produced them carries its code, or
|
|
8
|
+
it is the instrument's (`kind=lab`). Through the hypswarm CLI, with no tool that executes code, the seats wrote
|
|
9
|
+
"exact DP over capacity" and "brute force over 2^21" as prose; the verifier passed a wrong optimum (757 for
|
|
10
|
+
656) and the question settled. The gate now says which results report a computation with no code behind it.
|
|
11
|
+
`workflow.rerunnable(snap, result)`.
|
|
12
|
+
- **A question settles only when every live answer to it is checked.** A checked answer beside a live unchecked one
|
|
13
|
+
used to settle the question, and the two may disagree: in a real-model trading run a reviewer re-ran the numbers
|
|
14
|
+
behind "no look-ahead" and checked it while "it is look-ahead" stood unchecked on the same question. The board row
|
|
15
|
+
now says `answered` with the unchecked answers named (`unchecked`), the reviewed gate names them, and completion
|
|
16
|
+
waits. An answer nobody may review (its author is the only reviewer) doesn't block; the row lists it under
|
|
17
|
+
`unreviewable`. `derive.unchecked_answers` / `unreviewable_answers`.
|
|
18
|
+
- **Measuring returns isn't trading them.** Trading's BACKTEST_WITHOUT_COSTS no longer fires on a result that counts
|
|
19
|
+
its returns (`n_returns`, `insample_n_returns`, a value in units of returns, days, bars or trades) or reports a
|
|
20
|
+
statistic of them (an autocorrelation, its t, p, standard error or CI). A return, P&L or Sharpe still needs its
|
|
21
|
+
costs. A real-model run lost eight writes measuring a lag-1 autocorrelation.
|
|
22
|
+
- **A confidence interval that excludes zero isn't an exclusion.** "Excludes zero", "excludes 1" (a ratio's
|
|
23
|
+
null) and "rules out chance" say an effect is there, so they no longer need a power check
|
|
24
|
+
(EXCLUSION_WITHOUT_POWER). "Rules out an effect" and "no effect" still do.
|
|
25
|
+
|
|
3
26
|
## 0.7.0 — 2026-10-27: measure questions, and claim checks that know a version from a figure
|
|
4
27
|
|
|
5
28
|
- **Measure questions.** A question may say `asks: "measure"`, and its workflow's `measure_skips` gates then don't
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hypgraph
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: The evidence layer for AI research agents: a hash-chained research record that checks every claim as it is written and derives how well each is known.
|
|
5
5
|
Project-URL: Homepage, https://hypgraph.com
|
|
6
6
|
Project-URL: Documentation, https://hypgraph.com/engineers/
|
|
@@ -16,7 +16,7 @@ Standing is derived on every read, never stored; every write is an event in a SH
|
|
|
16
16
|
their numbers; reviews pin versions; independence is counted, not assumed. See https://hypgraph.com.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "0.7.
|
|
19
|
+
__version__ = "0.7.1"
|
|
20
20
|
|
|
21
21
|
from .bundle import verify_bundle # noqa: E402
|
|
22
22
|
from .graph import Author, Graph, GraphError, file_sha256 # noqa: E402
|
|
@@ -729,7 +729,13 @@ def question_board(g: "Graph", inv: str) -> list[dict]:
|
|
|
729
729
|
checked = [c for c in ans if states[c] == "checked"]
|
|
730
730
|
contested = [c for c in ans if states[c] == "contested"]
|
|
731
731
|
o = open_orders.get(q)
|
|
732
|
-
|
|
732
|
+
pending = D.unchecked_answers(s, q)
|
|
733
|
+
if checked and pending:
|
|
734
|
+
# two live answers, one checked: settled only once every live answer is checked (they may disagree)
|
|
735
|
+
status, by = "answered", (f"{', '.join(_code(s, c) for c in pending[:3])} unchecked beside "
|
|
736
|
+
f"{_code(s, checked[0])} (checked) — review it, or withdraw the wrong answer"
|
|
737
|
+
+ (f"; order {o['code']} on it" if o else ""))
|
|
738
|
+
elif checked:
|
|
733
739
|
status, by = "settled", f"{_code(s, checked[0])} (checked by " + ", ".join(
|
|
734
740
|
x.split(":")[0] for x in D.state(s, checked[0])["review"]["positive"][:2]) + ")"
|
|
735
741
|
elif contested:
|
|
@@ -741,7 +747,8 @@ def question_board(g: "Graph", inv: str) -> list[dict]:
|
|
|
741
747
|
else:
|
|
742
748
|
status, by = "open", ""
|
|
743
749
|
rows.append({"id": q, "code": n["code"], "text": _title(s, q), "status": status, "by": by,
|
|
744
|
-
"answers": [_code(s, c) for c in ans]
|
|
750
|
+
"answers": [_code(s, c) for c in ans], "unchecked": [_code(s, c) for c in pending],
|
|
751
|
+
"unreviewable": [_code(s, c) for c in D.unreviewable_answers(s, q)]})
|
|
745
752
|
return rows
|
|
746
753
|
|
|
747
754
|
|
|
@@ -237,6 +237,30 @@ def _moved_since(s: Snap, rid: str) -> list[str]:
|
|
|
237
237
|
return out
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
def _open_answers(s: Snap, q: str) -> list[str]:
|
|
241
|
+
return [e["src"] for e in s.inn.get(q, []) if e["rel"] == "answers" and s.nodes[e["src"]]["type"] == "claim"
|
|
242
|
+
and lifecycle(s, e["src"]) == "active" and not fallen(s, e["src"]) and not stale(s, e["src"])
|
|
243
|
+
and (state(s, e["src"]).get("review") or {}).get("state") != "checked"]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def reviewable(s: Snap, nid: str) -> bool:
|
|
247
|
+
"""Someone besides its author (and the curators) may review it."""
|
|
248
|
+
return bool(board(s, nid) - set(s.policy.curators) - {s.nodes[nid]["created_by"]})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def unchecked_answers(s: Snap, q: str) -> list[str]:
|
|
252
|
+
"""Live, fresh claims answering question ``q`` that no reviewer has checked and one could. A question settles only
|
|
253
|
+
when this is empty: a checked answer beside a live unchecked one is two answers the record hasn't reconciled, and
|
|
254
|
+
they may disagree (studies T1: "no look-ahead" checked while "is look-ahead" stood unchecked on the same
|
|
255
|
+
question). An answer nobody may review (its author is the only reviewer) can't block it; ``unreviewable_answers``
|
|
256
|
+
lists those."""
|
|
257
|
+
return [c for c in _open_answers(s, q) if reviewable(s, c)]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def unreviewable_answers(s: Snap, q: str) -> list[str]:
|
|
261
|
+
return [c for c in _open_answers(s, q) if not reviewable(s, c)]
|
|
262
|
+
|
|
263
|
+
|
|
240
264
|
def stale(s: Snap, nid: str) -> list[str]:
|
|
241
265
|
"""Why a result (or a claim through its results) no longer speaks for what it measured; [] when fresh. A result
|
|
242
266
|
is stale when the run that produced it used an artifact that is now superseded or withdrawn, or when its
|
|
@@ -30,7 +30,9 @@ EXCLUSION = re.compile(
|
|
|
30
30
|
r"\b(no (?:effect|difference|impact|influence|benefit|improvement|association|relationship)|"
|
|
31
31
|
r"makes? no difference|has no bearing|(?:does|do|did)(?: not|n't) (?:affect|matter|help|work|change|improve)|"
|
|
32
32
|
r"doesn't (?:affect|matter|help|work|change|improve)|not (?:associated|related|correlated)|ineffective|"
|
|
33
|
-
r"useless|rules? out|ruled out|excludes?|
|
|
33
|
+
r"useless|(?:rules? out|ruled out|excludes?)(?!\s+(?:zero|0(?:\.0+)?|the null|null|chance|a zero|one|"
|
|
34
|
+
r"1(?:\.0+)?)\b)|unrelated to)\b", re.I)
|
|
35
|
+
# "the 95 % CI excludes zero", "rules out chance": leaving out the null value asserts an effect, not its absence
|
|
34
36
|
HEDGE = re.compile(r"\b(could|might|may|likely|probably|perhaps|possibly|suggests?|consistent with|"
|
|
35
37
|
r"aligns? with|plausibl\w*)\b", re.I)
|
|
36
38
|
|
|
@@ -583,7 +583,17 @@ def claim_issues(s: D.Snap, inv: str, body: dict, support: list, pending: list)
|
|
|
583
583
|
|
|
584
584
|
# a statistic *about* returns (a signal's correlation with tomorrow's return, a hit rate, the volatility) isn't a return
|
|
585
585
|
# a strategy earned and charges no costs: a look-ahead diagnostic was refused three times as a backtest (studies T1)
|
|
586
|
-
_STAT_OF_RETURNS = re.compile(r"corr|agree|match|hit_rate|_ic\b|beta|volatil|std|skew|kurt"
|
|
586
|
+
_STAT_OF_RETURNS = re.compile(r"corr|agree|match|hit_rate|_ic\b|beta|volatil|std|(?:^|_)sd(?:_|$)|skew|kurt|acf|"
|
|
587
|
+
r"autocov|lag\d|"
|
|
588
|
+
r"t_?stat|p_?val|_se\b|ci_|_ci\b", re.I)
|
|
589
|
+
# a count of returns is not a return earned: n_returns, insample_n_returns, N_returns_in_sample (studies T3)
|
|
590
|
+
_COUNT_KEY = re.compile(r"(?:^|_)(?:n|num|count|nobs|obs)(?:_|$)|_count\b", re.I)
|
|
591
|
+
_COUNT_UNITS = {"returns", "days", "bars", "obs", "observations", "count", "draws", "trades", "rows", "n"}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _a_count(k: str, v: object) -> bool:
|
|
595
|
+
unit = str(v.get("unit") or "").strip().lower() if isinstance(v, dict) else ""
|
|
596
|
+
return bool(_COUNT_KEY.search(k)) or unit in _COUNT_UNITS
|
|
587
597
|
|
|
588
598
|
|
|
589
599
|
def result_issues(s: D.Snap, inv: str, body: dict, pending: list | None = None) -> list[Problem]:
|
|
@@ -595,7 +605,9 @@ def result_issues(s: D.Snap, inv: str, body: dict, pending: list | None = None)
|
|
|
595
605
|
if "objective_needs_feasibility" in p.validators and "objective" in v and "feasible" not in v:
|
|
596
606
|
out.append(Problem("OBJECTIVE_WITHOUT_FEASIBILITY", "a result reports an objective value without saying whether "
|
|
597
607
|
"the solution is feasible", fix="add values.feasible (true/false) and bound or gap"))
|
|
598
|
-
|
|
608
|
+
raw = {str(k).lower(): x for k, x in (body.get("values") or {}).items()}
|
|
609
|
+
earned = [k for k in v if re.search(r"return|sharpe|pnl|cagr|profit", k) and not _STAT_OF_RETURNS.search(k)
|
|
610
|
+
and not _a_count(k, raw.get(k))]
|
|
599
611
|
if "backtest_needs_costs" in p.validators and earned and not _has(v, r"fee|cost|slippage|commission"):
|
|
600
612
|
out.append(Problem("BACKTEST_WITHOUT_COSTS", "a backtest result reports returns without the costs it charged",
|
|
601
613
|
fix="add values for fees and slippage (zero is a value; absence isn't)"))
|
|
@@ -630,6 +642,8 @@ def _questions(s: D.Snap, inv: str) -> list[str]:
|
|
|
630
642
|
|
|
631
643
|
|
|
632
644
|
def _settling_claim(s: D.Snap, q: str) -> str:
|
|
645
|
+
if D.unchecked_answers(s, q): # a checked answer beside an unchecked one doesn't settle it
|
|
646
|
+
return ""
|
|
633
647
|
for e in s.inn.get(q, []):
|
|
634
648
|
c = e["src"]
|
|
635
649
|
if e["rel"] == "answers" and s.nodes[c]["type"] == "claim" and _live(s, c) and \
|
|
@@ -213,8 +213,46 @@ def _stale_why(sc: Scope) -> str:
|
|
|
213
213
|
"now)") if old else ""
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def rerunnable(s: D.Snap, r: str) -> bool:
|
|
217
|
+
"""Whether something can re-run the computation behind a result: the run that produced it carries its code, or it
|
|
218
|
+
is the instrument's own (kind=lab). A run with neither is a report of a computation. In a real run with no tool
|
|
219
|
+
that executes code, the seats wrote "exact DP over capacity" and "brute force over 2^21" with no code behind them,
|
|
220
|
+
a wrong optimum (757 for 656) passed its verifier, and the question settled (studies O1 through the CLI)."""
|
|
221
|
+
for e in s.inn.get(r, []):
|
|
222
|
+
run = s.nodes.get(e["src"]) or {}
|
|
223
|
+
if e["rel"] == "produced" and run.get("type") == "run" and _has_code(s, e["src"]):
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _has_code(s: D.Snap, run: str) -> bool:
|
|
229
|
+
"""The instrument's run, or one that carries what it executed: its source (``config.source``, as hypswarm's
|
|
230
|
+
register_run writes it), its ``code`` list, or an artifact it ``uses`` in the role ``code``."""
|
|
231
|
+
b = s.nodes[run].get("body") or {}
|
|
232
|
+
if b.get("kind") == "lab" or str((b.get("config") or {}).get("source") or "").strip() or b.get("code"):
|
|
233
|
+
return True
|
|
234
|
+
return any(e["rel"] == "uses" and (e.get("data") or {}).get("role") == "code" for e in s.out.get(run, []))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _computed(sc: Scope, rs: list[str]) -> tuple[list[str], str]:
|
|
238
|
+
"""The results among ``rs`` something can re-run, and why the others don't count."""
|
|
239
|
+
ok = [r for r in rs if rerunnable(sc.s, r)]
|
|
240
|
+
bare = [r for r in rs if r not in ok]
|
|
241
|
+
why = (f" ({', '.join(sc.code(r) for r in bare[-3:])} {'reports a computation' if len(bare) == 1 else 'report computations'}"
|
|
242
|
+
" with no code behind it: register the run with the code that computed it, so it can be re-run)") if bare else ""
|
|
243
|
+
return ok, why
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _profile(sc: Scope) -> str:
|
|
247
|
+
return str((sc.s.nodes.get(sc.inv, {}).get("body") or {}).get("profile") or "")
|
|
248
|
+
|
|
249
|
+
|
|
216
250
|
def g_result_exists(sc: Scope, now: str) -> dict:
|
|
217
251
|
rs = _current_results(sc)
|
|
252
|
+
if _profile(sc).startswith("scientific"):
|
|
253
|
+
# evidenced is the scientific workflow's computed stage ('worker, instrument'): a result there is a computation
|
|
254
|
+
rs, why = _computed(sc, rs)
|
|
255
|
+
return _g(bool(rs), rs, f"no live result on {sc.code(sc.root)} that can be re-run{why}{_stale_why(sc)}")
|
|
218
256
|
return _g(bool(rs), rs, f"no live result on {sc.code(sc.root)}{_stale_why(sc)}")
|
|
219
257
|
|
|
220
258
|
|
|
@@ -239,6 +277,13 @@ def g_reviewed(sc: Scope, now: str) -> dict:
|
|
|
239
277
|
s = sc.s
|
|
240
278
|
checked = [c for c in sc.claims if _live(s, c) and _checked_since(sc, c)]
|
|
241
279
|
fresh = [c for c in checked if not D.stale(s, c)]
|
|
280
|
+
pending = D.unchecked_answers(s, sc.root) if s.nodes[sc.root]["type"] == "question" else []
|
|
281
|
+
if fresh and pending:
|
|
282
|
+
others = ", ".join(sc.code(c) for c in pending[:3])
|
|
283
|
+
return _g(False, [], f"{sc.code(fresh[0])} is checked, but {others} also answer{'s' if len(pending) == 1 else ''} "
|
|
284
|
+
f"{sc.code(sc.root)} and nobody has checked {'it' if len(pending) == 1 else 'them'} — review "
|
|
285
|
+
f"{others}, or withdraw whichever answer is wrong: a question settles when every live answer "
|
|
286
|
+
"to it is checked")
|
|
242
287
|
if fresh:
|
|
243
288
|
return _g(True, fresh, "")
|
|
244
289
|
if checked:
|
|
@@ -347,7 +392,8 @@ def _trials(v: dict) -> bool:
|
|
|
347
392
|
def g_backtested(sc: Scope, now: str) -> dict:
|
|
348
393
|
s = sc.s
|
|
349
394
|
nearest, missing = "", []
|
|
350
|
-
|
|
395
|
+
results, bare = _computed(sc, _current_results(sc))
|
|
396
|
+
for r in results:
|
|
351
397
|
v = _vals(s.nodes[r]["body"])
|
|
352
398
|
miss = []
|
|
353
399
|
if not _has(v, r"oos|out_of_sample|holdout|test_"):
|
|
@@ -361,8 +407,8 @@ def g_backtested(sc: Scope, now: str) -> dict:
|
|
|
361
407
|
if not nearest or len(miss) < len(missing):
|
|
362
408
|
nearest, missing = r, miss
|
|
363
409
|
if nearest:
|
|
364
|
-
return _g(False, [], f"{sc.code(nearest)} lacks {', '.join(missing)}")
|
|
365
|
-
return _g(False, [], f"no backtest result on {sc.code(sc.root)}{_stale_why(sc)}")
|
|
410
|
+
return _g(False, [], f"{sc.code(nearest)} lacks {', '.join(missing)}{bare}")
|
|
411
|
+
return _g(False, [], f"no backtest result on {sc.code(sc.root)}{bare}{_stale_why(sc)}")
|
|
366
412
|
|
|
367
413
|
|
|
368
414
|
def _position_missing(body: dict) -> list[str]:
|
|
@@ -421,14 +467,15 @@ def g_still_valid(sc: Scope, now: str) -> dict:
|
|
|
421
467
|
|
|
422
468
|
def g_solved(sc: Scope, now: str) -> dict:
|
|
423
469
|
s = sc.s
|
|
424
|
-
|
|
470
|
+
results, bare = _computed(sc, _current_results(sc))
|
|
471
|
+
for r in results:
|
|
425
472
|
v = _vals(s.nodes[r]["body"])
|
|
426
473
|
if "feasible" in v and v["feasible"] in (False, 0, "false", "False", "no"):
|
|
427
474
|
return _g(True, [r], "")
|
|
428
475
|
if "objective" in v and v.get("feasible") in _TRUE and ("bound" in v or "gap" in v):
|
|
429
476
|
return _g(True, [r], "")
|
|
430
477
|
return _g(False, [], f"no result on {sc.code(sc.root)} with objective, feasible=true and a bound or gap (or "
|
|
431
|
-
"feasible=false)" + _stale_why(sc))
|
|
478
|
+
"feasible=false)" + bare + _stale_why(sc))
|
|
432
479
|
|
|
433
480
|
|
|
434
481
|
def g_result_reviewed(sc: Scope, now: str) -> dict:
|
|
@@ -528,6 +575,8 @@ def stage_of(s: D.Snap, t: str, now: str = "") -> dict:
|
|
|
528
575
|
if any(st.gate == "tests_pass_current" for st in stages):
|
|
529
576
|
settled += [(c, "verified failure: an instrument-recorded negative result, checked") for c in checked
|
|
530
577
|
if any(_lab_negative(s, r) for r in _claim_results(s, c))]
|
|
578
|
+
if settled and s.nodes[q]["type"] == "question" and D.unchecked_answers(s, q):
|
|
579
|
+
settled = [] # another live answer is unchecked: the record hasn't reconciled them
|
|
531
580
|
if stage is not None and settled:
|
|
532
581
|
rv = next((i for i, st in enumerate(stages) if st.gate == "reviewed"), len(stages) - 1)
|
|
533
582
|
return {**base, "workflow": True, "stage": stages[rv].name, "index": rv + 1, "of": len(stages),
|
|
@@ -21,7 +21,7 @@ def _g(profile: str, asks: str | None = "measure"):
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _measured(g, inv, q):
|
|
24
|
-
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded"},
|
|
24
|
+
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded", "config": {"source": "print('count')"}},
|
|
25
25
|
links=[{"rel": "part_of", "to": q}])["id"]
|
|
26
26
|
rs = g.create(author="w", type="result", inv=inv, fields={"summary": "A is right on 781 of 1000 items.",
|
|
27
27
|
"values": {"a_correct": {"k": 781, "n": 1000}}, "outcome": "positive"},
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""A question settles only when every live answer to it is checked. A checked answer beside a live unchecked one is
|
|
2
|
+
two answers the record hasn't reconciled, and they may disagree: in studies T1 a reviewer re-ran the numbers behind
|
|
3
|
+
"no look-ahead" and checked it, while "it is look-ahead" stood unchecked on the same question, and the question
|
|
4
|
+
counted as settled with the wrong answer."""
|
|
5
|
+
from hypgraph import Graph, Policy, profiles
|
|
6
|
+
|
|
7
|
+
H = "harness"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _g(profile: str = "", asks: str = "measure"):
|
|
11
|
+
g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
|
|
12
|
+
f = {"title": "Opposed answers.", "objective": "See two live answers keep a question open."}
|
|
13
|
+
if profile:
|
|
14
|
+
f["profile"] = profile
|
|
15
|
+
inv = g.create(author=H, type="investigation", fields=f)["id"]
|
|
16
|
+
q = g.create(author=H, type="question", inv=inv, code="Q1",
|
|
17
|
+
fields={"text": "Could the signal be known when a trade on it is placed?", "asks": asks})["id"]
|
|
18
|
+
return g, inv, q
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _answer(g, inv, q, author, statement, corr):
|
|
22
|
+
run = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "corr", "state": "succeeded"},
|
|
23
|
+
links=[{"rel": "part_of", "to": q}])["id"]
|
|
24
|
+
rs = g.create(author=author, type="result", inv=inv, fields={
|
|
25
|
+
"summary": f"The signal's correlation with the next day's return is {corr}.",
|
|
26
|
+
"values": {"corr_next": {"value": corr}}, "outcome": "positive"},
|
|
27
|
+
links=[{"rel": "produced", "from": run}])["id"]
|
|
28
|
+
return g.create(author=author, type="claim", inv=inv, fields={
|
|
29
|
+
"statement": statement, "scope": "prices.csv, every day.",
|
|
30
|
+
"measurements": [{"name": "corr", "key": "corr_next", "result": rs}]},
|
|
31
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _board(g, inv):
|
|
35
|
+
return {r["code"]: r for r in g.question_board(inv)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_a_second_live_answer_keeps_the_question_open_until_it_is_checked():
|
|
39
|
+
g, inv, q = _g()
|
|
40
|
+
no = _answer(g, inv, q, "quant", "There is no look-ahead: the correlation with the next day is 0.34.", 0.34)
|
|
41
|
+
g.review(author="critic", target=no, verdict="checked", method="Re-ran the correlation.", notes="0.34 holds.")
|
|
42
|
+
assert _board(g, inv)["Q1"]["status"] == "settled"
|
|
43
|
+
yes = _answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
|
|
44
|
+
row = _board(g, inv)["Q1"]
|
|
45
|
+
assert row["status"] == "answered" and row["unchecked"] == [g.get(yes)["code"]]
|
|
46
|
+
assert "unchecked beside" in row["by"] and g.get(no)["code"] in row["by"]
|
|
47
|
+
crit = {c["name"]: c for c in profiles.completion(g.snapshot(), inv)["criteria"]}
|
|
48
|
+
assert not crit["questions_settled"]["met"]
|
|
49
|
+
# the reviewer reads the second answer and objects: it falls, and the first answer settles the question again
|
|
50
|
+
g.review(author="critic", target=yes, verdict="objection", method="Read both claims against the file.",
|
|
51
|
+
notes="Contradicts the checked answer; the file's signal column is built from the next close.")
|
|
52
|
+
assert _board(g, inv)["Q1"]["status"] == "settled"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_two_checked_answers_settle():
|
|
56
|
+
g, inv, q = _g()
|
|
57
|
+
a = _answer(g, inv, q, "quant", "The correlation with the next day's return is 0.34.", 0.34)
|
|
58
|
+
b = _answer(g, inv, q, "risk", "The signal matches the next day's move on 0.71 of days.", 0.71)
|
|
59
|
+
for c in (a, b):
|
|
60
|
+
g.review(author="critic", target=c, verdict="checked", method="Re-ran it.", notes="The numbers hold as stated.")
|
|
61
|
+
assert _board(g, inv)["Q1"]["status"] == "settled" and _board(g, inv)["Q1"]["unchecked"] == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_the_reviewed_gate_names_the_unchecked_answer():
|
|
65
|
+
g, inv, q = _g("trading@2")
|
|
66
|
+
no = _answer(g, inv, q, "quant", "There is no look-ahead: the correlation with the next day is 0.34.", 0.34)
|
|
67
|
+
g.review(author="critic", target=no, verdict="checked", method="Re-ran the correlation.", notes="0.34 holds.")
|
|
68
|
+
assert g.stage(q)["complete"]
|
|
69
|
+
yes = _answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
|
|
70
|
+
st = g.stage(q)
|
|
71
|
+
gate = next(gt for gt in st["gates"] if gt["gate"] == "reviewed")
|
|
72
|
+
assert not st["complete"] and not gate["met"]
|
|
73
|
+
assert g.get(yes)["code"] in gate["why"] and "every live answer" in gate["why"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_a_checked_inconclusive_answer_does_not_end_the_question_beside_an_unchecked_finding():
|
|
77
|
+
g, inv, q = _g("trading@2", asks="")
|
|
78
|
+
run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "read", "state": "succeeded"},
|
|
79
|
+
links=[{"rel": "part_of", "to": q}])["id"]
|
|
80
|
+
rs = g.create(author="quant", type="result", inv=inv, fields={
|
|
81
|
+
"summary": "No column in prices.csv says when the signal was published.", "outcome": "inconclusive"},
|
|
82
|
+
links=[{"rel": "produced", "from": run}])["id"]
|
|
83
|
+
inc = g.create(author="quant", type="claim", inv=inv, fields={
|
|
84
|
+
"statement": "Whether the signal is known in time can't be told from these files.",
|
|
85
|
+
"scope": "prices.csv as given.", "kind": "inconclusive"},
|
|
86
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])["id"]
|
|
87
|
+
g.review(author="critic", target=inc, verdict="checked", method="Read the files.", notes="Nothing dates the signal.")
|
|
88
|
+
assert g.stage(q)["complete"]
|
|
89
|
+
_answer(g, inv, q, "risk", "It is look-ahead: the signal agrees with the next day's move, 0.71 of days.", 0.71)
|
|
90
|
+
assert not g.stage(q)["complete"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_an_answer_nobody_may_review_does_not_block():
|
|
94
|
+
g, inv, q = _g()
|
|
95
|
+
mine = _answer(g, inv, q, "critic", "The correlation with the next day's return is 0.34.", 0.34) # the only reviewer
|
|
96
|
+
theirs = _answer(g, inv, q, "quant", "The signal matches the next day's move on 0.71 of days.", 0.71)
|
|
97
|
+
g.review(author="critic", target=theirs, verdict="checked", method="Re-ran it.", notes="The numbers hold as stated.")
|
|
98
|
+
row = _board(g, inv)["Q1"]
|
|
99
|
+
assert row["status"] == "settled" and row["unreviewable"] == [g.get(mine)["code"]] and row["unchecked"] == []
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""A computational gate counts only a result something can re-run: the run that produced it carries its code, or it is
|
|
2
|
+
the instrument's (kind=lab). Through the CLI, with no tool that executes code, the seats of an optimization run
|
|
3
|
+
wrote "exact DP over capacity" and "brute force over 2^21" as prose, the verifier passed a wrong optimum (757 for
|
|
4
|
+
656) and the question settled. solved (optimization), backtested (trading) and the scientific workflow's evidenced
|
|
5
|
+
stage now want the code."""
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hypgraph import Graph, Policy
|
|
9
|
+
|
|
10
|
+
H = "harness"
|
|
11
|
+
SHA = "a" * 64
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _g(profile: str, reviewers=("verifier",)):
|
|
15
|
+
g = Graph(policy=Policy(reviewers=set(reviewers), extra_reserved={H}))
|
|
16
|
+
inv = g.create(author=H, type="investigation", fields={"title": "Load the van.", "profile": profile,
|
|
17
|
+
"objective": "Find the best selection under the weight limit."})["id"]
|
|
18
|
+
q = g.create(author=H, type="question", inv=inv, code="Q1",
|
|
19
|
+
fields={"text": "What is the highest total value that fits in the van?"})["id"]
|
|
20
|
+
return g, inv, q
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _gate(g, q, name):
|
|
24
|
+
return next(x for x in g.stage(q)["gates"] if x["gate"] == name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _solve(g, inv, q, run_fields, author="solver"):
|
|
28
|
+
inst = g.search("", "artifact", inv, 5)
|
|
29
|
+
inst = inst[0]["id"] if inst else g.create(author="lead", type="artifact", inv=inv, fields={
|
|
30
|
+
"sha256": SHA, "path": "items.csv", "kind": "instance"}, links=[{"rel": "part_of", "to": q}])["id"]
|
|
31
|
+
run = g.create(author=author, type="run", inv=inv, fields={"name": "dp", "state": "succeeded", **run_fields},
|
|
32
|
+
links=[{"rel": "uses", "to": inst}])["id"]
|
|
33
|
+
rs = g.create(author=author, type="result", inv=inv, fields={"summary": "Exact DP over capacity: the optimum.",
|
|
34
|
+
"values": {"objective": {"value": 757}, "feasible": {"value": True}, "bound": {"value": 757}},
|
|
35
|
+
"outcome": "positive"}, links=[{"rel": "produced", "from": run}])["id"]
|
|
36
|
+
g.create(author=author, type="claim", inv=inv, fields={"statement": "The highest total value is 757 (bound 757).",
|
|
37
|
+
"scope": "items.csv at a 212 kg limit."}, links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
|
|
38
|
+
return rs
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_a_solve_with_no_code_behind_it_is_not_solved():
|
|
42
|
+
g, inv, q = _g("optimization@2")
|
|
43
|
+
rs = _solve(g, inv, q, {"kind": "agent"})
|
|
44
|
+
gate = _gate(g, q, "solved")
|
|
45
|
+
assert not gate["met"] and "no code behind it" in gate["why"] and g.get(rs)["code"] in gate["why"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize("fields", [{"kind": "agent", "config": {"source": "import csv\nprint('objective', 656)"}},
|
|
49
|
+
{"kind": "agent", "code": ["solve.py"]}, {"kind": "lab"}])
|
|
50
|
+
def test_a_solve_with_its_code_or_by_the_instrument_is_solved(fields):
|
|
51
|
+
g, inv, q = _g("optimization@2")
|
|
52
|
+
_solve(g, inv, q, fields, author="lab" if fields["kind"] == "lab" else "solver")
|
|
53
|
+
assert _gate(g, q, "solved")["met"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_a_backtest_with_no_code_is_not_backtested():
|
|
57
|
+
g, inv, q = _g("trading@2", reviewers=("risk",))
|
|
58
|
+
run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "bt", "state": "succeeded"})["id"]
|
|
59
|
+
g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtest out of sample after costs.",
|
|
60
|
+
"values": {"oos_return": {"value": -0.18}, "fees": {"value": 0.001}, "trials": {"value": 20}},
|
|
61
|
+
"outcome": "negative"}, links=[{"rel": "produced", "from": run}, {"rel": "part_of", "to": q}])
|
|
62
|
+
gate = _gate(g, q, "backtested")
|
|
63
|
+
assert not gate["met"] and "no code behind it" in gate["why"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_a_scientific_result_needs_its_code_but_a_sourced_one_does_not():
|
|
67
|
+
g, inv, q = _g("scientific@2", reviewers=("critic",))
|
|
68
|
+
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "count", "state": "succeeded"},
|
|
69
|
+
links=[{"rel": "part_of", "to": q}])["id"]
|
|
70
|
+
g.create(author="w", type="result", inv=inv, fields={"summary": "A is right on 781 of 1000 items.",
|
|
71
|
+
"values": {"a_correct": {"k": 781, "n": 1000}}, "outcome": "positive"}, links=[{"rel": "produced", "from": run}])
|
|
72
|
+
assert not _gate(g, q, "result_exists")["met"]
|
|
73
|
+
from hypgraph import workflow as W
|
|
74
|
+
assert W.GATES["result_exists"] is W.g_result_exists # the knowledge workflow doesn't use the gate
|
|
75
|
+
assert "result_exists" not in {st.gate for st in __import__("hypgraph").profiles.KNOWLEDGE2.workflow.stages}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Measuring returns isn't trading them, and a confidence interval that leaves out zero isn't an exclusion. A real-model
|
|
2
|
+
trading run (studies T3, a mean-reversion edge) lost eight writes to BACKTEST_WITHOUT_COSTS on a lag-1
|
|
3
|
+
autocorrelation, because the result also counted its returns (`n_returns`), and three claims to
|
|
4
|
+
EXCLUSION_WITHOUT_POWER for "the bootstrap 95% CI excludes zero" — which says the effect is there."""
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hypgraph import Graph, GraphError, Policy
|
|
8
|
+
from hypgraph import language as L
|
|
9
|
+
|
|
10
|
+
H = "harness"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _trading():
|
|
14
|
+
g = Graph(policy=Policy(reviewers={"critic"}, extra_reserved={H}))
|
|
15
|
+
inv = g.create(author=H, type="investigation", fields={"title": "Reversal.", "profile": "trading@1",
|
|
16
|
+
"objective": "Measure the reversal before backtesting it."})["id"]
|
|
17
|
+
q = g.create(author=H, type="question", inv=inv, code="Q1",
|
|
18
|
+
fields={"text": "Is the lag-1 autocorrelation of daily returns distinguishable from zero?"})["id"]
|
|
19
|
+
return g, inv, q
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _result(g, inv, values, summary="Lag-1 autocorrelation of daily returns, in sample."):
|
|
23
|
+
run = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "acf", "state": "succeeded"})
|
|
24
|
+
return g.create(author="quant", type="result", inv=inv, fields={"summary": summary, "values": values,
|
|
25
|
+
"outcome": "positive"}, links=[{"rel": "produced", "from": run["id"]}])["id"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("values", [
|
|
29
|
+
{"acf1_insample": {"value": -0.09995, "unit": "correlation"}, "insample_n_returns": {"value": 1499, "unit": "returns"}},
|
|
30
|
+
{"autocorr_lag1": {"value": -0.1008, "unit": "correlation"}, "n_return_obs_in": {"value": 1500, "unit": "returns"}},
|
|
31
|
+
{"acf1": {"value": -0.100, "unit": "acf"}, "N_returns_in_sample": {"value": 1500, "unit": "returns"},
|
|
32
|
+
"return_t_stat": {"value": -3.87}, "return_p_value": {"value": 0.0001}},
|
|
33
|
+
# the T3 re-run's own body: a standard deviation of returns beside the autocorrelation
|
|
34
|
+
{"in_sample_autocorr_lag1": {"value": -0.1, "unit": "correlation"}, "in_sample_sd_of_daily_return": {"value": 0.0101,
|
|
35
|
+
"unit": "return"}, "n_pairs": {"value": 1498, "unit": "pairs"}, "t_stat": {"value": -3.87, "unit": "t"}},
|
|
36
|
+
])
|
|
37
|
+
def test_a_statistic_and_a_count_of_returns_need_no_costs(values):
|
|
38
|
+
g, inv, _q = _trading()
|
|
39
|
+
_result(g, inv, values)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_a_return_earned_still_needs_costs():
|
|
43
|
+
g, inv, _q = _trading()
|
|
44
|
+
with pytest.raises(GraphError, match="BACKTEST_WITHOUT_COSTS"):
|
|
45
|
+
_result(g, inv, {"oos_return": {"value": 0.756}, "n_trades": {"value": 795, "unit": "trades"}},
|
|
46
|
+
summary="Out-of-sample return of the reversal rule.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("text", ["the bootstrap 95% CI excludes zero", "the interval excludes 0.0",
|
|
50
|
+
"the odds ratio's CI excludes 1", "a shuffle test rules out chance"])
|
|
51
|
+
def test_leaving_out_the_null_is_not_an_exclusion(text):
|
|
52
|
+
assert L.asserted(L.EXCLUSION, text) == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("text", ["the trial rules out an effect", "the data exclude a benefit", "there is no effect"])
|
|
56
|
+
def test_real_exclusions_still_count(text):
|
|
57
|
+
assert L.asserted(L.EXCLUSION, text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_a_claim_that_the_interval_excludes_zero_is_written():
|
|
61
|
+
g, inv, q = _trading()
|
|
62
|
+
rs = _result(g, inv, {"acf1": {"value": -0.100}, "boot_ci_lo": {"value": -0.144}, "boot_ci_hi": {"value": -0.048}})
|
|
63
|
+
g.create(author="quant", type="claim", inv=inv, fields={
|
|
64
|
+
"statement": "The lag-1 autocorrelation is -0.100 and its bootstrap 95% CI [-0.144, -0.048] excludes zero.",
|
|
65
|
+
"scope": "bars.csv, days 0-1499.",
|
|
66
|
+
"measurements": [{"name": "acf1", "key": "acf1", "result": rs}, {"name": "lo", "key": "boot_ci_lo", "result": rs},
|
|
67
|
+
{"name": "hi", "key": "boot_ci_hi", "result": rs}]},
|
|
68
|
+
links=[{"rel": "answers", "to": q}, {"rel": "derived_from", "to": rs}])
|
|
@@ -89,7 +89,7 @@ def test_scientific_stage_is_read_off_the_record():
|
|
|
89
89
|
"success_criteria": "every fixture parses"},
|
|
90
90
|
links=[{"rel": "tests", "to": h, "data": {"expected": "all 9 fixtures parse"}}])["id"]
|
|
91
91
|
assert _stage(g, q) == ("run", 4, False)
|
|
92
|
-
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "parse", "state": "succeeded"},
|
|
92
|
+
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "parse", "state": "succeeded", "config": {"source": "print('parse')"}},
|
|
93
93
|
links=[{"rel": "executes", "to": x}])["id"]
|
|
94
94
|
assert _stage(g, q) == ("evidenced", 5, False)
|
|
95
95
|
rs = g.create(author="w", type="result", inv=inv, fields={"summary": "Parsed 9 of 9 trailing-comma fixtures.",
|
|
@@ -192,7 +192,7 @@ def test_uses_is_at_risk_only_under_a_workflow():
|
|
|
192
192
|
for pid, expect in (("scientific@1", False), ("scientific@2", True)):
|
|
193
193
|
g, inv, q = _g(pid)
|
|
194
194
|
data = g.create(author="w", type="artifact", inv=inv, fields={"sha256": SHA_C, "path": "data.csv", "kind": "data"})["id"]
|
|
195
|
-
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "fit", "state": "succeeded"},
|
|
195
|
+
run = g.create(author="w", type="run", inv=inv, fields={"kind": "agent", "name": "fit", "state": "succeeded", "config": {"source": "print('fit')"}},
|
|
196
196
|
links=[{"rel": "uses", "to": data}])["id"]
|
|
197
197
|
rs = g.create(author="w", type="result", inv=inv, fields={"summary": "Fitted the model on the data file.",
|
|
198
198
|
"values": {"rmse": {"value": 0.41}}, "outcome": "positive"}, links=[{"rel": "produced", "from": run}])["id"]
|
|
@@ -205,7 +205,7 @@ def test_uses_is_at_risk_only_under_a_workflow():
|
|
|
205
205
|
# ── trading ────────────────────────────────────────────────────────
|
|
206
206
|
|
|
207
207
|
def _bt(g, inv, author, values, summary="Backtested the thesis out of sample.", q="", x=""):
|
|
208
|
-
r = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"},
|
|
208
|
+
r = g.create(author=author, type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}},
|
|
209
209
|
links=[{"rel": "executes", "to": x}] if x else None)["id"]
|
|
210
210
|
links = [{"rel": "produced", "from": r}] + ([{"rel": "part_of", "to": q}] if q else [])
|
|
211
211
|
return g.create(author=author, type="result", inv=inv, fields={"summary": summary, "values": values, "outcome": "positive"},
|
|
@@ -260,7 +260,7 @@ def test_trading_trials_positions_and_no_live_orders():
|
|
|
260
260
|
|
|
261
261
|
def test_valid_until_makes_a_result_stale():
|
|
262
262
|
g, inv, _q = _g("trading@2")
|
|
263
|
-
r = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"})["id"]
|
|
263
|
+
r = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}})["id"]
|
|
264
264
|
rs = g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtested the thesis out of sample.",
|
|
265
265
|
"values": {"oos_return": {"value": 0.08}, "fees": {"value": 0.002}}, "outcome": "positive",
|
|
266
266
|
"valid_until": "2020-06-30"}, links=[{"rel": "produced", "from": r}])["id"]
|
|
@@ -311,7 +311,7 @@ def test_optimization_instance_verifier_and_changed_instance():
|
|
|
311
311
|
inst = g.create(author="lead", type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "instances/40-site.json", "kind": "instance"},
|
|
312
312
|
links=[{"rel": "part_of", "to": q}])["id"]
|
|
313
313
|
assert _stage(g, q) == ("modelled", 2, False)
|
|
314
|
-
run = g.create(author="solver", type="run", inv=inv, fields={"kind": "agent", "name": "cbc", "state": "succeeded"},
|
|
314
|
+
run = g.create(author="solver", type="run", inv=inv, fields={"kind": "agent", "name": "cbc", "state": "succeeded", "config": {"source": "print('cbc')"}},
|
|
315
315
|
links=[{"rel": "uses", "to": inst}])["id"]
|
|
316
316
|
rs = g.create(author="solver", type="result", inv=inv, fields={"summary": "Solved the model to a 0.0 gap.",
|
|
317
317
|
"values": {"objective": {"value": 412.0}, "feasible": {"value": True}, "gap": {"value": 0.0}}, "outcome": "positive"},
|
|
@@ -508,7 +508,7 @@ def test_an_edit_elsewhere_on_the_page_keeps_the_source_fresh():
|
|
|
508
508
|
def test_a_decision_on_stale_evidence_and_a_check_of_a_stale_solve_do_not_count():
|
|
509
509
|
g, inv, q = _g("trading@2", reviewers=("risk",))
|
|
510
510
|
data = g.create(author="quant", type="artifact", inv=inv, fields={"sha256": SHA_A, "path": "prices.csv"})["id"]
|
|
511
|
-
run_ = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded"},
|
|
511
|
+
run_ = g.create(author="quant", type="run", inv=inv, fields={"kind": "agent", "name": "backtest", "state": "succeeded", "config": {"source": "print('backtest')"}},
|
|
512
512
|
links=[{"rel": "uses", "to": data}])["id"]
|
|
513
513
|
rs = g.create(author="quant", type="result", inv=inv, fields={"summary": "Backtested the thesis out of sample.",
|
|
514
514
|
"values": {"oos_return": {"value": 0.08}, "fees": {"value": 0.002}, "trials": {"value": 1}}, "outcome": "positive"},
|
|
@@ -526,7 +526,7 @@ def test_a_decision_on_stale_evidence_and_a_check_of_a_stale_solve_do_not_count(
|
|
|
526
526
|
g2, inv2, q2 = _g("optimization@2", reviewers=("verifier",))
|
|
527
527
|
inst = g2.create(author="lead", type="artifact", inv=inv2, fields={"sha256": SHA_A, "path": "instance.csv", "kind": "instance"},
|
|
528
528
|
links=[{"rel": "part_of", "to": q2}])["id"]
|
|
529
|
-
r2 = g2.create(author="solver", type="run", inv=inv2, fields={"kind": "agent", "name": "solve", "state": "succeeded"},
|
|
529
|
+
r2 = g2.create(author="solver", type="run", inv=inv2, fields={"kind": "agent", "name": "solve", "state": "succeeded", "config": {"source": "print('solve')"}},
|
|
530
530
|
links=[{"rel": "uses", "to": inst}])["id"]
|
|
531
531
|
s2 = g2.create(author="solver", type="result", inv=inv2, fields={"summary": "Enumerated the instance.",
|
|
532
532
|
"values": {"objective": {"value": 40}, "feasible": {"value": True}, "gap": {"value": 0.0}}, "outcome": "positive"},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|