name: fireant-icb-bonds description: FireAnt ICB bond issuance data workflow for bonds-dashboard. Use when implementing or modifying industry bond data logic, sidebar industry ordering, ICB symbol fetching, duplicate symbol handling, grouped issuer/bond data, or industry charts based on FireAnt APIs.
FireAnt ICB Bonds
Use this workflow for industry tabs, issuer views, maturity views, and charts that depend on FireAnt bond data.
Vercel, Auth, And Env Rules
This project uses an older Vercel builds configuration. Keep API rewrites in vercel.json pointing to TypeScript files with the .ts suffix:
- Use
"src": "api/*.ts"for the@vercel/nodebuild entry. - Use
/api/proxy.ts?path=:path*, not/api/proxy?path=:path*. - Use
/api/auth.ts?path=:path*, not/api/auth?path=:path*. - Use
/api/news.ts,/api/news.ts?id=:id,/api/ai.ts?path=:path*, and/api/page-data.ts?view=:view. - Do not add nested exact Vercel handlers such as
api/auth/login.tsorapi/fireant/bonds/filter.tsunless the Vercel build strategy is changed and tested end to end. The working production path relies on wildcard rewrites into top-level handlers.
If deployed POST /api/auth/login or POST /api/fireant/bonds/filter returns 405 Method Not Allowed, first compare vercel.json against the working pattern above. In this repo, extensionless destinations caused POST requests to miss the intended Node function.
FireAnt OIDC login must use the deployed origin for callback URLs:
src/auth/oidc.tsxshould useAPP_URLfromsrc/api/config.ts, not readVITE_APP_BASE_URLdirectly.src/api/config.tsshould preferwindow.location.originwhenVITE_APP_BASE_URLis empty.- If
VITE_APP_BASE_URLis set tohttp://localhost:3000while running onbonds.fireant.vn, ignore that local value and use the browser origin. - On Vercel, set
VITE_APP_BASE_URL=https://bonds.fireant.vnor leave it empty. Never deploy production withVITE_APP_BASE_URL=http://localhost:3000. - FireAnt OIDC must allow production callbacks:
/signin-callback,/signout-callback, and/silent-renew-callbackonhttps://bonds.fireant.vn.
FireAnt REST base URL rules:
- Use only
https://restv2.fireant.vnfor FireAnt REST APIs. - Do not reintroduce
https://rest2.fireant.vnorhttps://rests.fireant.vn. - Server config should read
FIREANT_BASE_URL/VITE_FIREANT_BASE_URLwith fallbackhttps://restv2.fireant.vn. - Client config should read
VITE_FIREANT_BASE_URLwith fallbackhttps://restv2.fireant.vn.
TypeScript project rules:
- Keep
tsconfig.jsonscoped withincludeforserver.ts,vite.config.ts,api/**/*.ts, andsrc/**/*. - Keep
excludefordistandnode_modules. WithallowJs: true, omittingexcludemakes TypeScript/IDE scan built assets indist. baseUrl: "."is valid; if the IDE marks it, checkinclude/excludeand TypeScript server state before changing module paths.
Page Data API For AI
When chatbot or AI features need dashboard data, expose page-shaped payloads through api/_lib/page-data.ts and api/page-data.ts instead of reimplementing screen calculations inside the chatbot.
Supported routes:
GET /api/page-data/schemareturns endpoint descriptions and response sections.GET /api/page-data/market-overview?includeCashFlows=0|1&detailLimit=120returns cards and charts for market overview.GET /api/page-data/industry?industryId=Banking&includeCashFlows=0|1&detailLimit=150returns cards and charts for one industry.GET /api/page-data/issuer?q=STBsearches issuers.GET /api/page-data/issuer?symbol=STB&detailLimit=120returns issuer profile, cards, charts, and bonds.GET /api/page-data/watchlist?codes=BOND1,BOND2orPOST /api/page-data/watchlistwith{ "codes": [...] }hydrates watchlist page data.GET /api/page-data/maturity?days=365returns upcoming maturity page data.
Keep this API aligned with user-facing page structure: page, params, cards, charts, page-specific lists, and optional raw source data. Watchlist lives in browser localStorage, so server APIs must receive bond codes from the caller.
Source APIs
Always use the app proxy/helpers instead of hardcoding FireAnt URLs or bearer tokens.
Primary REST sources:
- Symbols by ICB:
fireantApi.getIcbSymbols(code)->GET /icb/{code}/symbols - Bonds by issuer:
fireantApi.getIssuerBonds(symbol)->GET /bonds/issuer/{symbol} - Bonds by industry:
fireantApi.getBondsByIndustryFilter({ icbCode, statusID })->POST /bonds/filter - Generic bond filter:
fireantApi.filterBonds(body)->POST /bonds/filter - Bond detail/cash flows:
fireantApi.getBond(code)->GET /bonds/{code} - Maturing bonds:
fireantApi.getMaturingSoon(days)->GET /bonds/stats/bonds/maturing-soon?days={days} - Issuer stats:
fireantApi.getTopDebtIssuers(top)->GET /bonds/stats/issuers/top-debt?top={top} - Industry stats:
fireantApi.getIndustries(top, level)->GET /bonds/stats/industries?top={top}&level={level} - Market overview level-1 industry stats:
fireantApi.getIndustries(1000, 1)->GET /bonds/stats/industries?top=1000&level=1
Legacy procedure endpoints are compatibility only:
bond_Filtermust not be the first choice for issuer, maturity, or industry loaders./api/fireant/bond_Filter?IssuerSymbol=STBis translated inapi/proxy.tsto/bonds/issuer/STB./api/fireant/bond_Filter?ICBCode=3010&StatusID=1is translated inapi/proxy.tstoPOST /bonds/filter./api/fireant/bond_Filter?MaturityDateFrom=...&MaturityDateTo=...is translated inapi/proxy.tsto/bonds/stats/bonds/maturing-soon.bond_StatisticsByIssueris translated to/bonds/stats/issuers/top-debt.bond_GetCategoryListis translated for issuer and industry category usage.
POST /bonds/filter Contract
Use POST /bonds/filter as the primary API for ad hoc bond filtering in UI filters and AI/system data requests.
Canonical JSON body:
{
"bondTypeID": 0,
"bondRateTypeID": 0,
"currencyID": 0,
"marketID": 0,
"icbCode": "string",
"issueFormID": 0,
"issueMethodID": 0,
"statusID": 0,
"issuerName": "string",
"issuerInstitutionID": 0,
"issuerSymbol": "string",
"isListing": 0,
"issueDateFrom": "2026-06-11T03:46:58.595Z",
"issueDateTo": "2026-06-11T03:46:58.595Z",
"maturityDateFrom": "2026-06-11T03:46:58.595Z",
"maturityDateTo": "2026-06-11T03:46:58.595Z",
"minBondRate": 0,
"maxBondRate": 0,
"minTenorMonths": 0,
"maxTenorMonths": 0,
"top": 0,
"sortBy": 0
}
Examples:
Filter by tenor:
{
"statusID": 1,
"minTenorMonths": 0,
"maxTenorMonths": 50
}
Filter by issue date:
{
"statusID": 1,
"issueDateFrom": "2026-06-11T03:46:58.595Z",
"issueDateTo": "2026-06-11T03:46:58.595Z"
}
Filter by maturity date:
{
"statusID": 1,
"maturityDateFrom": "2026-06-11T03:46:58.595Z",
"maturityDateTo": "2026-06-11T03:46:58.595Z"
}
Filter by bond yield / rate:
{
"statusID": 0,
"minBondRate": 0,
"maxBondRate": 0
}
ICB Industries
Use this exact industry set and keep it centralized in src/constants/industries.ts.
| Code | ID | Label | Level |
|---|---|---|---|
10 |
Technology |
Technology | 1 |
15 |
Telecommunications |
Telecommunications | 1 |
20 |
HealthCare |
Health care | 1 |
30 |
Financials |
Financials other | 1 |
3010 |
Banking |
Banking | 2 |
30202005 |
Securities |
Securities | 4 |
35 |
RealEstate |
Real estate | 1 |
40 |
ConsumerDiscretionary |
Consumer discretionary | 1 |
45 |
ConsumerStaples |
Consumer staples | 1 |
50 |
Industrials |
Industrials | 1 |
55 |
BasicMaterials |
Basic materials | 1 |
60 |
Energy |
Energy | 1 |
65 |
InfrastructureServices |
Infrastructure services | 1 |
Duplicate Symbol Rule
Each symbol must belong to exactly one industry. Deduplicate with a Set or Map.
Priority for overlapping financial symbols:
Securities(30202005)Banking(3010)Financials(30)
After this pass, Financials means financial symbols not already assigned to Securities or Banking.
Grouped Bond Data
For each final industry group:
- Fetch bonds by industry code with
fireantApi.getBondsByIndustryFilter({ icbCode, statusID: 1 }). - For
Financials, fetch30and exclude child industry bonds from3010and30202005. Check all returned ICB code shapes, includingicbCode,ICBCode,icbCodeLv1throughicbCodeLv4, nestedbondInfos,raw, andinfoObjvariants. - Deduplicate bonds by uppercase
bondCode. - Fetch bond detail with
getBond(bondCode)when chart values needtotalIssuedValue,currentListedValue, or cash flows. - Merge issuer symbol/name onto every bond.
- Group by industry ID and issuer symbol.
The final grouped shape should support:
symbols: deduped symbols assigned to the industry.bonds: deduped bonds for those symbols.issuerSummaries: issuer-level totals for ranking and market-share charts.industryStats: industry totals for KPI cards.projectedCashFlowBuckets: month buckets for cash-flow charts.
Industry pages should render in stages:
- Show cached industry data immediately.
- Load industry bond rows first so ranking and market-share cards can render early.
- Fetch bond detail in the background and replace cached data when it arrives.
- Keep chart state stable while data upgrades instead of resetting to empty.
Fetch Concurrency
When fetching a list of issuers or bond details, do not fetch sequentially and do not open unbounded Promise.all request storms.
Use mapWithConcurrency from src/utils/async.ts.
Recommended concurrency:
- Issuer bond lists:
6 - Bond detail / cash-flow fetches:
8to10 - Issuer profiles / translated names:
5
For expensive grouped industry loaders, keep an in-flight promise map alongside persistent cache. This prevents React StrictMode remounts, quick tab switches, or duplicate callers from starting the same industry calculation twice before the first result is cached.
Fetch And Calculation Contract
loadIssuerBondsByFilter(symbol)must useGET /bonds/issuer/{symbol}directly.loadMaturingBonds(days)must use/bonds/stats/bonds/maturing-soondirectly.loadBondsByIndustryFilter(icbCode, statusID)must usePOST /bonds/filterdirectly.loadBondFilterRows(query)should usePOST /bonds/filterfor tenor/date/rate/general market filters and may call legacybond_Filteronly as a compatibility fallback when REST filtering fails.- Normalize every API payload through
normalizeBondRowbefore UI mapping. - Cache by normalized query and dedupe in-flight calls with maps to avoid duplicate StrictMode requests.
- Convert VND values to billion VND only at UI/chart boundaries. Keep raw service totals in original API units unless a function name or interface explicitly documents billion VND.
- Calculate issuer summaries from deduped bonds. Use
currentListedValueas remaining debt fallback whentotalRemainingDebtis absent. - For cash-flow charts, use
cashFlowsfrom detail. If cash flows are absent, fallback to maturity principal usingcurrentListedValue || totalRemainingDebt || totalIssuedValue. - Market overview KPI cards and level-1 industry charts must come from
fireantApi.getIndustries(1000, 1), not grouped residual industry data. - Market overview KPI cards map level-1 industry stats by summing:
bondCount,totalIssuedVolume,totalIssuedValue, andtotalRemainingDebt. - Market overview value-by-industry chart uses
totalIssuedValueandtotalCurrentListedValue. - Market overview volume-by-industry chart uses
totalIssuedVolumeandtotalCurrentListedVolume. - Market overview total issued volume KPI and industry issued/listed volume KPIs display volume as millions of bonds at the UI boundary (
volume / 1_000_000); keep raw service values unchanged. - Market overview projected cash-flow charts must include x-axis
dataZoomfor both month and year modes. - Industry projected cash-flow charts must include x-axis
dataZoomfor both month and year modes. - Dashboard core warmup must not fetch grouped data for every industry. When a user opens or hovers a specific industry, fetch that industry first with
warmIndustryData(industryId)/loadIndustryBaseBondGroupData(industryId)/loadIndustryBondGroupData(industryId).
Industry Stats Contract
Cards and the interest chart should use /bonds/stats/industries by industry level when possible:
- Level 1 industries use
level=1. Bankinguseslevel=2.Securitiesuseslevel=4.
Map API fields to cards:
totalIssuedVolume-> issued volumetotalIssuedValue-> issued valuetotalCurrentListedVolume-> listed volumetotalCurrentListedValue-> listed valuetotalDebtFull-> original debttotalRemainingDebt-> remaining debt
Map API fields to the interest chart:
avgRate-> average rateavgCouponRate-> coupon ratefloatingRate-> floating rate
For Financials / financials other, calculate residual stats:
financialsOther = financialsLevel1 - bankingLevel2 - securitiesLevel4;
Apply direct subtraction for count/volume/value/debt fields. For rate fields, calculate the residual weighted by totalIssuedValue.
Chart Data Contract
Industry tabs should render directly from grouped industry data:
- Debt ranking in industry:
issuerSummariessorted bytotalRemainingDebt. - Debt market share in industry:
issuerSummariesshare oftotalRemainingDebt. - Remaining debt and bond count relation:
issuerSummaries.totalRemainingDebtandissuerSummaries.bondCount. - Monthly/yearly projected cash flow:
projectedCashFlowBuckets.
Do not use top_debt_200 as the primary source for industry charts when grouped ICB bond data is required.
Bond Detail Assessment
Use this rule for the Đánh giá section in the bond detail popup, specifically the item renamed to Mức lãi suất so với ngành.
Purpose
Evaluate a corporate bond's interest rate relative to peer bonds in the same industry.
Output
ThấpTrung bìnhCaonullwhen there is not enough peer data to evaluate
Normalization
remainingTerm = maturityDate - todaytermGroup:short_termwhen remaining term is less than 36 monthslong_termwhen remaining term is 36 months or more
rateType:fixedfloating
- Never compare fixed and floating bonds directly.
Peer Group Construction
Start with:
industry + rateType + termGroup
If peer bond count is below 5, expand in this order:
- Drop
termGroup, keepindustry + rateType - If still below 5, drop
rateType, keepindustry
Threshold Rules
- If
peerBondCount >= 5, use dynamic percentiles:lãi suất < P25->ThấpP25 <= lãi suất <= P75->Trung bìnhlãi suất > P75->Cao
- If
industryPeerCountis from 2 to 4, use the industry's median:- below median ->
Thấp - near median ->
Trung bình - above median ->
Cao - include
confidence: low
- below median ->
- If
industryPeerCount < 2, returnnull
Absolute Rules
- Do not use fixed hardcoded thresholds.
- Do not compare across other industries.
- Do not compare floating with fixed.
- Recalculate thresholds whenever bond data changes or on daily refresh.