Skill flagged — suspicious patterns detected

ClawHub Security flagged this skill as suspicious. Review the scan results before using.

TA Radar

v1.2.0

Multi-Dimensional Technical Analysis Radar for cryptocurrencies. Supports spot trading pairs (Binance/Gate.io) and on-chain contract addresses (via DexScreen...

0· 66·0 current·0 all-time

Install

OpenClaw Prompt Flow

Install with OpenClaw

Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw for deanpeng-dotcom/ta-radar.

Previewing Install & Setup.
Prompt PreviewInstall & Setup
Install the skill "TA Radar" (deanpeng-dotcom/ta-radar) from ClawHub.
Skill page: https://clawhub.ai/deanpeng-dotcom/ta-radar
Keep the work scoped to this skill only.
After install, inspect the skill metadata and help me finish setup.
Use only the metadata you can verify from ClawHub; do not invent missing requirements.
Ask before making any broader environment changes.

Command Line

CLI Commands

Use the direct CLI path if you want to install manually and keep every step visible.

OpenClaw CLI

Bare skill slug

openclaw skills install ta-radar

ClawHub CLI

Package manager switcher

npx clawhub@latest install ta-radar
Security Scan
Capability signals
Crypto
These labels describe what authority the skill may exercise. They are separate from suspicious or malicious moderation verdicts.
VirusTotalVirusTotal
Suspicious
View report →
OpenClawOpenClaw
Suspicious
medium confidence
Purpose & Capability
The name/description (TA Radar for crypto) align with the described data sources (Binance, Gate.io, DexScreener) and the indicators computed. However SKILL.md metadata lists an install command 'pip install -r requirements.txt' while README and the embedded script claim 'zero-dependency' pure-Python operation and no requirements.txt is present in the manifest — this inconsistency is unexplained.
!
Instruction Scope
The agent is instructed to write a full Python script to /tmp and execute it, then delete it. Running code supplied inside the SKILL.md is expected for instruction-only skills but is higher-risk than simple API calls because the embedded script can perform arbitrary I/O and network requests. From the visible parts the script fetches only the listed public endpoints (api.binance.info, api.gateio.ws, allorigins.win→DexScreener). I could not inspect the entire embedded script (it was truncated in the provided SKILL.md), so unknown behavior may exist. The instruction to return the script's full stdout unchanged may expose unexpected local details if the script prints them.
Install Mechanism
There is no separate install spec and no archived downloads; runtime network calls happen only during script execution. The presence of an install command in the SKILL.md metadata (pip install -r requirements.txt) conflicts with the 'zero-dependency' claim and with the manifest (no requirements.txt). No high-risk installer URLs or extracted archives are present.
Credentials
Declared environment variables are minimal and appropriate: TA_SYMBOL (required) and TA_INTERVAL (optional). The skill does not request secrets or credentials and the visible script only reads those vars. No evidence the skill asks for unrelated credentials or system config paths.
Persistence & Privilege
The skill is not set to always:true and does not request persistent system-level changes. It writes a temporary file to /tmp and deletes it; no installation of persistent daemons or modification of other skills is indicated.
What to consider before installing
This skill largely looks like what it says (a crypto TA tool), but there are three reasons to be cautious: (1) SKILL.md asks the agent to write and run a long embedded Python script — running code embedded in a skill is more powerful and riskier than just calling an API; (2) the metadata claims 'pip install -r requirements.txt' while the package claims zero-dependency and no requirements.txt is present — ask the maintainer to clarify or show the repository and requirements.txt before installing; (3) I could not inspect the entire embedded script (it was truncated here), so review the full script to ensure it doesn't call unexpected endpoints, exfiltrate data, or read local files. Recommended precautions: run the skill only in an isolated/sandboxed environment (or review the full embedded script first), verify the repository/source code on GitHub, and confirm there are no hidden endpoints or calls beyond the listed public APIs (Binance, Gate.io, DexScreener via allorigins.win). If you rely on it for real funds, consider running the script locally yourself after manual code review rather than allowing autonomous agent execution.

Like a lobster shell, security has layers — review code before you run it.

latestvk973qtgpkpdtswzsxme23f781n84ptec
66downloads
0stars
1versions
Updated 2w ago
v1.2.0
MIT-0

TA Radar (Multi-Dimensional Technical Analysis Radar)

Overview

This skill provides zero-dependency technical analysis for cryptocurrency assets, supporting two input types:

  • Spot Trading Pairs (e.g. BTC, ETHUSDT): Fetches K-line data first from Binance official mirror (api.binance.info), automatically falls back to Gate.io API (api.gateio.ws) if Binance is unreachable (e.g. network restrictions in mainland China).
  • On-Chain Contract Addresses (starts with 0x): Resolves the base token symbol via DexScreener through the allorigins.win public proxy, then queries exchange K-line data using the resolved symbol.

GFW Compatibility: Gate.io API is used as an automatic fallback to ensure availability for users in mainland China when Binance endpoints are blocked. DexScreener is accessed through a public proxy to avoid direct network restrictions.

v1.2 Updates

  • [NEW] Dual Data Source Fallback: Gate.io added as backup K-line data source, automatic failover from Binance with zero user interaction.
  • [NEW] Beginner-Friendly Annotations: Each indicator conclusion includes plain language explanations, no financial background required to understand signals.

Agent Execution Workflow

When the skill is triggered by a user request, follow these steps strictly:

Step 1: Parse User Parameters

Extract the following parameters from user input:

ParameterDescriptionDefault Value
SYMBOLTrading pair ticker (e.g. BTCUSDT) or on-chain contract address (e.g. 0xabc123...)Required
INTERVALTimeframe for analysis, allowed values: 1h, 4h, 1d1h

Automatic Routing (handled internally by the script, no agent action needed):

  • If input starts with 0x → resolve symbol via DexScreener first, then fetch K-line data via dual source logic
  • Otherwise → directly fetch K-line data via dual source logic, auto append USDT suffix if missing

Step 2: Write Python Script to Temporary File

Save the complete Python code from the Embedded Script section below to:

/tmp/ta_radar_run.py

Use bash heredoc for writing:

cat > /tmp/ta_radar_run.py << 'PYEOF'
<PASTE COMPLETE PYTHON SCRIPT HERE>
PYEOF

Step 3: Execute Script and Capture Output

TA_SYMBOL="<SYMBOL>" TA_INTERVAL="<INTERVAL>" python3 /tmp/ta_radar_run.py
  • Success (exit code 0): Present the full standard output of the script to the user exactly as-is, no trimming or summarization.
  • Failure (exit code non-0): Present the standard error output to the user and prompt to check parameter format.

Step 4: Clean Up Temporary File

rm -f /tmp/ta_radar_run.py

Embedded Script

Full Python 3 script with zero third-party dependencies, all indicator calculations implemented manually using built-in libraries.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TA Radar v1.2
Zero-dependency TA engine: EMA / RSI / MACD / Bollinger Bands
Data sources: Binance (api.binance.info) first, auto fallback to Gate.io (api.gateio.ws)
DEX contract resolution: DexScreener via allorigins public proxy

Changelog v1.2:
  [NEW-1] Dual data source fallback: fetch_klines() encapsulates Binance + Gate.io logic,
          any Binance error (HTTP/Timeout/URLError) automatically switches to Gate.io,
          warning is rendered only if both sources fail.
  [NEW-2] Beginner-friendly annotations: Each indicator conclusion includes
          plain language explanation of signal meaning, no jargon or metaphors.
"""

import os
import sys
import json
import math
import socket
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime, UTC

# ─────────────────────────────────────────────
#  Environment Variables
# ─────────────────────────────────────────────
SYMBOL   = os.environ.get("TA_SYMBOL", "").strip()
INTERVAL = os.environ.get("TA_INTERVAL", "1h").strip().lower()
LIMIT    = 300 # Sufficient historical data for accurate EMA/MACD convergence

VALID_INTERVALS = {"1h", "4h", "1d"}


# ─────────────────────────────────────────────
#  Network Request: Returns (data, error_msg) tuple
# ─────────────────────────────────────────────

def safe_fetch(url: str, timeout: int = 12):
    """
    Perform HTTP GET request, returns (parsed_json, error_msg) tuple.

    Success:  (dict|list, None)
    Failure:  (None, error string)
      - HTTP error   → "HTTP {code}: {msg}"
      - Timeout      → "Timeout ({n}s): {base_url}"
      - URLError     → "URLError: {reason}"
      - JSON error   → "JSONDecodeError: {detail}"
      - Other        → "UnexpectedError: {type}: {msg}"
    """
    req = urllib.request.Request(
        url,
        headers={"User-Agent": "Mozilla/5.0 ta-radar/1.2"}
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8")
        try:
            return json.loads(raw), None
        except json.JSONDecodeError as e:
            return None, f"JSONDecodeError: {e}"

    except urllib.error.HTTPError as e:
        try:
            body = e.read().decode("utf-8")
            detail = json.loads(body).get("msg", body[:200])
        except Exception:
            detail = str(e.reason)
        return None, f"HTTP {e.code}: {detail}"

    except urllib.error.URLError as e:
        reason = str(e.reason)
        base = url.split("?")[0]
        if "timed out" in reason.lower() or isinstance(e.reason, socket.timeout):
            return None, f"Timeout ({timeout}s): {base}"
        return None, f"URLError: {reason}"

    except socket.timeout:
        return None, f"Timeout ({timeout}s): {url.split('?')[0]}"

    except Exception as e:
        return None, f"UnexpectedError: {type(e).__name__}: {e}"


# ─────────────────────────────────────────────
#  Input Validation
# ─────────────────────────────────────────────

def validate_inputs():
    if not SYMBOL:
        print("❌ Error: TA_SYMBOL environment variable not provided. Specify a trading pair or contract address.", file=sys.stderr)
        sys.exit(1)
    if INTERVAL not in VALID_INTERVALS:
        print(
            f"❌ Error: Unsupported timeframe '{INTERVAL}'. Use 1h / 4h / 1d.",
            file=sys.stderr
        )
        sys.exit(1)


# ─────────────────────────────────────────────
#  Data Fetching Layer
# ─────────────────────────────────────────────

def is_contract_address(s: str) -> bool:
    """Check if input is an on-chain contract address: starts with 0x and length >=10."""
    return s.startswith("0x") and len(s) >= 10


def fetch_binance_klines(symbol: str, interval: str, limit: int):
    """
    Fetch closing price list from Binance official mirror.
    Endpoint: api.binance.info
    Returns (closes: list | None, error_msg: str | None).
    """
    sym = symbol.strip().upper()
    if not sym.endswith("USDT"):
        sym += "USDT"

    url = (
        f"https://api.binance.info/api/v3/klines"
        f"?symbol={sym}&interval={interval}&limit={limit}"
    )
    data, err = safe_fetch(url)

    if err:
        return None, f"Binance [{sym}] → {err}"
    if not isinstance(data, list) or len(data) == 0:
        return None, f"Binance [{sym}] returned empty or invalid data"
    try:
        closes = [float(c[4]) for c in data]
    except (IndexError, ValueError, TypeError) as e:
        return None, f"Binance [{sym}] K-line parse error: {e}"
    if len(closes) < 26:
        return None, f"Binance [{sym}] insufficient K-line data ({len(closes)} bars, minimum 26 required)"

    return closes, None


def fetch_gate_klines(symbol: str, interval: str, limit: int):
    """
    Fetch closing price list from Gate.io as fallback data source.
    Endpoint: api.gateio.ws (accessible in mainland China)
    Gate.io K-line interval mapping: 1h -> 1h, 4h -> 4h, 1d -> 1d
    Returns (closes: list | None, error_msg: str | None).
    """
    sym = symbol.strip().upper()
    if not sym.endswith("USDT"):
        sym += "USDT"

    # Gate.io pair format: BTC_USDT (underscore separated)
    gate_pair = sym[:-4] + "_USDT"

    url = (
        f"https://api.gateio.ws/api/v4/spot/candlesticks"
        f"?currency_pair={urllib.parse.quote(gate_pair)}"
        f"&interval={interval}&limit={limit}"
    )
    data, err = safe_fetch(url)

    if err:
        return None, f"Gate.io [{gate_pair}] → {err}"
    if not isinstance(data, list) or len(data) == 0:
        return None, f"Gate.io [{gate_pair}] returned empty or invalid data"

    # Gate.io candlestick format (v4):
    # [timestamp, volume, close, high, low, open, ...]
    # Close price at index 2
    try:
        closes = [float(c[2]) for c in data]
    except (IndexError, ValueError, TypeError) as e:
        return None, f"Gate.io [{gate_pair}] K-line parse error: {e}"

    if len(closes) < 26:
        return None, f"Gate.io [{gate_pair}] insufficient K-line data ({len(closes)} bars, minimum 26 required)"

    return closes, None


def fetch_klines(symbol: str, interval: str, limit: int):
    """
    Dual data source K-line entry point.
    Tries Binance first, automatically falls back to Gate.io on failure.
    Returns (closes: list | None, source_name: str, debug_msgs: list).
      - closes = None means both sources failed.
      - source_name = name of the data source actually used, for report header.
      - debug_msgs = collection of error messages from each step.
    """
    debug_msgs = []

    # Try Binance
    closes, binance_err = fetch_binance_klines(symbol, interval, limit)
    if closes is not None:
        return closes, "Binance", debug_msgs

    debug_msgs.append(f"Binance unavailable: {binance_err}")
    print(f"  ⚠  Binance data source unavailable, switching to Gate.io...\n")

    # Fallback to Gate.io
    closes, gate_err = fetch_gate_klines(symbol, interval, limit)
    if closes is not None:
        return closes, "Gate.io", debug_msgs

    debug_msgs.append(f"Gate.io unavailable: {gate_err}")
    return None, "", debug_msgs


def resolve_symbol_from_dex(address: str):
    """
    Resolve baseToken.symbol and pair label from contract address via DexScreener through allorigins proxy.
    Returns (base_symbol: str | None, pair_label: str, error_msg: str | None).
    """
    dex_url = (
        f"https://api.dexscreener.com/latest/dex/search"
        f"?q={urllib.parse.quote(address)}"
    )
    proxy_url = (
        f"https://api.allorigins.win/raw"
        f"?url={urllib.parse.quote(dex_url)}"
    )

    data, err = safe_fetch(proxy_url, timeout=18)

    if err:
        return None, "", f"DexScreener proxy request failed → {err}"
    if not isinstance(data, dict):
        return None, "", f"DexScreener returned invalid format (expected dict, got {type(data).__name__})"

    pairs = data.get("pairs")
    if not pairs or not isinstance(pairs, list) or len(pairs) == 0:
        return None, "", f"DexScreener found no pairs for address {address[:20]}..."

    def get_liq(p):
        try:
            return float(p.get("liquidity", {}).get("usd", 0) or 0)
        except (TypeError, ValueError):
            return 0.0

    top = sorted(pairs, key=get_liq, reverse=True)[0]
    base_symbol  = (top.get("baseToken", {}).get("symbol") or "").strip().upper()
    quote_symbol = top.get("quoteToken", {}).get("symbol", "?")
    dex_id       = top.get("dexId", "?")
    chain_id     = top.get("chainId", "?")
    liq_usd      = get_liq(top)

    pair_label = (
        f"{base_symbol}/{quote_symbol} on {dex_id} "
        f"(chain={chain_id}, liq=${liq_usd:,.0f})"
    )

    if not base_symbol:
        return None, pair_label, "DexScreener returned empty baseToken.symbol"

    return base_symbol, pair_label, None


# ─────────────────────────────────────────────
#  Pure Python Technical Indicators (zero dependencies)
# ─────────────────────────────────────────────

def calc_ema(prices: list, period: int) -> list:
    """Exponential Moving Average (EMA). First value is SMA, k = 2 / (period+1)."""
    if len(prices) < period:
        return []
    k = 2.0 / (period + 1)
    result = [sum(prices[:period]) / period]
    for p in prices[period:]:
        result.append(p * k + result[-1] * (1 - k))
    return result


def calc_rsi(prices: list, period: int = 14):
    """Relative Strength Index (RSI), Wilder's smoothing method. Returns latest value, None if insufficient data."""
    if len(prices) < period + 1:
        return None
    gains, losses = [], []
    for i in range(1, len(prices)):
        d = prices[i] - prices[i - 1]
        gains.append(max(d, 0.0))
        losses.append(max(-d, 0.0))
    ag = sum(gains[:period]) / period
    al = sum(losses[:period]) / period
    for i in range(period, len(gains)):
        ag = (ag * (period - 1) + gains[i]) / period
        al = (al * (period - 1) + losses[i]) / period
    if al == 0:
        return 100.0
    return 100.0 - (100.0 / (1 + ag / al))


def calc_macd(prices: list, fast: int = 12, slow: int = 26, signal: int = 9):
    """Moving Average Convergence Divergence (MACD). Returns (dif, dea, hist) latest values, None if insufficient data."""
    ef = calc_ema(prices, fast)
    es = calc_ema(prices, slow)
    if not ef or not es:
        return None
    offset = len(ef) - len(es)
    dif_series = [f - s for f, s in zip(ef[offset:], es)]
    if len(dif_series) < signal:
        return None
    dea_series = calc_ema(dif_series, signal)
    if not dea_series:
        return None
    dif  = dif_series[-1]
    dea  = dea_series[-1]
    return dif, dea, dif - dea


def calc_bollinger(prices: list, period: int = 20, num_std: float = 2.0):
    """Bollinger Bands. Returns (upper, middle, lower) latest values, None if insufficient data."""
    if len(prices) < period:
        return None
    w = prices[-period:]
    mid = sum(w) / period
    std = math.sqrt(sum((p - mid) ** 2 for p in w) / period)
    return mid + num_std * std, mid, mid - num_std * std


# ─────────────────────────────────────────────
#  Formatting Utilities
# ─────────────────────────────────────────────

def fmt(val: float) -> str:
    """Format price, automatically adapts to magnitude."""
    if val == 0:
        return "0"
    a = abs(val)
    if a >= 1000:  return f"{val:,.2f}"
    elif a >= 1:   return f"{val:.4f}"
    elif a >= 0.01: return f"{val:.6f}"
    else:           return f"{val:.8f}"


# ─────────────────────────────────────────────
#  [NEW-2] Beginner-Friendly Explanation Library
#  Each explanation is objective, plain language, no metaphors or jargon.
# ─────────────────────────────────────────────

EMA_EXPLAIN = {
    "bull": "Short-term EMA above long-term EMA indicates recent price strength.",
    "bear": "Short-term EMA below long-term EMA indicates recent price weakness.",
    "flat": "Mixed EMA alignment indicates sideways consolidation, no clear trend.",
}

RSI_EXPLAIN = {
    "ob":  "RSI above 70 indicates recent large gains, potential short-term pullback risk.",
    "os":  "RSI below 30 indicates recent large losses, potential short-term bounce opportunity.",
    "mid": "RSI between 30-70 indicates normal momentum, neutral signal.",
}

MACD_EXPLAIN = {
    "bull_above": "MACD above signal line and zero line indicates strong upward momentum.",
    "bear_below": "MACD below signal line and zero line indicates strong downward momentum.",
    "bull_below": "MACD crossed above signal line but still below zero line, improving momentum but not confirmed.",
    "bear_above": "MACD crossed below signal line but still above zero line, weakening momentum, monitor closely.",
}

BOLL_EXPLAIN = {
    "near_upper": "Price near upper Bollinger Band indicates limited upside potential, watch for resistance.",
    "near_lower": "Price near lower Bollinger Band indicates statistical support area, watch for bounce.",
    "squeeze":    "Narrow Bollinger Band width indicates low volatility, often precursor to directional move.",
    "mid_above":  "Price above middle Bollinger Band indicates short-term relative strength.",
    "mid_below":  "Price below middle Bollinger Band indicates short-term relative weakness.",
}


# ─────────────────────────────────────────────
#  Report Rendering
# ─────────────────────────────────────────────

def render_report(
    display_sym: str,
    interval: str,
    closes: list,
    data_source: str = "Binance",
    source_note: str = "",
):
    price = closes[-1]
    ts    = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")

    # ── Calculate Indicators ──────────────────────────
    ema7_s  = calc_ema(closes, 7)
    ema25_s = calc_ema(closes, 25)
    ema99_s = calc_ema(closes, 99)
    rsi     = calc_rsi(closes, 14)
    macd_r  = calc_macd(closes)
    boll_r  = calc_bollinger(closes, 20)

    ema7_v  = ema7_s[-1]  if ema7_s  else None
    ema25_v = ema25_s[-1] if ema25_s else None
    ema99_v = ema99_s[-1] if ema99_s else None

    # ── Trend Signals ──────────────────────────────
    ema_bull   = bool(ema7_v and ema25_v and ema99_v and ema7_v > ema25_v > ema99_v)
    ema_bear   = bool(ema7_v and ema25_v and ema99_v and ema7_v < ema25_v < ema99_v)
    rsi_ob     = rsi is not None and rsi > 70
    rsi_os     = rsi is not None and rsi < 30
    macd_bull  = bool(macd_r and macd_r[0] > 0 and macd_r[2] > 0)
    macd_bear  = bool(macd_r and macd_r[0] < 0 and macd_r[2] < 0)
    boll_up    = bool(boll_r and price > boll_r[1])
    near_upper = bool(boll_r and price >= boll_r[0] * 0.995)
    near_lower = bool(boll_r and price <= boll_r[2] * 1.005)
    boll_squeeze = bool(boll_r and (boll_r[0] - boll_r[2]) / boll_r[1] * 100 < 5)

    # ── Composite Trend Voting ──────────────────────────
    bull_votes = sum([
        ema_bull,
        macd_bull,
        boll_up,
        rsi is not None and 40 <= rsi <= 60,
    ])
    bear_votes = sum([
        ema_bear,
        macd_bear,
        not boll_up and boll_r is not None,
        rsi_ob,
    ])

    if bull_votes >= 3:
        trend_label = "Bullish (Bulls in control)"
    elif bear_votes >= 3:
        trend_label = "Bearish (Bears in control)"
    elif bull_votes == bear_votes:
        trend_label = "Neutral (Range bound)"
    elif bull_votes > bear_votes:
        trend_label = "Neutral (Slightly bullish)"
    else:
        trend_label = "Neutral (Slightly bearish)"

    # ── Support / Resistance Levels ─────────────────────────
    sup, res = [], []
    if boll_r:
        ub, mb, lb = boll_r
        (sup if lb < price else res).append(("Lower Bollinger", lb))
        (sup if mb < price else res).append(("Middle Bollinger", mb))
        (res if ub > price else sup).append(("Upper Bollinger", ub))
    if ema25_v:
        (sup if ema25_v < price else res).append(("EMA25", ema25_v))
    if ema99_v:
        (sup if ema99_v < price else res).append(("EMA99", ema99_v))

    sup = sorted(sup, key=lambda x: x[1], reverse=True)[:3]
    res = sorted(res, key=lambda x: x[1])[:3]

    def fmt_levels(lvls):
        if not lvls:
            return "No valid levels available"
        return "  /  ".join(f"{n} {fmt(v)}" for n, v in lvls)

    # ── Composite Analysis Text ──────────────────────────
    parts = []

    if ema7_v and ema25_v and ema99_v:
        if ema_bull:
            parts.append(
                f"EMA alignment is bullish (EMA7 {fmt(ema7_v)} > EMA25 {fmt(ema25_v)} > "
                f"EMA99 {fmt(ema99_v)}), indicating an established uptrend structure."
            )
        elif ema_bear:
            parts.append(
                f"EMA alignment is bearish (EMA7 {fmt(ema7_v)} < EMA25 {fmt(ema25_v)} < "
                f"EMA99 {fmt(ema99_v)}), indicating an established downtrend structure."
            )
        else:
            parts.append(
                f"EMA alignment is mixed (EMA7 {fmt(ema7_v)}, EMA25 {fmt(ema25_v)}, "
                f"EMA99 {fmt(ema99_v)}), indicating sideways consolidation with no clear directional signal."
            )

    if rsi is not None:
        if rsi_ob:
            parts.append(
                f"RSI reading is {rsi:.1f}, in overbought territory (threshold 70). "
                f"Short-term momentum is overextended, potential for pullback or consolidation."
            )
        elif rsi_os:
            parts.append(
                f"RSI reading is {rsi:.1f}, in oversold territory (threshold 30). "
                f"Short-term momentum is excessively negative, potential for technical bounce."
            )
        else:
            parts.append(
                f"RSI reading is {rsi:.1f}, in neutral territory (between 30 and 70). "
                f"Current momentum is not extreme."
            )

    if macd_r:
        dif, dea, hist = macd_r
        if dif > dea and dif > 0:
            parts.append(
                f"MACD line ({fmt(dif)}) is above signal line ({fmt(dea)}) and both are above zero line. "
                f"Histogram is positive ({fmt(hist)}), confirming upward momentum, golden cross valid."
            )
        elif dif < dea and dif < 0:
            parts.append(
                f"MACD line ({fmt(dif)}) is below signal line ({fmt(dea)}) and both are below zero line. "
                f"Histogram is negative ({fmt(hist)}), confirming downward momentum, death cross valid."
            )
        elif dif > dea:
            parts.append(
                f"MACD line ({fmt(dif)}) has crossed above signal line ({fmt(dea)}) but remains below zero line. "
                f"Histogram turned positive ({fmt(hist)}), momentum is improving but not yet confirmed above zero."
            )
        else:
            parts.append(
                f"MACD line ({fmt(dif)}) has crossed below signal line ({fmt(dea)}) but remains above zero line. "
                f"Histogram turned negative ({fmt(hist)}), upward momentum is weakening, direction pending confirmation."
            )

    if boll_r:
        ub, mb, lb = boll_r
        bw = (ub - lb) / mb * 100
        if near_upper:
            parts.append(
                f"Current price ({fmt(price)}) is near or touching upper Bollinger Band ({fmt(ub)}). "
                f"Band width is {bw:.1f}%, statistical resistance overhead, watch for volume confirmation on breakout attempts."
            )
        elif near_lower:
            parts.append(
                f"Current price ({fmt(price)}) is near or touching lower Bollinger Band ({fmt(lb)}). "
                f"Band width is {bw:.1f}%, near statistical support area, watch for bounce confirmation at this level."
            )
        elif boll_squeeze:
            parts.append(
                f"Bollinger Band width has narrowed to {bw:.1f}%, price is in low volatility consolidation phase. "
                f"This pattern typically precedes a directional expansion, monitor volume for breakout direction clues."
            )
        else:
            pos = "above" if price > mb else "below"
            parts.append(
                f"Current price ({fmt(price)}) is {pos} middle Bollinger Band ({fmt(mb)}). "
                f"Band width is {bw:.1f}%, volatility is within normal range."
            )

    analysis_text = "\n".join(f"  {p}" for p in parts)

    # ── Print Report ────────────────────────
    SEP = "─" * 60
    EQ  = "═" * 60

    print(f"\n{EQ}")
    print(f"  TA Radar v1.2  |  {display_sym}  |  {interval.upper()}")
    print(f"  Data Source: {data_source}" + (f"  |  {source_note}" if source_note else ""))
    print(f"  Generated: {ts}")
    print(f"{EQ}\n")

    # ── Core Data Panel ──────────────────────
    print("【Core Data Panel】")
    print(SEP)
    print(f"  Current Price  : {fmt(price)}")
    print()

    # EMA
    print("  ▸ EMA (7 / 25 / 99)")
    if ema7_v and ema25_v and ema99_v:
        if ema_bull:
            label   = "Bullish alignment ↑"
            explain = EMA_EXPLAIN["bull"]
        elif ema_bear:
            label   = "Bearish alignment ↓"
            explain = EMA_EXPLAIN["bear"]
        else:
            label   = "Mixed alignment ↔"
            explain = EMA_EXPLAIN["flat"]
        print(f"    EMA7  = {fmt(ema7_v)}")
        print(f"    EMA25 = {fmt(ema25_v)}")
        print(f"    EMA99 = {fmt(ema99_v)}")
        print(f"    Conclusion  : {label}")
        print(f"    Explanation : {explain}")
    else:
        print("    Insufficient data to calculate")
    print()

    # RSI
    print("  ▸ RSI (14)")
    if rsi is not None:
        if rsi_ob:
            label   = "Overbought ⚠"
            explain = RSI_EXPLAIN["ob"]
        elif rsi_os:
            label   = "Oversold ⚠"
            explain = RSI_EXPLAIN["os"]
        else:
            label   = "Neutral ✓"
            explain = RSI_EXPLAIN["mid"]
        print(f"    RSI   = {rsi:.2f}")
        print(f"    Conclusion  : {label}")
        print(f"    Explanation : {explain}")
    else:
        print("    Insufficient data to calculate")
    print()

    # MACD
    print("  ▸ MACD (12 / 26 / 9)")
    if macd_r:
        dif, dea, hist = macd_r
        if dif > dea and dif > 0:
            label   = "Golden cross (above zero line) ↑"
            explain = MACD_EXPLAIN["bull_above"]
        elif dif < dea and dif < 0:
            label   = "Death cross (below zero line) ↓"
            explain = MACD_EXPLAIN["bear_below"]
        elif dif > dea:
            label   = "Golden cross (below zero line, pending confirmation) ↗"
            explain = MACD_EXPLAIN["bull_below"]
        else:
            label   = "Death cross (above zero line, weakening momentum) ↘"
            explain = MACD_EXPLAIN["bear_above"]
        print(f"    MACD Line  = {fmt(dif)}")
        print(f"    Signal Line = {fmt(dea)}")
        print(f"    Histogram   = {fmt(hist)}")
        print(f"    Conclusion  : {label}")
        print(f"    Explanation : {explain}")
    else:
        print("    Insufficient

Comments

Loading comments...