chore: remove dead functions and write-only state (no behaviour change)
Zero callers anywhere (tests included), verified by grep/AST: - dashboard_publisher.DashboardPublisher.publish_all (run_cycle inlines both publishes) - signal_log.SignalLogger.advance_with_bars (the backfill script uses backfill_seeds / resolve_from_store / reseed_unresolvable) and the never-read SignalLogger.path property plus its write-only self._path - llm_client.reset_llm_client (tests reset the singleton by assignment) - market_outlook._last_run_ts, residue of the old "now - last < 3600" dedup; _last_run_hour (the UTC hour bucket) is the live key - news_engine FundamentalContext.event_impact_on_pairs: written once, read by nothing; its only feeder NewsEngine._get_affected_pairs and that helper's unit test go with it - position_sizing.LIVE_DEPTH_BARS (referenced only by its own docstring, which now states the depths inline), the unused Iterable import and the unused atr_ok parameter of _fill_geometry - analyze_signal_log: unused `field` import and the unused `rows` parameter of persona_scorecard / news_outcome_buckets (both read the DB directly) The two backfill/label scripts' --db help text now names the config.json setting it actually falls back to. Suite: 463 passed, 0 failed (one test removed with its subject). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
This commit is contained in:
@@ -67,7 +67,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional, Sequence, Tuple
|
from typing import Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
@@ -754,8 +754,7 @@ def _fmt_r(r: Optional[float]) -> str:
|
|||||||
return " n/a " if r is None else f"{r:+.2f}R"
|
return " n/a " if r is None else f"{r:+.2f}R"
|
||||||
|
|
||||||
|
|
||||||
def persona_scorecard(rows: Sequence[LogRow], events: Sequence[SignalEvent],
|
def persona_scorecard(events: Sequence[SignalEvent], db_path: str) -> List[dict]:
|
||||||
db_path: str) -> List[dict]:
|
|
||||||
"""Per persona × stance: n, mean R and win rate of the underlying
|
"""Per persona × stance: n, mean R and win rate of the underlying
|
||||||
event (needs panel_takes_json — read straight from the DB)."""
|
event (needs panel_takes_json — read straight from the DB)."""
|
||||||
by_row = _row_index(events)
|
by_row = _row_index(events)
|
||||||
@@ -921,8 +920,7 @@ def news_extremes(rows: Sequence[LogRow], streams: Dict[str, List[dict]],
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def news_outcome_buckets(rows: Sequence[LogRow],
|
def news_outcome_buckets(events: Sequence[SignalEvent],
|
||||||
events: Sequence[SignalEvent],
|
|
||||||
db_path: str) -> List[dict]:
|
db_path: str) -> List[dict]:
|
||||||
"""Resolved events bucketed by (direction, news bucket, macro stance).
|
"""Resolved events bucketed by (direction, news bucket, macro stance).
|
||||||
|
|
||||||
@@ -1306,13 +1304,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
print_report(rows, events, panel, hit)
|
print_report(rows, events, panel, hit)
|
||||||
print_stored_books(events)
|
print_stored_books(events)
|
||||||
print("\n── Persona scorecard (stance × event outcome) " + "─" * 22)
|
print("\n── Persona scorecard (stance × event outcome) " + "─" * 22)
|
||||||
for p in persona_scorecard(rows, events, args.db):
|
for p in persona_scorecard(events, args.db):
|
||||||
print(f" {p['persona']:11} {p['stance']:8} n={p['n']:3d} "
|
print(f" {p['persona']:11} {p['stance']:8} n={p['n']:3d} "
|
||||||
f"mean {p['mean_r']:+.3f}R win {100 * p['win_rate']:.0f}%")
|
f"mean {p['mean_r']:+.3f}R win {100 * p['win_rate']:.0f}%")
|
||||||
if args.news:
|
if args.news:
|
||||||
print_news(news_lead_lag(rows, streams),
|
print_news(news_lead_lag(rows, streams),
|
||||||
news_extremes(rows, streams),
|
news_extremes(rows, streams),
|
||||||
news_outcome_buckets(rows, events, args.db))
|
news_outcome_buckets(events, args.db))
|
||||||
print_parity(parity_report(rows))
|
print_parity(parity_report(rows))
|
||||||
if args.csv:
|
if args.csv:
|
||||||
export_csv(events, args.csv)
|
export_csv(events, args.csv)
|
||||||
|
|||||||
@@ -432,10 +432,6 @@ class DashboardPublisher:
|
|||||||
if self.news_payload is not None:
|
if self.news_payload is not None:
|
||||||
publish(self.news_payload, os.path.join(self.out_dir, NEWS_FILE))
|
publish(self.news_payload, os.path.join(self.out_dir, NEWS_FILE))
|
||||||
|
|
||||||
def publish_all(self) -> None:
|
|
||||||
self.publish_snapshot()
|
|
||||||
self.publish_news()
|
|
||||||
|
|
||||||
def run_cycle(self, tfs: List[Timeframe]) -> None:
|
def run_cycle(self, tfs: List[Timeframe]) -> None:
|
||||||
for tf in tfs: # sequential — see module docstring (cache dedup)
|
for tf in tfs: # sequential — see module docstring (cache dedup)
|
||||||
self.scan_tf(tf)
|
self.scan_tf(tf)
|
||||||
|
|||||||
@@ -198,10 +198,3 @@ def get_llm_client() -> DeepSeekClient:
|
|||||||
enabled=settings.get("enabled", True) is not False,
|
enabled=settings.get("enabled", True) is not False,
|
||||||
)
|
)
|
||||||
return _client
|
return _client
|
||||||
|
|
||||||
|
|
||||||
def reset_llm_client() -> None:
|
|
||||||
"""Drop the singleton (tests / config reloads)."""
|
|
||||||
global _client
|
|
||||||
with _client_lock:
|
|
||||||
_client = None
|
|
||||||
|
|||||||
+2
-5
@@ -329,7 +329,6 @@ def _collect_payload(settings: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
# ── Orchestration, hourly dedup, and request budget ──────────────────
|
# ── Orchestration, hourly dedup, and request budget ──────────────────
|
||||||
|
|
||||||
_state_lock = threading.Lock()
|
_state_lock = threading.Lock()
|
||||||
_last_run_ts: Optional[float] = None
|
|
||||||
_last_run_hour: Optional[int] = None # UTC hour bucket of the last attempt
|
_last_run_hour: Optional[int] = None # UTC hour bucket of the last attempt
|
||||||
_outlook_day: Optional[_dt.date] = None
|
_outlook_day: Optional[_dt.date] = None
|
||||||
_outlook_count = 0
|
_outlook_count = 0
|
||||||
@@ -344,10 +343,9 @@ def _skip(reason: str) -> None:
|
|||||||
|
|
||||||
def reset_outlook_state() -> None:
|
def reset_outlook_state() -> None:
|
||||||
"""Drop hourly and daily state (tests / deployment config reloads)."""
|
"""Drop hourly and daily state (tests / deployment config reloads)."""
|
||||||
global _last_run_ts, _last_run_hour, _outlook_day, _outlook_count
|
global _last_run_hour, _outlook_day, _outlook_count
|
||||||
global _warned_budget
|
global _warned_budget
|
||||||
with _state_lock:
|
with _state_lock:
|
||||||
_last_run_ts = None
|
|
||||||
_last_run_hour = None
|
_last_run_hour = None
|
||||||
_outlook_day = None
|
_outlook_day = None
|
||||||
_outlook_count = 0
|
_outlook_count = 0
|
||||||
@@ -360,7 +358,7 @@ def _begin_attempt(cap: int, force: bool) -> bool:
|
|||||||
Hourly dedup is by UTC hour BUCKET, not ``now - last < 3600``: the
|
Hourly dedup is by UTC hour BUCKET, not ``now - last < 3600``: the
|
||||||
H1 loop calls this at boundary+10 s+scan time, so consecutive hours
|
H1 loop calls this at boundary+10 s+scan time, so consecutive hours
|
||||||
were routinely 3,590 s apart and every other hour was dropped."""
|
were routinely 3,590 s apart and every other hour was dropped."""
|
||||||
global _last_run_ts, _last_run_hour, _outlook_day, _outlook_count
|
global _last_run_hour, _outlook_day, _outlook_count
|
||||||
global _warned_budget
|
global _warned_budget
|
||||||
now = time.time()
|
now = time.time()
|
||||||
hour_bucket = int(now // 3600)
|
hour_bucket = int(now // 3600)
|
||||||
@@ -383,7 +381,6 @@ def _begin_attempt(cap: int, force: bool) -> bool:
|
|||||||
_skip("daily budget exhausted")
|
_skip("daily budget exhausted")
|
||||||
return False
|
return False
|
||||||
_outlook_count += 1
|
_outlook_count += 1
|
||||||
_last_run_ts = now
|
|
||||||
_last_run_hour = hour_bucket
|
_last_run_hour = hour_bucket
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ class FundamentalContext:
|
|||||||
warning_message: str = ""
|
warning_message: str = ""
|
||||||
news_sentiment: Dict[str, float] = field(default_factory=dict) # currency -> sentiment
|
news_sentiment: Dict[str, float] = field(default_factory=dict) # currency -> sentiment
|
||||||
news_headlines: List[NewsSentiment] = field(default_factory=list)
|
news_headlines: List[NewsSentiment] = field(default_factory=list)
|
||||||
event_impact_on_pairs: Dict[str, str] = field(default_factory=dict)
|
|
||||||
calendar_available: bool = False # True if live calendar data was fetched
|
calendar_available: bool = False # True if live calendar data was fetched
|
||||||
llm_summary: str = "" # DeepSeek market-context blurb (display-only)
|
llm_summary: str = "" # DeepSeek market-context blurb (display-only)
|
||||||
|
|
||||||
@@ -415,15 +414,6 @@ class NewsEngine:
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Map upcoming high-impact events to affected pairs
|
|
||||||
for event in context.events_upcoming_1h:
|
|
||||||
if event.is_high_impact:
|
|
||||||
affected = self._get_affected_pairs(event.currency)
|
|
||||||
for pair in affected:
|
|
||||||
context.event_impact_on_pairs[pair] = (
|
|
||||||
f"{event.currency} {event.event} in {event.minutes_until:.0f}min"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Economic calendar fetch failed: {e}")
|
logger.warning(f"Economic calendar fetch failed: {e}")
|
||||||
|
|
||||||
@@ -1149,11 +1139,3 @@ class NewsEngine:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_affected_pairs(currency: str) -> List[str]:
|
|
||||||
_MAP = {
|
|
||||||
"USD": ["XAU/USD"],
|
|
||||||
"XAU": ["XAU/USD"],
|
|
||||||
}
|
|
||||||
return _MAP.get(currency, [])
|
|
||||||
|
|||||||
+8
-19
@@ -10,11 +10,11 @@ inputs (rule 4: live == backtest parity).
|
|||||||
Design contract
|
Design contract
|
||||||
---------------
|
---------------
|
||||||
* **Bounded lookbacks.** No feature may read further back than the live
|
* **Bounded lookbacks.** No feature may read further back than the live
|
||||||
fetch depth of its timeframe (``LIVE_DEPTH_BARS``). The primary frame
|
fetch depth of its timeframe (5000 bars on M5/M15, 1460 on H1, 380 on
|
||||||
is hard-capped at ``MAX_LOOKBACK_BARS`` rows, confirmation frames at
|
H4). The primary frame is hard-capped at ``MAX_LOOKBACK_BARS`` rows,
|
||||||
60 rows, and the burst scan at 200 rows, so a feature computed on a
|
confirmation frames at 60 rows, and the burst scan at 200 rows, so a
|
||||||
full backtest frame equals the same feature computed on the shorter
|
feature computed on a full backtest frame equals the same feature
|
||||||
live tail.
|
computed on the shorter live tail.
|
||||||
* **Closed bars only.** The frames handed in are exactly the frames
|
* **Closed bars only.** The frames handed in are exactly the frames
|
||||||
``analyze_from_dataframes`` received: UTC tz-aware, oldest-first,
|
``analyze_from_dataframes`` received: UTC tz-aware, oldest-first,
|
||||||
capitalized OHLCV columns, forming candle already dropped. The last
|
capitalized OHLCV columns, forming candle already dropped. The last
|
||||||
@@ -46,7 +46,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Iterable, Optional, Sequence, Tuple
|
from typing import Dict, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -60,17 +60,6 @@ logger = logging.getLogger(__name__)
|
|||||||
#: A trained artifact carrying a different value is ignored.
|
#: A trained artifact carrying a different value is ignored.
|
||||||
FEATURE_VERSION = 1
|
FEATURE_VERSION = 1
|
||||||
|
|
||||||
#: Bars each timeframe actually holds in LIVE mode. Any feature whose
|
|
||||||
#: lookback exceeds its timeframe's depth here would be unreproducible
|
|
||||||
#: live, so the windows below stay far inside these numbers.
|
|
||||||
LIVE_DEPTH_BARS: Dict[Timeframe, int] = {
|
|
||||||
Timeframe.M5: 5000,
|
|
||||||
Timeframe.M15: 5000,
|
|
||||||
Timeframe.H1: 1460,
|
|
||||||
Timeframe.H4: 380,
|
|
||||||
Timeframe.D1: 300,
|
|
||||||
}
|
|
||||||
|
|
||||||
#: Hard cap on any primary-timeframe window used in this module.
|
#: Hard cap on any primary-timeframe window used in this module.
|
||||||
MAX_LOOKBACK_BARS = 500
|
MAX_LOOKBACK_BARS = 500
|
||||||
|
|
||||||
@@ -338,7 +327,7 @@ def _fill_time(out: Dict[str, float], dt: pd.Timestamp) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _fill_geometry(out: Dict[str, float], P: pd.DataFrame, setup,
|
def _fill_geometry(out: Dict[str, float], P: pd.DataFrame, setup,
|
||||||
atr: float, atr_ok: bool, norm, d: float) -> None:
|
atr: float, norm, d: float) -> None:
|
||||||
out["f_atr"] = max(0.0, _finite(atr))
|
out["f_atr"] = max(0.0, _finite(atr))
|
||||||
ranges = (P["High"] - P["Low"]).astype(float)
|
ranges = (P["High"] - P["Low"]).astype(float)
|
||||||
last_range = _finite(ranges.iloc[-1])
|
last_range = _finite(ranges.iloc[-1])
|
||||||
@@ -566,7 +555,7 @@ def build_features(
|
|||||||
|
|
||||||
for step, args in (
|
for step, args in (
|
||||||
(_fill_time, (out, decision_time(P, primary_tf))),
|
(_fill_time, (out, decision_time(P, primary_tf))),
|
||||||
(_fill_geometry, (out, P, setup, atr, atr_ok, norm, d)),
|
(_fill_geometry, (out, P, setup, atr, norm, d)),
|
||||||
(_fill_exhaustion, (out, report, KL, px, d, norm)),
|
(_fill_exhaustion, (out, report, KL, px, d, norm)),
|
||||||
(_fill_quality, (out, report, setup, IND, d)),
|
(_fill_quality, (out, report, setup, IND, d)),
|
||||||
(_fill_mtf, (out, report, primary_tf, confirmation_tfs, d)),
|
(_fill_mtf, (out, report, primary_tf, confirmation_tfs, d)),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def _status_counts(con: sqlite3.Connection) -> dict:
|
|||||||
|
|
||||||
def main(argv=None) -> int:
|
def main(argv=None) -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
||||||
ap.add_argument("--db", default=None, help="defaults to signal_log.path")
|
ap.add_argument("--db", default=None, help="defaults to the signal_log.path setting in config.json")
|
||||||
ap.add_argument("--dry-run", action="store_true")
|
ap.add_argument("--dry-run", action="store_true")
|
||||||
ap.add_argument("--reseed-unresolvable", action="store_true")
|
ap.add_argument("--reseed-unresolvable", action="store_true")
|
||||||
ap.add_argument("--symbol", default="XAU/USD")
|
ap.add_argument("--symbol", default="XAU/USD")
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def _counts(con: sqlite3.Connection, symbol: str,
|
|||||||
|
|
||||||
def main(argv=None) -> int:
|
def main(argv=None) -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[1])
|
||||||
ap.add_argument("--db", default=None, help="defaults to signal_log.path")
|
ap.add_argument("--db", default=None, help="defaults to the signal_log.path setting in config.json")
|
||||||
ap.add_argument("--dry-run", action="store_true",
|
ap.add_argument("--dry-run", action="store_true",
|
||||||
help="read-only candidate counts, writes nothing")
|
help="read-only candidate counts, writes nothing")
|
||||||
ap.add_argument("--budget", type=int, default=100000,
|
ap.add_argument("--budget", type=int, default=100000,
|
||||||
|
|||||||
@@ -806,7 +806,6 @@ class SignalLogger:
|
|||||||
the daemon's thread pool."""
|
the daemon's thread pool."""
|
||||||
|
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str):
|
||||||
self._path = path
|
|
||||||
parent = os.path.dirname(path)
|
parent = os.path.dirname(path)
|
||||||
if parent:
|
if parent:
|
||||||
os.makedirs(parent, exist_ok=True)
|
os.makedirs(parent, exist_ok=True)
|
||||||
@@ -860,10 +859,6 @@ class SignalLogger:
|
|||||||
raise
|
raise
|
||||||
time.sleep(delay * (attempt + 1))
|
time.sleep(delay * (attempt + 1))
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> str:
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
def _migrate_signal_columns(self) -> None:
|
def _migrate_signal_columns(self) -> None:
|
||||||
"""Add newer signal columns to pre-existing tables (idempotent)."""
|
"""Add newer signal columns to pre-existing tables (idempotent)."""
|
||||||
existing = {r[1] for r in self._conn.execute("PRAGMA table_info(signals)")}
|
existing = {r[1] for r in self._conn.execute("PRAGMA table_info(signals)")}
|
||||||
@@ -1291,27 +1286,6 @@ class SignalLogger:
|
|||||||
rowid, exc_info=True)
|
rowid, exc_info=True)
|
||||||
return written
|
return written
|
||||||
|
|
||||||
def advance_with_bars(self, rowid: int, bars: Iterable[Dict[str, Any]],
|
|
||||||
evidence: str = "external_bars") -> str:
|
|
||||||
"""Advance ONE pending outcome from caller-supplied bars (backfill
|
|
||||||
from provider history). Returns the resulting status."""
|
|
||||||
with self._lock:
|
|
||||||
row = self._conn.execute(
|
|
||||||
"SELECT advisory_outcome_status, advisory_outcome_json"
|
|
||||||
" FROM signals WHERE id=?", (rowid,)).fetchone()
|
|
||||||
if row is None or row[0] != "PENDING" or not row[1]:
|
|
||||||
return str(row[0]) if row else "MISSING"
|
|
||||||
state = advance_outcome(json.loads(row[1]), list(bars))
|
|
||||||
state["evidence"] = evidence
|
|
||||||
status = state.get("status", "UNRESOLVABLE")
|
|
||||||
self._conn.execute(
|
|
||||||
"UPDATE signals SET advisory_outcome_status=?,"
|
|
||||||
" advisory_r_multiple=?, advisory_outcome_json=?"
|
|
||||||
" WHERE id=? AND advisory_outcome_status='PENDING'",
|
|
||||||
(status, canonical_r(state), _dumps(state), rowid))
|
|
||||||
self._conn.commit()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def backfill_seeds(self, limit: Optional[int] = None) -> Dict[str, int]:
|
def backfill_seeds(self, limit: Optional[int] = None) -> Dict[str, int]:
|
||||||
"""Assign setup_key / advisory_event_id to legacy setup rows and
|
"""Assign setup_key / advisory_event_id to legacy setup rows and
|
||||||
seed the canonical ones that carry no outcome yet. Existing
|
seed the canonical ones that carry no outcome yet. Existing
|
||||||
|
|||||||
+2
-14
@@ -881,18 +881,6 @@ def _():
|
|||||||
assert NewsEngine._parse_rss_date("not-a-date") is None
|
assert NewsEngine._parse_rss_date("not-a-date") is None
|
||||||
|
|
||||||
|
|
||||||
@test("NewsEngine._get_affected_pairs")
|
|
||||||
def _():
|
|
||||||
pairs = NewsEngine._get_affected_pairs("USD")
|
|
||||||
assert "XAU/USD" in pairs
|
|
||||||
|
|
||||||
pairs_xau = NewsEngine._get_affected_pairs("XAU")
|
|
||||||
assert "XAU/USD" in pairs_xau
|
|
||||||
|
|
||||||
pairs_unknown = NewsEngine._get_affected_pairs("ARS")
|
|
||||||
assert pairs_unknown == []
|
|
||||||
|
|
||||||
|
|
||||||
@test("EconomicEvent properties")
|
@test("EconomicEvent properties")
|
||||||
def _():
|
def _():
|
||||||
future_time = datetime.now(timezone.utc) + timedelta(minutes=30)
|
future_time = datetime.now(timezone.utc) + timedelta(minutes=30)
|
||||||
@@ -8174,7 +8162,7 @@ def _():
|
|||||||
"stance": "support"}])))
|
"stance": "support"}])))
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
buckets = asl.news_outcome_buckets(rows, events, path)
|
buckets = asl.news_outcome_buckets(events, path)
|
||||||
finally:
|
finally:
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
assert sum(b["n"] for b in buckets) == 3
|
assert sum(b["n"] for b in buckets) == 3
|
||||||
@@ -9117,7 +9105,7 @@ def _():
|
|||||||
ev = _types.SimpleNamespace(
|
ev = _types.SimpleNamespace(
|
||||||
rows=[_types.SimpleNamespace(id=1), _types.SimpleNamespace(id=2)],
|
rows=[_types.SimpleNamespace(id=1), _types.SimpleNamespace(id=2)],
|
||||||
r_headline=2.0)
|
r_headline=2.0)
|
||||||
out = asl.persona_scorecard([], [ev], path)
|
out = asl.persona_scorecard([ev], path)
|
||||||
assert len(out) == 1, out
|
assert len(out) == 1, out
|
||||||
assert out[0]["n"] == 1, f"two rows of ONE event must count once: {out}"
|
assert out[0]["n"] == 1, f"two rows of ONE event must count once: {out}"
|
||||||
assert out[0]["mean_r"] == 2.0
|
assert out[0]["mean_r"] == 2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user