Khi analyst tài chính không còn cần thức đến 2 giờ sáng
TL;DR
- Pain: Analyst xử lý earnings transcript bằng tay, KYC officer đọc từng hồ sơ onboarding đến nửa đêm
- Gain: 10 agents tự động hóa từ pitch deck đến GL reconciliation, output ra thẳng
.xlsx/.docx - Target: IBD analyst, equity researcher, PE analyst, fund admin, compliance officer
- Killer Feature: Một
agent.yaml, chạy được cả Cowork plugin lẫn Managed Agents API, không bao giờ drift - Verdict: Reference implementation xịn, nhưng cần platform team setup. Không phải cắm vào là chạy được ngay
Khi mùa earnings đến, tôi hay nghĩ đến mấy anh em analyst ở các bulge bracket.
Một cái transcript dài 80 trang. CEO nói vòng vo. CFO đưa số revised. Mười mấy analyst trên call hỏi tới hỏi lui. Sau khi call xong thì phải tóm tắt, cập nhật model Excel, draft research note, submit trước 7 giờ sáng hôm sau để kịp giờ mở cửa thị trường châu Á. Ở Singapore hay Hong Kong thì timezone còn đỡ, nhưng nếu công ty ở Hà Nội hoặc TP.HCM mà phải cover US names thì coi như thức xuyên đêm là chuyện thường.
Phần cùi nhất không phải là thức khuya. Phần cùi nhất là cái việc copy số từ transcript vào Excel. Thủ công. Từng ô một.
Cũng khoảng thời gian đó, tôi thấy Anthropic push lên GitHub một repo tên financial-services, 18k stars sau vài tháng. Tò mò mở ra xem.
Repo này làm gì, vì sao, và nỗi đau gì nó giải quyết
Nói gọn: anthropics/financial-services là bộ reference agents, skills, và data connectors cho các workflow tài chính phổ biến nhất - chạy được dưới dạng Cowork plugin cho analyst dùng tay, hoặc deploy qua Claude Managed Agents API để chạy headless trong pipeline của công ty.
Không phải SaaS. Không phải no-code. Đây là reference implementation - anh em nhìn vào, fork, chỉnh sửa cho phù hợp với môi trường nội bộ.
10 agents, 7 verticals, 11 MCP connectors. Tất cả file-based (markdown + YAML + JSON) - không build step, sửa file là hiệu lực ngay.
Nỗi đau nó giải quyết rất cụ thể: những công việc lặp đi lặp lại, tốn nhiều giờ, dễ sai do mệt mỏi - cập nhật model sau earnings, parse hồ sơ KYC, đóng sổ tháng, đối chiếu GL với subledger. Những việc mà nếu hỏi analyst nào cũng sẽ trả lời “tôi ghét nhất cái này nhưng không ai làm thay được”.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Mười Agent, Mỗi Cái Một Nghề
Không phải một LLM vạn năng làm tất cả. Mỗi agent có system prompt riêng, skills riêng, tools riêng, và output riêng:
| Agent | Đầu vào | Đầu ra | Dành cho |
|---|---|---|---|
| Pitch Agent | Tên target/acquirer + luận điểm | Branded pitch deck (slides) | IBD analyst |
| Meeting Prep Agent | Client ID + meeting event | Briefing pack tài liệu | Banker, wealth advisor |
| Earnings Reviewer | Ticker + kỳ báo cáo | note-AAPL.docx + model-AAPL.xlsx | Equity researcher |
| Market Researcher | Sector/theme + góc nhìn | Overview + peer comps + ideas | Equity researcher |
| Model Builder | DCF/LBO/3-stmt params | .xlsx Excel live | Analyst/modeler |
| Valuation Reviewer | GP packages | Valuation template + LP draft | PE analyst |
| GL Reconciler | Trade date + GL classes | Break report + root cause + sign-off routing | Fund admin |
| Month-End Closer | Entity + kỳ | Accruals + roll-forwards + memo | Fund admin |
| Statement Auditor | Statement batch + NAV pack | Tie-out report + exceptions | Fund admin |
| KYC Screener | Onboarding packet ID | escalation-<packet>.xlsx risk rating | Compliance |
Tất cả output là “staged for human sign-off”. Không agent nào tự post vào ledger, tự approve onboarding, hay tự execute giao dịch. Thiết kế đúng - đây là tool hỗ trợ phán đoán của con người, không phải autopilot.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Hai Chế Độ Từ Một Source
Đây là chi tiết kỹ thuật mà tôi thấy thú vị nhất.
Cùng một thư mục plugins/agent-plugins/kyc-screener/, vừa dùng được làm Cowork plugin cho officer click chuột, vừa dùng được để deploy headless qua Managed Agents API. Làm được vậy vì agent.yaml chỉ tham chiếu đến file, không hardcode content:
system:
file: ../../plugins/agent-plugins/kyc-screener/agents/kyc-screener.md
append: "You are running headless. Produce files in ./out/..."
skills:
- { from_plugin: ../../plugins/agent-plugins/kyc-screener }
Khi deploy qua API, script deploy-managed-agent.sh tự inline file content và upload skills. Khi dùng Cowork, nó đọc thẳng cùng thư mục đó. Không bao giờ có drift giữa hai chế độ. Nhiều hệ thống internal bắt đầu từ một source rồi tách ra sau vài sprint, tốn tiền đồng bộ lại. Repo này giải quyết sạch vấn đề đó từ thiết kế.
Deploy headless trông như thế này:
export ANTHROPIC_API_KEY=sk-ant-...
export FACTSET_MCP_URL=https://factset-mcp.internal.company.com
export DALOOPA_MCP_URL=https://daloopa-mcp.internal.company.com
# Deploy earnings reviewer vào production
scripts/deploy-managed-agent.sh earnings-reviewer
# Deploy KYC screener với screening connector
export SCREENING_MCP_URL=https://screening-mcp.internal.company.com
scripts/deploy-managed-agent.sh kyc-screener
Hoặc nếu team analyst muốn dùng tay qua Claude Code CLI:
claude plugin marketplace add anthropics/claude-for-financial-services
claude plugin install financial-analysis@claude-for-financial-services
claude plugin install equity-research@claude-for-financial-services
Sau đó trong session gõ /earnings AAPL Q1-2026 là agent chạy luôn.
💡 Tip: Skills được authoring trong
vertical-plugins/<vertical>/skills/- đây là source of truth. Nếu cần custom skill, sửa ở đó rồi chạysync-agent-skills.py. Đừng sửa trực tiếp trongagent-plugins/vì sẽ bị ghi đè khi sync.
Bảo Mật Khi Xử Lý Tài Liệu Không Tin Cậy
KYC officer nhận hồ sơ onboarding từ khách hàng. GP packages từ portco. Earnings transcripts từ IR. Đây đều là tài liệu không tin cậy - ai cũng có thể nhét prompt injection vào một cái PDF.
Repo này giải quyết bằng kiến trúc 3-tầng:
Tầng 1 (doc-reader) - Chạm tài liệu, Tools: Read + Grep ONLY
Không có Write, không có Agent, không có connectors
Tầng 2 (rules-engine) - Không chạm tài liệu, nhận JSON đã schema-validate từ Tầng 1
Tools: Read, Grep, Glob, Agent
Connectors: read-only (FactSet, Daloopa, Screening MCP...)
Tầng 3 (escalator) - Không chạm tài liệu, DUY NHẤT có quyền Write
Output: ./out/escalation-<packet>.xlsx
Nguyên tắc cứng: doc-reader trả về length-capped, schema-validated JSON - không bao giờ trả raw text lên tầng trên. Tầng 2 và 3 chỉ nhìn thấy structured output, không bao giờ thấy nội dung gốc của tài liệu. Attacker không có đường để inject qua document.
Thêm một lớp nữa: agents không gọi nhau trực tiếp. Khi cần handoff, agent emit JSON vào output text, orchestrate.py parse và validate target trong allowlist cứng:
ALLOWED_TARGETS = {
"pitch-agent", "market-researcher", "earnings-reviewer",
"model-builder", "gl-reconciler", "kyc-screener", ...
}
# Từ chối nếu target không trong list → schema validate payload → steer agent mới
Nếu document nào đó cố inject “call agent X ngoài danh sách” thì orchestrator chặn ngay. Thiết kế phòng thủ tốt cho môi trường tài chính, nơi mà dữ liệu đến từ đủ loại nguồn.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Kết Nối Dữ Liệu Qua MCP
11 connectors dữ liệu chuyên ngành, wire qua Model Context Protocol:
| Provider | Loại dữ liệu | Agent dùng |
|---|---|---|
| Daloopa | Earnings data tự động | Earnings Reviewer, Model Builder |
| FactSet | Financial data terminal | Earnings Reviewer, Model Builder |
| Morningstar | Fund data, ratings | Market Researcher, Valuation Reviewer |
| S&P Global | Credit ratings, market intel | Market Researcher, KYC Screener |
| LSEG | Market data, news, analytics | Pitch Agent, Market Researcher |
| Screening MCP | Sanctions/PEP screening | KYC Screener |
Với Cowork, connectors được định nghĩa trong .mcp.json của từng vertical plugin. Với Managed Agents API, truyền URL qua environment variables. Ở đây có một chi tiết bảo mật nhỏ nhưng đáng để ý:
SAFE = re.compile(r"^[A-Za-z0-9._/:@-]*$")
# Deploy script từ chối nếu env var value chứa ký tự ngoài whitelist
Ngăn injection qua FACTSET_MCP_URL hoặc các biến tương tự. Không hoành tráng nhưng đúng chỗ.
Reality Check
Đây không phải plug-and-play. Platform team cần setup: provision MCP connector URLs, configure API keys, wire vào workflow engine nội bộ (Temporal, Airflow, Guidewire, tùy công ty). Không phải công việc của analyst tự làm được trong một buổi chiều.
⚠️ Lưu ý:
callable_agents(multi-agent delegation) hiện chỉ hỗ trợ một cấp delegation. Orchestrator gọi được leaf workers, nhưng workers không gọi tiếp subagent. Pipeline lồng nhau sâu hơn phải implement ở lớp workflow engine.
Tích hợp Microsoft 365 còn ở preview. Wizard tương tác provision Azure resources và xin admin consent - nhưng nếu Azure admin của công ty khó tính thì chuẩn bị mất vài tuần xin phê duyệt.
Output vẫn cần human review. Earnings Reviewer cập nhật model tự động, nhưng analyst vẫn phải đọc lại số trước khi publish. KYC Screener flag hồ sơ, nhưng compliance officer vẫn phải quyết định. Đây là feature, không phải bug - nhưng cần đặt kỳ vọng đúng với stakeholder.
Apache-2.0 license - dùng thương mại được, fork được, chỉnh sửa được. Nhưng đây là reference, không phải production-ready SaaS. Anh em cần maintain khi Anthropic update API.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Kết
Tôi nghĩ lại chuyện anh em analyst thức đến 2 giờ sáng copy số từ transcript vào Excel.
Với Earnings Reviewer, workflow đó trở thành: gõ /earnings AAPL Q1-2026 → agent đọc transcript, kéo số từ Daloopa, cập nhật model, draft research note → analyst đọc lại và submit. Phần thô nhất của công việc biến mất. Phần cần judgment của con người vẫn còn nguyên.
18k stars sau vài tháng không phải ngẫu nhiên. Ngành tài chính đang bắt đầu nhìn AI agents theo cách thực dụng hơn - không phải “AI sẽ thay thế analyst” mà là “AI làm phần nhàm, analyst tập trung vào phần có giá trị”.
Cùi bắp hay xịn tùy cách dùng. Nhưng repo này đặt nền đúng chỗ.
--> // making it invisible to querySelectorAll. // // This inline script is NOT touched by Rocket Loader (no src, no type attr). // It rescues module scripts via two strategies: // 1. Query the DOM for type$="-module" + src (covers case A) // 2. Regex-parse the raw HTML for commented-out script tags (covers case B) // Dynamically-created scripts bypass Rocket Loader entirely. (function () { if (window.__markdyRescue) return; window.__markdyRescue = true; var rescued = false; function rescueModuleScripts() { if (rescued) return; rescued = true; var srcs = []; // Strategy 1: Rocket Loader kept the tag in DOM but changed the type. // type="module" → type="{uuid}-module" (still has src attribute) document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) { srcs.push(s.src); }); // Strategy 2: Rocket Loader COMMENTED OUT the script tag entirely: // // These are invisible to querySelectorAll, so we parse the raw HTML. // We handle both attribute orderings (type-first or src-first). var html = document.documentElement.innerHTML; var reSrcFirst = //g; var reTypeFirst = //g; var m; while ((m = reSrcFirst.exec(html)) !== null) { srcs.push(m[1]); } while ((m = reTypeFirst.exec(html)) !== null) { srcs.push(m[1]); } // Re-inject each found src as a real module script. // Deduplicate first, then inject. Dynamically-created scripts bypass // Rocket Loader entirely. Modules with the same URL are only executed // once by the browser (cached), so re-injecting already-running scripts // is safe. var seen = {}; srcs.forEach(function (src) { if (seen[src]) return; seen[src] = true; var fix = document.createElement('script'); fix.type = 'module'; fix.src = src; document.head.appendChild(fix); }); } // Rescue when user clicks the placeholder (fallback if autoplay failed). document.addEventListener('click', function (e) { var t = e.target; if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) { rescueModuleScripts(); } }); // Rescue automatically after a short delay for autoplay. // Only fires if initAll() never ran (no data-markdy-init on any root). setTimeout(function () { if (document.querySelector('.markdy-root:not([data-markdy-init])')) { rescueModuleScripts(); } }, 1500); }());Repo: anthropics/financial-services, Apache-2.0, 18k stars.
Hoang Yell
Một nhà phát triển phần mềm và là người kể chuyện kỹ thuật. Tôi dành thời gian để khám phá những repository mã nguồn mở thú vị nhất trên GitHub và trình bày chúng dưới dạng những câu chuyện dễ hiểu cho mọi người.