Spaces:
Running
Running
Commit ·
9e26574
1
Parent(s): c4eac91
Strategy Fix Deploy A: confidence tiering, entry-momentum readout, dynamic TP, synthetic-instrument filter
Browse files- F1a: Telegram alerts tier 'TRADE-GRADE' (conf>=9.0) vs 'WATCH ONLY' (8.8-9.0); alert threshold and dedup unchanged
- F2a/2b: card.entry_momentum (m5/m15 ROC%, aligned) - zero extra API calls; alert line shows 15m momentum + alignment
- F3a: card.levels.tp_dyn additive field (0.7R via TP_DYN_R env); alert line under TP3; TP1/2/3 unchanged
- F5b: exclude BingX synthetic/CFD instruments (oil, forex, metals, index proxies) from scan universe; /api/scan reports synthetics_excluded count
Source: analysis/signal_verdict.md (n=40, Jul 8-11) + automation-docs/STRATEGY-FIX-PLAN.md
All additive/display-only, zero change to scoring weights or trading logic.
- main.py +37 -7
- scorer.py +46 -1
- telegram_alert.py +31 -0
main.py
CHANGED
|
@@ -9,7 +9,7 @@ Endpoints:
|
|
| 9 |
All computation is deterministic — no AI in the data path.
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
-
import math, statistics, time, os
|
| 13 |
from pathlib import Path
|
| 14 |
from functools import lru_cache
|
| 15 |
from datetime import datetime, timezone
|
|
@@ -37,6 +37,28 @@ logger = logging.getLogger("main")
|
|
| 37 |
|
| 38 |
ALERT_INTERVAL = 300 # 5 minutes
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
async def _alert_loop():
|
| 41 |
"""Background task: scan every 5 min and fire Telegram alerts for high-confidence cards."""
|
| 42 |
from telegram_alert import send_alert, prune_sent_cache, send_raw_message
|
|
@@ -56,7 +78,7 @@ async def _alert_loop():
|
|
| 56 |
try:
|
| 57 |
src = get_source()
|
| 58 |
secondary = get_secondary()
|
| 59 |
-
candidates = rank_universe(src, src.min_vol, 30)
|
| 60 |
for c in candidates:
|
| 61 |
try:
|
| 62 |
card = score_symbol(src, c["symbol"], alert_account, 1.0,
|
|
@@ -143,20 +165,27 @@ def get_secondary():
|
|
| 143 |
return _binance if _binance_ok else None
|
| 144 |
|
| 145 |
|
| 146 |
-
def rank_universe(src, min_volume: float, top_n: int) -> list[dict]:
|
|
|
|
| 147 |
perps = src.universe()
|
| 148 |
rows = []
|
|
|
|
| 149 |
for t in src.tickers():
|
| 150 |
if t["symbol"] not in perps:
|
| 151 |
continue
|
|
|
|
|
|
|
|
|
|
| 152 |
qv = t["quote_volume_usd"]
|
| 153 |
lo, hi = t.get("low", 0), t.get("high", 0)
|
| 154 |
if qv < min_volume or lo <= 0:
|
| 155 |
continue
|
| 156 |
t["range_24h_pct"] = (hi - lo) / lo * 100
|
| 157 |
rows.append(t)
|
|
|
|
|
|
|
| 158 |
if len(rows) < 3:
|
| 159 |
-
return [{"symbol": s} for s in src.anchors]
|
| 160 |
|
| 161 |
log_vols = [math.log(r["quote_volume_usd"]) for r in rows]
|
| 162 |
ranges = [r["range_24h_pct"] for r in rows]
|
|
@@ -174,7 +203,7 @@ def rank_universe(src, min_volume: float, top_n: int) -> list[dict]:
|
|
| 174 |
if extra:
|
| 175 |
extra["anchor"] = True
|
| 176 |
picked.append(extra)
|
| 177 |
-
return picked
|
| 178 |
|
| 179 |
|
| 180 |
# ── API routes ─────────────────────────────────────────────────────────────
|
|
@@ -196,7 +225,7 @@ def scan(top: int = 8, min_volume: float = None, account: float = 100000,
|
|
| 196 |
try:
|
| 197 |
src = get_source()
|
| 198 |
floor = min_volume or src.min_vol
|
| 199 |
-
candidates = rank_universe(src, floor, top)
|
| 200 |
secondary = get_secondary()
|
| 201 |
cards = []
|
| 202 |
for c in candidates:
|
|
@@ -229,7 +258,8 @@ def scan(top: int = 8, min_volume: float = None, account: float = 100000,
|
|
| 229 |
cards.append({"symbol": c["symbol"], "error": str(e)[:120]})
|
| 230 |
cards.sort(key=lambda c: c.get("confidence", 0), reverse=True)
|
| 231 |
return {"fetched_at": datetime.now(timezone.utc).isoformat(),
|
| 232 |
-
"source": src.name, "market": src.market_note, "cards": cards
|
|
|
|
| 233 |
except Exception as e:
|
| 234 |
raise HTTPException(503, str(e))
|
| 235 |
|
|
|
|
| 9 |
All computation is deterministic — no AI in the data path.
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
+
import math, statistics, time, os, re
|
| 13 |
from pathlib import Path
|
| 14 |
from functools import lru_cache
|
| 15 |
from datetime import datetime, timezone
|
|
|
|
| 37 |
|
| 38 |
ALERT_INTERVAL = 300 # 5 minutes
|
| 39 |
|
| 40 |
+
# ── Synthetic-instrument hygiene (Strategy Fix Plan F5b) ───────────────────
|
| 41 |
+
# BingX lists non-crypto CFD proxies (oil, forex, metals, index futures) on the
|
| 42 |
+
# same USDT-M perpetual endpoint as real crypto. 10/50 alerts in the Jul 8-11
|
| 43 |
+
# batch were these — unanalyzable (no independent data source) and off-mission.
|
| 44 |
+
# Patterns below are derived from the 7 confirmed synthetic symbols found in
|
| 45 |
+
# analysis/replay_results.csv (NO_DATA rows), not guessed:
|
| 46 |
+
# CO1OILBRENT2USD-USDT, NCCO1OILWTI2USD-USDT, NCCOGOLD2USD-USDT,
|
| 47 |
+
# NCCOXAG2USD-USDT, NCFXGBP2JPY-USDT, NCSISP5002USD-USDT, SINASDAQ1002USD-USDT
|
| 48 |
+
# The 3rd pattern (digit + 3-letter fake-FX code immediately before "-USDT")
|
| 49 |
+
# alone catches all 7 observed cases and is low-false-positive: real crypto
|
| 50 |
+
# tickers (BTC-USDT, 1000PEPE-USDT, etc.) never end in <digit><3 letters>-USDT.
|
| 51 |
+
EXCLUDE_SYNTHETICS = os.environ.get("EXCLUDE_SYNTHETICS", "true").lower() == "true"
|
| 52 |
+
_SYNTHETIC_PATTERNS = [
|
| 53 |
+
re.compile(r"^(CO1|NCFX|NCS|NCCO|SIN)", re.IGNORECASE), # known prefixes
|
| 54 |
+
re.compile(r"(XAU|XAG|NGAS|NATGAS|BRENT|WTI|GOLD|SILVER|NASDAQ|SP500)", re.IGNORECASE), # commodity/index keywords
|
| 55 |
+
re.compile(r"\d[A-Z]{3}-USDT$"), # embedded fake-FX quote suffix
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def is_synthetic(symbol: str) -> bool:
|
| 60 |
+
return any(p.search(symbol) for p in _SYNTHETIC_PATTERNS)
|
| 61 |
+
|
| 62 |
async def _alert_loop():
|
| 63 |
"""Background task: scan every 5 min and fire Telegram alerts for high-confidence cards."""
|
| 64 |
from telegram_alert import send_alert, prune_sent_cache, send_raw_message
|
|
|
|
| 78 |
try:
|
| 79 |
src = get_source()
|
| 80 |
secondary = get_secondary()
|
| 81 |
+
candidates, _synth_excluded = rank_universe(src, src.min_vol, 30)
|
| 82 |
for c in candidates:
|
| 83 |
try:
|
| 84 |
card = score_symbol(src, c["symbol"], alert_account, 1.0,
|
|
|
|
| 165 |
return _binance if _binance_ok else None
|
| 166 |
|
| 167 |
|
| 168 |
+
def rank_universe(src, min_volume: float, top_n: int) -> tuple[list[dict], int]:
|
| 169 |
+
"""Returns (picked_candidates, synthetics_excluded_count)."""
|
| 170 |
perps = src.universe()
|
| 171 |
rows = []
|
| 172 |
+
synth_excluded = 0
|
| 173 |
for t in src.tickers():
|
| 174 |
if t["symbol"] not in perps:
|
| 175 |
continue
|
| 176 |
+
if EXCLUDE_SYNTHETICS and is_synthetic(t["symbol"]):
|
| 177 |
+
synth_excluded += 1
|
| 178 |
+
continue
|
| 179 |
qv = t["quote_volume_usd"]
|
| 180 |
lo, hi = t.get("low", 0), t.get("high", 0)
|
| 181 |
if qv < min_volume or lo <= 0:
|
| 182 |
continue
|
| 183 |
t["range_24h_pct"] = (hi - lo) / lo * 100
|
| 184 |
rows.append(t)
|
| 185 |
+
if synth_excluded:
|
| 186 |
+
logger.info("rank_universe: %d synthetic instruments excluded", synth_excluded)
|
| 187 |
if len(rows) < 3:
|
| 188 |
+
return [{"symbol": s} for s in src.anchors], synth_excluded
|
| 189 |
|
| 190 |
log_vols = [math.log(r["quote_volume_usd"]) for r in rows]
|
| 191 |
ranges = [r["range_24h_pct"] for r in rows]
|
|
|
|
| 203 |
if extra:
|
| 204 |
extra["anchor"] = True
|
| 205 |
picked.append(extra)
|
| 206 |
+
return picked, synth_excluded
|
| 207 |
|
| 208 |
|
| 209 |
# ── API routes ─────────────────────────────────────────────────────────────
|
|
|
|
| 225 |
try:
|
| 226 |
src = get_source()
|
| 227 |
floor = min_volume or src.min_vol
|
| 228 |
+
candidates, synth_excluded = rank_universe(src, floor, top)
|
| 229 |
secondary = get_secondary()
|
| 230 |
cards = []
|
| 231 |
for c in candidates:
|
|
|
|
| 258 |
cards.append({"symbol": c["symbol"], "error": str(e)[:120]})
|
| 259 |
cards.sort(key=lambda c: c.get("confidence", 0), reverse=True)
|
| 260 |
return {"fetched_at": datetime.now(timezone.utc).isoformat(),
|
| 261 |
+
"source": src.name, "market": src.market_note, "cards": cards,
|
| 262 |
+
"synthetics_excluded": synth_excluded}
|
| 263 |
except Exception as e:
|
| 264 |
raise HTTPException(503, str(e))
|
| 265 |
|
scorer.py
CHANGED
|
@@ -14,13 +14,35 @@ PA is loaded lazily — if patterns.py / chart_patterns.py / order_blocks.py /
|
|
| 14 |
confluence.py are missing, falls back to 40/20/20/20 (old formula).
|
| 15 |
"""
|
| 16 |
from __future__ import annotations
|
| 17 |
-
import math, statistics
|
| 18 |
import pandas as pd
|
| 19 |
from indicators import analyze_timeframe
|
| 20 |
from exchange import pick_source, INTERVAL_MS
|
| 21 |
from catalyst import score_catalyst as _catalyst_score
|
| 22 |
import time
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# Markov + HMM — imported lazily to avoid startup errors if not yet installed
|
| 25 |
try:
|
| 26 |
from markov import analyze_symbol as _markov_analyze
|
|
@@ -691,6 +713,19 @@ def score_symbol(src, symbol: str,
|
|
| 691 |
else:
|
| 692 |
direction = "long" if m15.get("rsi14", 50) < 50 else "short"
|
| 693 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
s_pos, n_pos = _score_positioning(funding_primary, oi_primary,
|
| 695 |
funding_secondary, oi_secondary,
|
| 696 |
taker_ratio=taker_ratio,
|
|
@@ -717,6 +752,15 @@ def score_symbol(src, symbol: str,
|
|
| 717 |
close = m15.get("close", 0)
|
| 718 |
atr_pct = m15.get("atr_pct", 0)
|
| 719 |
levels = _stop_target(tf_data, direction) if close > 0 else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
sizing = _size_suggestion(account, risk_pct, close, levels.get("sl", close), user_leverage)
|
| 721 |
|
| 722 |
# ── New: probability, leverage, duration ─────────────────────────────
|
|
@@ -888,6 +932,7 @@ def score_symbol(src, symbol: str,
|
|
| 888 |
"close": close,
|
| 889 |
"levels": levels,
|
| 890 |
"sizing": sizing,
|
|
|
|
| 891 |
# ── Probability & EV ──
|
| 892 |
"probability": {
|
| 893 |
"p_win": p_win,
|
|
|
|
| 14 |
confluence.py are missing, falls back to 40/20/20/20 (old formula).
|
| 15 |
"""
|
| 16 |
from __future__ import annotations
|
| 17 |
+
import math, statistics, os
|
| 18 |
import pandas as pd
|
| 19 |
from indicators import analyze_timeframe
|
| 20 |
from exchange import pick_source, INTERVAL_MS
|
| 21 |
from catalyst import score_catalyst as _catalyst_score
|
| 22 |
import time
|
| 23 |
|
| 24 |
+
# ── Strategy Fix Plan config (F2a, F3a) — additive, no scoring-engine change ──
|
| 25 |
+
# TP_DYN_R: dynamic-TP distance in R multiples, evidence zone 0.5-0.8R (default 0.7).
|
| 26 |
+
TP_DYN_R = float(os.environ.get("TP_DYN_R", "0.7"))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _roc_pct(df, n: int) -> float | None:
|
| 30 |
+
"""% change from the closed candle n bars back to the latest closed candle.
|
| 31 |
+
|
| 32 |
+
Used for entry-momentum readout (Strategy Fix Plan F2a) — no extra API calls,
|
| 33 |
+
reuses the already-fetched 5m/15m raw OHLCV DataFrames.
|
| 34 |
+
"""
|
| 35 |
+
if df is None or len(df) < n + 1:
|
| 36 |
+
return None
|
| 37 |
+
try:
|
| 38 |
+
prev = float(df["close"].iloc[-1 - n])
|
| 39 |
+
cur = float(df["close"].iloc[-1])
|
| 40 |
+
if prev == 0:
|
| 41 |
+
return None
|
| 42 |
+
return round((cur - prev) / prev * 100, 4)
|
| 43 |
+
except Exception:
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
# Markov + HMM — imported lazily to avoid startup errors if not yet installed
|
| 47 |
try:
|
| 48 |
from markov import analyze_symbol as _markov_analyze
|
|
|
|
| 713 |
else:
|
| 714 |
direction = "long" if m15.get("rsi14", 50) < 50 else "short"
|
| 715 |
|
| 716 |
+
# ── Entry momentum (Strategy Fix Plan F2a) — data only, no scoring change ──
|
| 717 |
+
# Reuses the 5m/15m raw DataFrames already fetched above — zero extra API calls.
|
| 718 |
+
m5_roc_pct = _roc_pct(_raw_dfs.get("5m"), 3) # ~last 15 min on 5m tf
|
| 719 |
+
m15_roc_pct = _roc_pct(_raw_dfs.get("15m"), 1) # last closed 15m candle
|
| 720 |
+
entry_momentum = {
|
| 721 |
+
"m5_roc_pct": m5_roc_pct,
|
| 722 |
+
"m15_roc_pct": m15_roc_pct,
|
| 723 |
+
"aligned": bool(
|
| 724 |
+
(direction == "long" and m5_roc_pct is not None and m5_roc_pct > 0) or
|
| 725 |
+
(direction == "short" and m5_roc_pct is not None and m5_roc_pct < 0)
|
| 726 |
+
),
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
s_pos, n_pos = _score_positioning(funding_primary, oi_primary,
|
| 730 |
funding_secondary, oi_secondary,
|
| 731 |
taker_ratio=taker_ratio,
|
|
|
|
| 752 |
close = m15.get("close", 0)
|
| 753 |
atr_pct = m15.get("atr_pct", 0)
|
| 754 |
levels = _stop_target(tf_data, direction) if close > 0 else {}
|
| 755 |
+
|
| 756 |
+
# ── Dynamic TP (Strategy Fix Plan F3a) — additive field only; TP1/2/3 unchanged ──
|
| 757 |
+
if levels:
|
| 758 |
+
sl_dist = abs(close - levels.get("sl", close))
|
| 759 |
+
levels["tp_dyn"] = round(
|
| 760 |
+
close + TP_DYN_R * sl_dist if direction == "long" else close - TP_DYN_R * sl_dist,
|
| 761 |
+
6,
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
sizing = _size_suggestion(account, risk_pct, close, levels.get("sl", close), user_leverage)
|
| 765 |
|
| 766 |
# ── New: probability, leverage, duration ─────────────────────────────
|
|
|
|
| 932 |
"close": close,
|
| 933 |
"levels": levels,
|
| 934 |
"sizing": sizing,
|
| 935 |
+
"entry_momentum": entry_momentum,
|
| 936 |
# ── Probability & EV ──
|
| 937 |
"probability": {
|
| 938 |
"p_win": p_win,
|
telegram_alert.py
CHANGED
|
@@ -103,6 +103,31 @@ def _rr(card: dict) -> str:
|
|
| 103 |
|
| 104 |
# ── Message builder ───────────────────────────────────────────────────────────
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
def _order_flow_line(card: dict) -> str:
|
| 107 |
"""Format the taker buy/sell ratio line for Telegram. Returns '' if unavailable."""
|
| 108 |
taker_ratio = (
|
|
@@ -158,8 +183,12 @@ def build_message(card: dict, tv_sym: str) -> str:
|
|
| 158 |
else f"{state_emoji} HMM <b>{state}</b>"
|
| 159 |
)
|
| 160 |
flow_line = _order_flow_line(card)
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
lines = [
|
|
|
|
| 163 |
f"{dir_emoji} <b>{sym} {direction} · {conf:.1f}/10</b>",
|
| 164 |
"",
|
| 165 |
f"📍 <b>Entry</b> <code>{_fmt_price(card.get('close'))}</code>",
|
|
@@ -167,6 +196,8 @@ def build_message(card: dict, tv_sym: str) -> str:
|
|
| 167 |
f"🎯 <b>TP1</b> <code>{_fmt_price(lvl.get('tp1'))}</code> R/R {_rr(card)}",
|
| 168 |
f" <b>TP2</b> <code>{_fmt_price(lvl.get('tp2'))}</code>",
|
| 169 |
f" <b>TP3</b> <code>{_fmt_price(lvl.get('tp3'))}</code>",
|
|
|
|
|
|
|
| 170 |
"",
|
| 171 |
f"⚙️ Leverage <b>{lev.get('user_leverage', '—')}x</b> Margin <b>{_money(lev.get('margin_to_post'))}</b>",
|
| 172 |
f"💸 Loss@SL <b>{_money(lev.get('loss_at_stop'))}</b> Risk <b>{_money(sz.get('risk_usd'))}</b>",
|
|
|
|
| 103 |
|
| 104 |
# ── Message builder ───────────────────────────────────────────────────────────
|
| 105 |
|
| 106 |
+
def _tier_line(conf: float) -> str:
|
| 107 |
+
"""Confidence tier prefix (Strategy Fix Plan F1a).
|
| 108 |
+
|
| 109 |
+
Alert threshold stays 8.8 (data collection continues); this only labels
|
| 110 |
+
the message so Utkarsh can tell trade-grade from watch-only at a glance.
|
| 111 |
+
Auto-trade gate (CONF_MIN=9.0, per ADDENDUM) is a separate, later concern.
|
| 112 |
+
"""
|
| 113 |
+
if conf >= 9.0:
|
| 114 |
+
return "💎 <b>TRADE-GRADE</b>"
|
| 115 |
+
if conf >= 8.8:
|
| 116 |
+
return "🔭 <b>WATCH ONLY</b> — below auto-trade bar"
|
| 117 |
+
return ""
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _momentum_line(card: dict) -> str:
|
| 121 |
+
"""Entry-momentum line (Strategy Fix Plan F2b). Empty if data unavailable."""
|
| 122 |
+
mom = card.get("entry_momentum") or {}
|
| 123 |
+
m15v = mom.get("m15_roc_pct")
|
| 124 |
+
if m15v is None:
|
| 125 |
+
return ""
|
| 126 |
+
mark = "✓ aligned" if mom.get("aligned") else "⚠ AGAINST direction"
|
| 127 |
+
sign = "+" if m15v >= 0 else ""
|
| 128 |
+
return f"⚡ Momentum 15m: {sign}{m15v:.2f}% ({mark})"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
def _order_flow_line(card: dict) -> str:
|
| 132 |
"""Format the taker buy/sell ratio line for Telegram. Returns '' if unavailable."""
|
| 133 |
taker_ratio = (
|
|
|
|
| 183 |
else f"{state_emoji} HMM <b>{state}</b>"
|
| 184 |
)
|
| 185 |
flow_line = _order_flow_line(card)
|
| 186 |
+
tier_line = _tier_line(float(conf))
|
| 187 |
+
momentum_line = _momentum_line(card)
|
| 188 |
+
tp_dyn = lvl.get("tp_dyn")
|
| 189 |
|
| 190 |
lines = [
|
| 191 |
+
*([tier_line] if tier_line else []),
|
| 192 |
f"{dir_emoji} <b>{sym} {direction} · {conf:.1f}/10</b>",
|
| 193 |
"",
|
| 194 |
f"📍 <b>Entry</b> <code>{_fmt_price(card.get('close'))}</code>",
|
|
|
|
| 196 |
f"🎯 <b>TP1</b> <code>{_fmt_price(lvl.get('tp1'))}</code> R/R {_rr(card)}",
|
| 197 |
f" <b>TP2</b> <code>{_fmt_price(lvl.get('tp2'))}</code>",
|
| 198 |
f" <b>TP3</b> <code>{_fmt_price(lvl.get('tp3'))}</code>",
|
| 199 |
+
*([f" <b>TP-dyn</b> <code>{_fmt_price(tp_dyn)}</code> (0.7R)"] if tp_dyn is not None else []),
|
| 200 |
+
*([momentum_line] if momentum_line else []),
|
| 201 |
"",
|
| 202 |
f"⚙️ Leverage <b>{lev.get('user_leverage', '—')}x</b> Margin <b>{_money(lev.get('margin_to_post'))}</b>",
|
| 203 |
f"💸 Loss@SL <b>{_money(lev.get('loss_at_stop'))}</b> Risk <b>{_money(sz.get('risk_usd'))}</b>",
|