Shop It Docs
TradingView UDF

GET /search

Autocomplete symbol search with prefix-boost ranking

/search powers the Charting Library's symbol-picker autocomplete. As the user types, the library calls this endpoint with the current input and shows up to limit results.

  • Method: GET
  • Path: /api/nepse/tradingview/search
  • Content-Type: application/json (bare array — see below)

Query parameters

ParamRequiredTypeDefaultNotes
querystring""Substring to match against company symbol/name, index tv_symbol/index_name, or sub-index tv_symbol/name. Empty → browse mode: return first N. Typing ^ alone surfaces every index + sub-index.
limitint30Clamped to [1, 100]. Invalid, zero, negative, or over-cap values reset to 30.
typestringAccepted for UDF compatibility. The backend does not filter on it — stock and index results are always merged.
exchangestringAccepted for UDF compatibility but ignored (NEPSE is the only exchange).

Response

The response is a bare JSON array at the root — NOT an object wrapping it. If you return {"results": [...]}, the TradingView search box silently shows "no matches" forever. This is the most common UDF implementation mistake.

[
  {
    "symbol": "^NEPSE",
    "full_name": "NEPSE:^NEPSE",
    "description": "NEPSE Index",
    "exchange": "NEPSE",
    "ticker": "^NEPSE",
    "type": "index"
  },
  {
    "symbol": "NABBC",
    "full_name": "NEPSE:NABBC",
    "description": "Narayani Development Bank Limited",
    "exchange": "NEPSE",
    "ticker": "NABBC",
    "type": "stock"
  },
  {
    "symbol": "NABIL",
    "full_name": "NEPSE:NABIL",
    "description": "Nabil Bank Limited",
    "exchange": "NEPSE",
    "ticker": "NABIL",
    "type": "stock"
  }
]
[]

A bare empty array — never null, never {}. Enforced in the handler:

if results == nil {
    results = []SearchResult{}
}
writeJSON(w, http.StatusOK, results)
type SearchResult struct {
    Symbol      string `json:"symbol"`       // ticker (includes `^` for indices)
    FullName    string `json:"full_name"`    // "NEPSE:<symbol>"
    Description string `json:"description"`  // company/index/sub-index name
    Exchange    string `json:"exchange"`     // "NEPSE"
    Ticker      string `json:"ticker"`       // same as Symbol
    Type        string `json:"type"`         // "stock" | "index"
}

For stocks the ticker comes from nepse_companies.symbol. For indices and sub-indices the SQL prepends the ^ ('^' || tv_symbol) so the returned symbol can be round-tripped into /symbols or /history unchanged. full_name is always "NEPSE:" + symbol.

Unified search (stocks + indices + sub-indices)

A single SearchForTV query UNIONs three branches and ranks with the same prefix-boost shape. Postgres forbids expression-based ORDER BY on a bare UNION, so the branches are wrapped in a subquery so the outer CASE can run against the merged column set:

SELECT tv_symbol, description, kind
FROM (
    (
        SELECT symbol AS tv_symbol, name AS description, 'stock'::text AS kind
        FROM nepse_companies
        WHERE status = 'A'
          AND (symbol ILIKE '%' || @search || '%' OR name ILIKE '%' || @search || '%')
    ) UNION ALL (
        SELECT '^' || tv_symbol AS tv_symbol, index_name AS description, 'index'::text AS kind
        FROM nepse_indices
        WHERE tv_symbol ILIKE '%' || @search || '%'
           OR index_name ILIKE '%' || @search || '%'
           OR ('^' || tv_symbol) ILIKE '%' || @search || '%'
    ) UNION ALL (
        SELECT '^' || tv_symbol AS tv_symbol, name AS description, 'index'::text AS kind
        FROM nepse_sub_indices
        WHERE tv_symbol ILIKE '%' || @search || '%'
           OR name ILIKE '%' || @search || '%'
           OR ('^' || tv_symbol) ILIKE '%' || @search || '%'
    )
) AS merged
ORDER BY
  CASE WHEN tv_symbol ILIKE @search || '%' THEN 0 ELSE 1 END,
  tv_symbol
LIMIT @lim;

Each index branch matches against both the bare tv_symbol (so typing NEP still finds ^NEPSE) and the ^-prefixed form (so typing ^NEP also works). The Go handler maps the kind column into the JSON type field (stockstock, indexindex).

Ranking examples

For query=NAB (stocks only — no indices match):

RankSymbolWhy
1NABBCsymbol starts with NAB
2NABILsymbol starts with NAB
3NABILD2089symbol starts with NAB
4NABILD87symbol starts with NAB
5KDBYmatches because name = "Kumari Dhanabriddhi Yojana" contains nab

For query=BANK:

RankSymbolKindWhy
1^BANKINGindexprefix match on tv_symbol
2BCBLstocksymbol prefix; name contains BANK
3KBBLstockname contains "Bank"
^DEVBANKindexsubstring match via ^-prefixed form

For query=^ (browse all indices):

All 4 top-level indices + 13 sub-indices appear; no stocks match because companies don't contain ^.

Graceful degradation

/search never returns 5xx or 504 to the client. A DB error or timeout degrades silently to []. Rationale: a 500 response would blank the library's search UI for the rest of the session; an empty array just means "no results right now."

results, err := h.svc.SearchSymbols(ctx, query, limit)
if err != nil {
    slog.Warn("tradingview: search degraded", "error", err, "query", query)
    writeJSON(w, http.StatusOK, []SearchResult{})
    return
}

This applies to all error classes including timeouts — the server-side error is logged, the browser sees [].

Limit clamping

Values are normalized in the handler before hitting the service:

Raw inputEffective
omitted30
0 or negative30
1100unchanged
> 10030
non-numeric (abc)30

This matches UDF's expected behavior: a sane default for browse-mode and no way for a misbehaving frontend to fetch unbounded result sets.

Caching

/search is not Redis-cached — queries are highly variable (every keystroke). Nginx is configured with a 1-minute proxy_cache_valid 200 which is usually enough to absorb a burst of identical requests from a single session.

Verification

# Bare array, prefix-boost visible
curl -s "$BASE/search?query=NAB&limit=5" | jq .

# Empty query returns first N alphabetically
curl -s "$BASE/search?limit=3" | jq '[.[].ticker]'
# ["ACLBSL", "ADBL", "ADBLD83"]

# No matches → empty array, not null
curl -s "$BASE/search?query=ZZZZZZZ"
# []

# limit=9999 clamps to default 30
curl -s "$BASE/search?query=A&limit=9999" | jq length
# 30

Implementation

  • Handler: internal/modules/nepse/tradingview/handler.go(*Handler).Search
  • Service: internal/modules/nepse/tradingview/service.go(*Service).SearchSymbols
  • Type: internal/modules/nepse/tradingview/types.goSearchResult
  • sqlc: SearchForTV in internal/platform/database/queries/charts.sql (companies + indices + sub-indices UNION)