diff --git a/synth_client/client.py b/synth_client/client.py index 7954178..15ed20f 100644 --- a/synth_client/client.py +++ b/synth_client/client.py @@ -107,6 +107,51 @@ def _get(self, path: str, mock_path_parts: list[str], params: dict | None = None return self._load_mock(*mock_path_parts) return self._request(path, params) + # ─── Asset Summary Helper ──────────────────────────────────────── + + def get_asset_summary(self, asset: str, horizon: str = "24h") -> dict: + """ + Lightweight helper to expose the latest forecast snapshot for an asset. + + This is primarily used by downstream tools (e.g. Tide Chart gTrade + integration) that only need a current price and basic forecast context, + without re‑implementing Synth API plumbing. + + Args: + asset: Asset symbol (must be one of SUPPORTED_ASSETS) + horizon: Forecast horizon — "1h" or "24h" (default: "24h") + + Returns: + Dict with at least: + - asset + - horizon + - current_price + - forecast_future (if available from prediction percentiles) + + Raises: + ValueError: If asset or horizon is not supported. + FileNotFoundError / requests.HTTPError: Propagated from underlying + forecast fetch when data is unavailable. + """ + if asset not in SUPPORTED_ASSETS: + raise ValueError(f"Unsupported asset: {asset}. Supported: {SUPPORTED_ASSETS}") + if horizon not in SUPPORTED_HORIZONS: + raise ValueError( + f"Unsupported horizon: {horizon}. Supported: {SUPPORTED_HORIZONS}" + ) + + pct = self.get_prediction_percentiles(asset, horizon=horizon) + summary: dict = { + "asset": asset, + "horizon": horizon, + "current_price": pct.get("current_price"), + } + + if "forecast_future" in pct: + summary["forecast_future"] = pct["forecast_future"] + + return summary + # ─── Prediction Percentiles ────────────────────────────────────── def get_prediction_percentiles(self, asset: str, horizon: str = "24h") -> dict: diff --git a/tests/test_synth_client.py b/tests/test_synth_client.py index c4ee926..7b07194 100644 --- a/tests/test_synth_client.py +++ b/tests/test_synth_client.py @@ -128,3 +128,25 @@ def test_leaderboard(client, asset): if len(data) > 0: assert "neuron_uid" in data[0] assert "rewards" in data[0] + + +def test_get_asset_summary_returns_current_price(client): + summary = client.get_asset_summary("BTC", horizon="24h") + assert summary["asset"] == "BTC" + assert summary["horizon"] == "24h" + assert "current_price" in summary + assert isinstance(summary["current_price"], (int, float)) + + +def test_get_asset_summary_rejects_unsupported_asset(client): + import pytest + + with pytest.raises(ValueError): + client.get_asset_summary("DOGE", horizon="24h") + + +def test_get_asset_summary_rejects_unsupported_horizon(client): + import pytest + + with pytest.raises(ValueError): + client.get_asset_summary("BTC", horizon="7d") diff --git a/tools/tide-chart/main.py b/tools/tide-chart/main.py index 84b518e..6b873ec 100644 --- a/tools/tide-chart/main.py +++ b/tools/tide-chart/main.py @@ -1048,15 +1048,25 @@ def gtrade_resolve_pair(): except Exception: trading_vars = None pair_index = resolve_pair_index(asset, trading_vars, skip_fetch=True) - # Include fresh price for the frontend openTrade struct + # Include fresh price for the frontend openTrade struct. + # Use SynthClient's forecast percentiles helper instead of a hard-coded, + # non-existent client method to keep this logic aligned with the SDK. current_price = None try: - summary = client.get_asset_summary(asset) - if summary and "current_price" in summary: - current_price = summary["current_price"] + summary = client.get_asset_summary(asset, horizon="24h") + current_price = summary.get("current_price") except Exception: - pass - return jsonify({"asset": asset, "pair_index": pair_index, "current_price": current_price}) + # If Synth data is temporarily unavailable, still return a valid + # response shape so the frontend can fall back gracefully. + current_price = None + + return jsonify( + { + "asset": asset, + "pair_index": pair_index, + "current_price": current_price, + } + ) @app.route("/api/gtrade/open-trades") def gtrade_open_trades(): diff --git a/tools/tide-chart/tests/test_tool.py b/tools/tide-chart/tests/test_tool.py index 92f76c4..c1005d7 100644 --- a/tools/tide-chart/tests/test_tool.py +++ b/tools/tide-chart/tests/test_tool.py @@ -753,7 +753,7 @@ def test_flask_gtrade_resolve_pair_invalid(): def test_flask_gtrade_resolve_pair_valid(): - """Verify resolve-pair returns for a valid asset (pair_index may be None without live API).""" + """Verify resolve-pair returns for a valid asset and includes current_price when Synth data is available.""" client = _make_client() app = create_app(client) with app.test_client() as tc: @@ -762,6 +762,10 @@ def test_flask_gtrade_resolve_pair_valid(): data = json.loads(resp.data) assert data["asset"] == "SPY" assert "pair_index" in data + # In mock mode, SynthClient is backed by local prediction percentiles, + # so current_price should be present and numeric. + assert "current_price" in data + assert isinstance(data["current_price"], (int, float)) def test_dashboard_html_contains_wallet_ui():