Date: 2026-06-29
SDK: futu-api>=10.8.6808
Series: Examples 107–114
Status: Design phase — not yet implemented
Eight composite examples that combine multiple new SDK 10.8.6808 APIs into non-trivial analysis flows. Each example demonstrates a real-world trading or research workflow by chaining 2–6 API calls with data transformation logic.
| # | Name | APIs | Theme |
|---|---|---|---|
| 107 | Earnings Surprise Dashboard | 4 | Financials + Research |
| 108 | Short Squeeze Risk Scanner | 3 | Short Volume + Brokers |
| 109 | Insider Activity Monitor | 4 | Shareholders + Insiders |
| 110 | Dividend Capture Calendar | 3 | Corporate Actions |
| 111 | Company Health Score | 4 | Fundamentals + Financials |
| 112 | Option Flow Screener | 3 | Options Vol + Screen |
| 113 | Institutional Flow Tracker | 4 | Shareholders + Brokers |
| 114 | Valuation Heat Map | 3 | Valuation + Screener |
import sys, time, json, logging
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import futu as ft
from connect import create_quote_context
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logger = logging.getLogger(__name__)| Return Pattern | How to Unpack | Which APIs |
|---|---|---|
(RET_OK, DataFrame) |
ret, df = ctx.api(...) |
earnings_price_move, earnings_price_history, top_ten_buy_sell_brokers, shareholders_holding_changes, shareholders_institutional, insider_trade_list, insider_holder_list, option_volatility, option_exercise_probability, company_profile, company_executives |
(RET_OK, raw_dict) |
ret, data = ctx.api(...) |
financials_statements, research_analyst_consensus, corporate_actions_dividends, corporate_actions_stock_splits, valuation_detail, company_executive_background, company_operational_efficiency |
(RET_OK, us_df, hk_df) |
ret, us_df, hk_df = ctx.api(...) |
get_short_interest, get_daily_short_volume |
(RET_OK, dict) with nested DataFrames |
ret, d = ctx.api(...); d["hk_buy_back_list"] |
corporate_actions_buybacks |
(RET_OK, (last_page, all_count, DataFrame)) |
ret, (last, count, df) = ctx.api(...) |
get_option_screen |
(RET_OK, (last_page, all_count, data_list)) |
ret, (last, count, data_list) = ctx.api(...) |
get_stock_screen |
def safe_call(ctx, label, func, *args, **kwargs):
"""Call an API method safely, logging errors without crashing."""
try:
result = func(*args, **kwargs)
ret = result[0]
if ret != ft.RET_OK:
logger.warning("%s: ret=%d msg=%s", label, ret, result[1] if len(result) > 1 else "?")
return None
return result
except Exception as e:
logger.error("%s exception: %s", label, e)
return Nonedef fetch_all(ctx, api_func, code, max_pages=5):
"""Paginate through an API that returns (ret, df) with df.attrs['next_key']."""
all_data = []
next_key = None
for page in range(max_pages):
result = safe_call(ctx, api_func.__name__, api_func, code, next_key=next_key)
if result is None:
break
ret, df = result[0], result[1]
if df is not None and not df.empty:
all_data.append(df)
next_key = df.attrs.get("next_key") if hasattr(df, "attrs") else None
if not next_key:
break
return pd.concat(all_data, ignore_index=True) if all_data else Noneassert trd_ctx is not None, "This example requires a trade context"- What it demonstrates (3 bullet points)
- SDK APIs used (table)
- How to run (1 command)
- Expected output (2-3 sample lines)
- Risk level
Directory: examples/107_earnings_surprise/
Fetch financial statements for a stock, compare actual EPS against analyst consensus, show historical earnings price reactions, and compute a composite "surprise score." Single-stock report card.
| API | Return Type | Purpose |
|---|---|---|
get_financials_statements() |
(ret, dict) |
Actual EPS, revenue |
get_research_analyst_consensus() |
(ret, dict) |
Consensus EPS estimates |
get_financials_earnings_price_move() |
(ret, DataFrame) |
Post-earnings price reaction |
get_financials_earnings_price_history() |
(ret, DataFrame) |
Pre-earnings volatility + IV crush |
1. User provides stock code (default: US.NVDA)
2. get_financials_statements(code, 0, 0) → dict with income_statement list
→ Extract: fiscal_year, net_profit, revenue, eps (basic + diluted)
3. get_research_analyst_consensus(code) → dict with analyst_consensus list
→ Extract: current_eps_estimate, current_target_price
4. get_financials_earnings_price_move(code) → DataFrame
→ Extract: price change at T=0, T+1, T+5 (before vs after earnings)
5. get_financials_earnings_price_history(code) → DataFrame
→ Extract: predict_vola_ratio, option_iv_crush, volume
6. Compute surprise_score:
surprise_bps = (actual_eps - consensus_eps) / |consensus_eps| * 10000
price_reaction_bps = max(|move_0|, |move_1|)
volatility_impact = iv_crush_pct
→ Composite: weighted (40% surprise, 30% reaction, 30% vol impact)
7. Print dashboard:
=== EARNINGS SURPRISE DASHBOARD: US.NVDA ===
Period | Actual EPS | Consensus | Surprise bps | Price Move T+1 | IV Crush
2025-Q4 | $1.25 | $1.10 | +1364 | +3.2% | -18.5%
2025-Q3 | $0.95 | $1.02 | -686 | -2.1% | -12.3%
COMPOSITE SURPRISE SCORE: 73/100 (Strong Beat)
- No analyst consensus for this stock (may be too small) → skip surprise calc
- Financial statements not available (new IPO) → print historical price reactions only
- EPS = 0 causing division by zero → use absolute change instead of bps
- No earnings price history (never had earnings) → skip that section
- Multiple fiscal periods → show last 4 quarters, oldest first
Given: code = "US.NVDA"
When: script runs
Then: prints a table with ≥1 row of earnings data
Then: each row shows: period, actual EPS, consensus (or "N/A"), surprise bps
Then: price move and IV crush columns are populated
Then: composite surprise score is printed at the bottom
Then: handles stocks with no consensus data without crashing
examples/107_earnings_surprise/
├── main.py # entry point, dashboard logic
└── README.md
None — read-only.
Directory: examples/108_short_squeeze/
Scan a list of US stocks and rank them by short squeeze risk using three metrics: short interest ratio (SI%), short volume ratio, and broker concentration changes. Highlights stocks with rising squeeze pressure.
| API | Return Type | Purpose |
|---|---|---|
get_short_interest() |
(ret, us_df, hk_df) |
SI%, days to cover |
get_daily_short_volume() |
(ret, us_df, hk_df) |
Daily short volume ratio |
get_top_ten_buy_sell_brokers() |
(ret, DataFrame) |
Broker net flow |
1. Scan list (configurable — default: GME, AMC, NVDA, TSLA, AAPL, PLTR, SNAP)
2. For each stock:
a. get_short_interest(code) → us_df
- shares_short, short_percent (SI%), days_to_cover, avg_daily_share_volume
b. get_daily_short_volume(code) → us_df
- total_shares_short, short_percent (short vol %), volume
c. get_top_ten_buy_sell_brokers(code, days_before=5) → DataFrame
- net_vol, broker_name, buy_sell_type
→ compute: top_broker_concentration = top 3 net_vol / total net_vol
3. Compute squeeze_score (0–100):
- SI% > 20% → 40 pts | SI% > 40% → 60 pts
- days_to_cover > 5 → 15 pts | > 10 → 25 pts
- short_vol_ratio > 30% → 15 pts | > 50% → 25 pts
- broker_concentration rising → 15 pts
4. Sort by squeeze_score descending
5. Print table:
=== SHORT SQUEEZE RISK SCANNER ===
Rank | Stock | SI% | Days2Cover | ShortVol% | BrokerConc | Score | Signal
1 | GME | 42.1% | 12.3 | 55.2% | 0.68 | 89 | 🔴 HIGH
2 | AMC | 28.5% | 8.1 | 38.7% | 0.55 | 62 | 🟡 WATCH
- Not a US stock → skip (short interest APIs return US-only data)
- No short data for this stock → print "No short data available"
- Broker data unavailable (needs LV1) → skip broker concentration, reduce max score
- Zero volume day → skip that stock, log warning
- Pagination: get_short_interest and get_daily_short_volume may return many rows → use latest
Given: stock list = ["US.GME", "US.AMC", "US.NVDA"]
When: script runs
Then: prints a sorted rank table with ≥2 rows
Then: each row shows: stock, SI%, days to cover, short vol %, score
Then: scores are 0–100 scale
Then: handles stocks with incomplete data gracefully
Then: total runtime < 30s for 7 stocks
examples/108_short_squeeze/
├── main.py
└── README.md
None — read-only.
Directory: examples/109_insider_monitor/
Detect unusual insider trading patterns for a stock. Cross-reference insider trade history with current holdings, flag cluster trades and abnormal sale/purchase ratios.
| API | Return Type | Purpose |
|---|---|---|
get_insider_holder_list() |
(ret, DataFrame) |
Current insider positions |
get_insider_trade_list() |
(ret, DataFrame) |
Historical insider trades |
get_shareholders_holding_changes() |
(ret, DataFrame) |
Broader shareholder changes |
get_company_executives() |
(ret, DataFrame) |
Executive roster for context |
1. User provides stock code (default: US.AAPL)
2. get_company_executives(code) → DataFrame
→ Cache: leader_name, position_name for later lookup
3. get_insider_holder_list(code) → DataFrame
→ Extract: holder_id, name, title, holder_quantity, holder_pct,
insider_bought_count, insider_sold_count
4. get_insider_trade_list(code) → DataFrame
→ For each insider: recent trades (trade_shares, min_trade_date, max_trade_date,
min_price, max_price, transaction_type)
→ Group by holder_id: total_bought, total_sold, net_change
5. get_shareholders_holding_changes(code) → DataFrame
→ Larger picture: period_text, share_change_num, share_ratio_change, holder_type
6. Compute anomaly signals:
a. Cluster buy: ≥3 insiders bought in same week → "BUY CLUSTER"
b. Unusual sale: insider sold >50% of holdings → "HEAVY SALE"
c. Insider vs executive: insider selling while executive roster unchanged → neutral
d. Net insider ratio: (bought_count - sold_count) / total_insiders
> 0.3 → "BULLISH" | < -0.3 → "BEARISH"
7. Print report:
=== INSIDER ACTIVITY MONITOR: US.AAPL ===
Total Insiders: 12 | Bought (30d): 3 | Sold (30d): 1 | Net Ratio: +0.17
Name | Title | Holdings | Bought | Sold | Net | Signal
Tim Cook | CEO | 3,200 | 0 | 0 | 0 | —
Luca Maestri | CFO | 850 | 200 | 0 | +200 | BUY
...
🔴 ALERT: Cluster buy detected (3 insiders, 2026-05-20 week)
🟡 ALERT: Executive sale — Kate Adams sold 100% of holdings
- No insider data (non-US stock) → print "Insider data only available for US stocks"
- Insider list empty → exit with message
- Trade list empty for some insiders → show holdings only
- Division by zero when computing ratios → handle with if-else
- Pagination: insider_trade_list may have next_key → fetch up to 3 pages
Given: code = "US.AAPL"
When: script runs
Then: prints insider count, bought/sold counts, net ratio
Then: lists individual insiders with holdings and recent trade activity
Then: flags any cluster buys or heavy sales as ALERTS
Then: handles stocks with no insider data gracefully
examples/109_insider_monitor/
├── main.py
└── README.md
None — read-only.
Directory: examples/110_dividend_calendar/
Build a forward-looking calendar of corporate action events (dividend ex-dates, stock splits, buyback windows) for a portfolio of stocks. Computes expected yield and suggests optimal entry/exit for dividend capture.
| API | Return Type | Purpose |
|---|---|---|
get_corporate_actions_dividends() |
(ret, dict) |
Dividend history + schedule |
get_corporate_actions_buybacks() |
(ret, dict) |
Buyback history + schedule |
get_corporate_actions_stock_splits() |
(ret, dict) |
Split history |
1. Portfolio list (configurable, default: HK.00005, HK.00002, HK.00066, HK.00001)
2. For each stock:
a. get_corporate_actions_dividends(code) → dict
→ Parse: ex_date, cash_dividend, dividend_currency, payment_date, record_date
→ Only keep rows where ex_date >= today AND cash_dividend > 0
b. get_corporate_actions_buybacks(code) → dict
→ Extract: hk_buy_back_list DataFrame (publ_date, end_date, buy_back_money,
buy_back_sum, percentage, high_price, low_price)
→ Flag: active buyback = end_date >= today
c. get_corporate_actions_stock_splits(code) → dict
→ Extract: split_list for reference (usually historical)
3. Merge into timeline sorted by ex_date:
=== DIVIDEND CAPTURE CALENDAR ===
Date | Stock | Event | Amount | Yield% | Notes
2026-06-05 | HK.00005 | EX-DIVIDEND | HKD 0.90 | 3.8% | Record: 06-06
2026-06-12 | HK.00002 | EX-DIVIDEND | HKD 0.65 | 4.2% | Record: 06-13
2026-06-15 | HK.00066 | BUYBACK ACTIVE | — | — | Window: -06-30
2026-07-10 | HK.00005 | SPLIT | 1:5 | — | Consolidation
STRATEGY: Buy HK.00005 on 06-03 (T-2), sell on 06-08 (T+1)
Expected capture: HKD 0.90 per share (net of ~0.1% price drop)
- No upcoming dividends → show "No upcoming dividend events"
- Buyback already ended → filter out, show only active windows
- Stock split in the past → skip, only show future splits
- Dividend amount = 0 → skip zero-value events
- Dividend yield computation: annual_dividend / current_price → requires fetching price
Given: portfolio = ["HK.00005", "HK.00002"]
When: script runs
Then: prints a calendar sorted by date with dividend ex-dates
Then: shows yield % for each dividend event
Then: marks active buyback windows
Then: suggests optimal entry/exit dates
Then: handles stocks with no upcoming events without crashing
examples/110_dividend_calendar/
├── main.py
└── README.md
None — read-only. Price data for yield computation uses get_stock_quote().
Directory: examples/111_company_health/
Compute a composite health score for a company by evaluating four dimensions: management quality (executive profile), operational efficiency, financial statements, and company profile. Output a radar chart (ASCII or text) and detailed breakdown.
| API | Return Type | Purpose |
|---|---|---|
get_company_profile() |
(ret, DataFrame) |
Industry, employees, description |
get_company_executives() |
(ret, DataFrame) |
Executive roster, tenure |
get_company_operational_efficiency() |
(ret, dict) |
ROE, ROA, margins |
get_financials_statements() |
(ret, dict) |
Revenue, profit, debt |
Dimension 1 — Management Quality (0–25 pts):
- CEO tenure > 5 years: +5
- CFO present: +5
- Board size 5–12: +5
- Average executive age 45–60: +5
- No recent insider selling cluster: +5
Dimension 2 — Operational Efficiency (0–25 pts):
- ROE > 15%: +10 | ROE > 8%: +5 | else: 0
- Operating margin > 20%: +10 | > 10%: +5
- Revenue per employee > industry median proxy: +5
Dimension 3 — Financial Health (0–25 pts):
- Revenue growth YoY > 10%: +8 | > 5%: +4
- Net profit margin > 15%: +8 | > 5%: +4
- Debt/equity < 0.5: +9 | < 1.0: +5
Dimension 4 — Company Profile (0–25 pts):
- Employee count > 10,000: +5 (large, established)
- Business description mentions growing keywords: +10 (AI, cloud, biotech, etc.)
- Industry in favored sector list: +10
1. User provides stock code (default: US.NVDA)
2. get_company_profile(code) → DataFrame
→ Build dict: name→value pairs (industry, employees, description, etc.)
3. get_company_executives(code) → DataFrame
→ Compute: exec_count, avg_age, avg_tenure, has_cfo, has_ceo
4. get_company_operational_efficiency(code) → dict
→ Parse: roe, operating_margin, net_margin, roa from return data
5. get_financials_statements(code, 0, 0) → dict
→ Extract latest: revenue, net_profit, total_assets, total_liabilities
6. Compute all 4 dimension scores → total (0–100)
7. Print report:
=== COMPANY HEALTH SCORE: US.NVDA ===
Dimension | Score | Max | Details
Management Quality | 20 | 25 | CEO 12yr tenure, CFO present
Operational Efficiency | 22 | 25 | ROE 42%, margin 48%
Financial Health | 18 | 25 | Revenue +18% YoY, D/E 0.3
Company Profile | 15 | 25 | AI sector, 30K employees
---------------------------------------------------
TOTAL | 75 | 100 | GRADE: A-
RADAR:
Mgmt
25
/ \
Profile Efficiency
\ /
25
Financial
- No operational efficiency data (non-US stock) → score 0 for that dimension
- No executives list → score 0, skip
- Financial statements not available → use what's available, reduce max score
- description not found in profile → skip keyword matching
- Division by zero in ratios → handle with try/except
Given: code = "US.NVDA"
When: script runs
Then: prints all 4 dimension scores with details
Then: prints total score and letter grade (A/B/C/D/F)
Then: ASCII radar chart is printed
Then: handles missing data gracefully (partial scores)
examples/111_company_health/
├── main.py # scoring engine + report
├── scoring.py # dimension scoring functions
└── README.md
None — read-only.
Directory: examples/112_option_flow/
Screen the entire US options market for unusual flow using implied volatility, exercise probability, and option screening data. Identifies options with compressed IV/HV spreads and high probability of exercise.
| API | Return Type | Purpose |
|---|---|---|
get_option_volatility() |
(ret, DataFrame) |
IV, HV, volatility premium |
get_option_exercise_probability() |
(ret, DataFrame) |
Strike probability |
get_option_screen() |
(ret, (bool, int, DataFrame)) |
Screen options by criteria |
1. Define screen criteria:
- Markets: US (OptMarketCategory.US)
- Filter: volume > 100, OI > 500
- Filter: IV_HV_ratio between 0.8–1.2 (compressed vol)
- Filter: delta 0.2–0.8 (not too deep ITM/OTM)
- Sort by: implied_volatility ASC (find cheapest vol)
2. OptionScreenRequest builder:
req = OptionScreenRequest([ft.OptMarketCategory.US])
req.add_option_filter(ft.OptionFilterType.OPTION_TYPE, ["CALL", "PUT"])
req.add_option_filter(ft.OptionFilterType.VOLUME, lower=100)
req.add_option_filter(ft.OptionFilterType.OPEN_INTEREST, lower=500)
req.add_option_filter(ft.OptionFilterType.IMPLIED_VOLATILITY, lower=0.1, upper=1.0)
req.add_option_filter(ft.OptionFilterType.DELTA, lower=0.2, upper=0.8)
req.add_sort(ft.OptionSortType.IMPLIED_VOLATILITY, desc=False)
req.add_option_retrieve(ft.OptionRetrieveType.IMPLIED_VOLATILITY)
req.add_option_retrieve(ft.OptionRetrieveType.HISTORY_VOLATILITY)
req.add_option_retrieve(ft.OptionRetrieveType.DELTA)
req.add_option_retrieve(ft.OptionRetrieveType.OPEN_INTEREST)
req.add_option_retrieve(ft.OptionRetrieveType.PREMIUM)
req.add_underlying_retrieve(ft.UnderlyingRetrieveType.STOCK_PRICE)
req.add_underlying_retrieve(ft.UnderlyingRetrieveType.STOCK_NAME)
3. get_option_screen(req) → (last_page, all_count, DataFrame)
→ Print top 20 rows
4. For top 5 results, drill down:
a. get_option_volatility(option_code) → DataFrame
→ Latest IV, HV, volatility_premium
b. get_option_exercise_probability(option_code) → DataFrame
→ strike_probability for each strike
5. Compute flow_score for each option:
- IV/HV near 1.0 (compressed): +30 pts
- Exercise probability > 40%: +30 pts (ITM exercise likely)
- Volume/OI ratio > 0.5 (active flow): +20 pts
- Premium < $2.00 (cheap entry): +20 pts
6. Print report:
=== OPTION FLOW SCREENER ===
# | Underlying | Strike | Type | Expiry | IV | HV | IV/HV | ExProb | Vol/OI | Score
1 | NVDA | 120 | CALL | 06-20 | 32.5% | 35.1% | 0.93 | 52% | 1.2 | 85
2 | AAPL | 200 | PUT | 06-20 | 28.1% | 30.2% | 0.93 | 38% | 0.8 | 72
DRILLDOWN: NVDA 120 CALL 06-20
IV History: 45d avg=34.2%, current=32.5% → vol compressing
ExProb: 52% → high probability of ITM at expiry
Recommendation: LONG CALL (premium rich in compressed vol)
- Option screen returns 0 results → expand criteria, print "No matching options"
- LV2 options subscription required → if empty, print "LV2 options data required"
- Drilldown API fails for a specific option → skip that one, continue
- ALL_COUNT > 200 → only first page processed (configurable)
- Option volatility API requires specific permissions → handle gracefully
Given: script runs
When: screen is executed
Then: prints top 20 options sorted by IV (lowest first)
Then: at least 3 drilldown rows with IV/HV and exercise probability
Then: flow scores computed and shown for drilldown results
Then: handles empty results gracefully
examples/112_option_flow/
├── main.py
├── screen_builder.py # OptionScreenRequest construction
└── README.md
None — read-only.
Directory: examples/113_institutional_flow/
Track institutional investor activity for a stock: who holds, how much they bought/sold, and cross-reference with broker-level flow data to detect accumulation or distribution patterns.
| API | Return Type | Purpose |
|---|---|---|
get_shareholders_institutional() |
(ret, DataFrame) |
Aggregate institutional holdings |
get_shareholders_holder_detail() |
(ret, DataFrame) |
Per-institution detail |
get_shareholders_holding_changes() |
(ret, DataFrame) |
Recent changes |
get_top_ten_buy_sell_brokers() |
(ret, DataFrame) |
Top broker net flow |
1. User provides stock code (default: US.AAPL)
2. get_shareholders_institutional(code) → DataFrame
→ Extract latest: institution_quantity, holder_quantity, holder_pct,
holder_quantity_change, holder_pct_change
3. get_shareholders_holder_detail(code) → DataFrame
→ Extract: holder_name, share_num, share_ratio, change_in_shares
→ Sort by share_ratio descending → top 10 holders
4. get_shareholders_holding_changes(code) → DataFrame
→ Filter: holder_type = INSTITUTIONAL
→ Group by: name, sum(share_change_num), max(holding_date)
→ Identify: top 5 accumulators (+), top 5 distributors (-)
5. get_top_ten_buy_sell_brokers(code, days_before=5) → DataFrame
→ net_vol, broker_name → which brokers driving the flow
6. Cross-reference:
- If institutional holdings rising AND top brokers buying → "STRONG ACCUMULATION"
- If institutional holdings falling AND top brokers selling → "STRONG DISTRIBUTION"
- If institutional rising but retail selling → "INSTITUTIONAL CONVICTION"
- Mixed signals → "MIXED — NO CLEAR DIRECTION"
7. Print report:
=== INSTITUTIONAL FLOW TRACKER: US.AAPL ===
Period | Inst. Qty | Qty Change | Holders | Holders Change | Pct
2026Q1 | 4,520,000 | +230,000 | 1,250 | +45 | 62.3%
TOP 10 INSTITUTIONAL HOLDERS
Rank | Institution | Shares | %Out | Change
1 | Vanguard Group | 520,000 | 7.2% | +15,000
2 | BlackRock | 480,000 | 6.6% | -2,000
3 | State Street | 380,000 | 5.2% | +8,000
TOP 5 ACCUMULATORS (last period)
+ Vanguard Group: +15,000
+ Fidelity: +12,000
TOP 5 DISTRIBUTORS
- Berkshire Hathaway: -25,000
BROKER FLOW (5d)
Broker | Net Vol | Signal
Morgan Stanley | +45,000 | BUYING
Goldman Sachs | -22,000 | SELLING
VERDICT: STRONG ACCUMULATION (institutions + brokers aligned)
- No institutional data (HK stock, small cap) → print "Institutional data limited"
- holder_detail not available → skip that section, continue
- broker data not available (LV1) → base verdict on institutional data only
- holder_detail returns empty DataFrame → show institutional aggregate only
- Institutional data may have next_key pagination → fetch up to 3 pages
Given: code = "US.AAPL"
When: script runs
Then: prints aggregate institutional holdings for latest period
Then: prints top 10 holders sorted by share count
Then: prints top 5 accumulators and top 5 distributors
Then: prints broker flow (or "N/A" if LV1 unavailable)
Then: prints verdict: one of 4 possible signals
Then: handles non-US stocks without crashing
examples/113_institutional_flow/
├── main.py
└── README.md
None — read-only.
Directory: examples/114_valuation_heatmap/
Screen a market (HK or US) for valuation outliers using PE/PB/PS percentiles. Group by industry/sector, compute median valuation percentile, and print a color-coded heatmap of cheap vs expensive sectors.
| API | Return Type | Purpose |
|---|---|---|
get_valuation_detail() |
(ret, dict) |
PE, PB, PS, PCF percentiles |
get_valuation_plate_stock_list() |
(ret, DataFrame) |
Stocks in a plate with valuation |
get_stock_screen() |
(ret, (bool, int, list)) |
Screen by valuation criteria |
1. User provides market (default: HK) and reference stock (default: HK.00700)
2. Phase 1 — Plate-level scan:
a. get_valuation_plate_stock_list(code) → DataFrame
→ For each stock in same plate: PE, PB, PS, market_cap
b. Group by stock → compute percentile rank within plate:
PE_percentile = rank(stock_pe) / count(plate_stocks) * 100
PB_percentile = rank(stock_pb) / count(plate_stocks) * 100
Average percentile = (PE_pctile + PB_pctile + PS_pctile) / 3
3. Phase 2 — Market-wide screen:
a. StockScreenRequest builder:
req = StockScreenRequest()
req.add_simple_field(ft.SimpleFilterField.MARKET, [market])
req.add_simple_field(ft.SimpleFilterField.SECURITY_TYPE, ["STOCK"])
req.add_retrieve_simple(ft.RetrieveSimpleField.PE)
req.add_retrieve_simple(ft.RetrieveSimpleField.PB)
req.add_retrieve_simple(ft.RetrieveSimpleField.MARKET_CAP)
req.add_retrieve_simple(ft.RetrieveSimpleField.STOCK_NAME)
req.add_retrieve_simple(ft.RetrieveSimpleField.INDUSTRY)
req.set_sort(ft.SortDirection.DESC, ft.SortPropertyType.SIMPLE,
[str(ft.RetrieveSimpleField.MARKET_CAP.value)])
b. get_stock_screen(req) → (last_page, all_count, data_list)
→ Parse data_list: each item has stock_id + results list
→ Build pandas DataFrame: stock_name, industry, PE, PB, market_cap
4. Phase 3 — Compute heat map:
a. Group by industry → median PE, median PB, median market_cap
b. Compute industry score:
relative_value = industry_median_PE / market_median_PE
if relative_value < 0.8 → "CHEAP" (green)
if 0.8–1.2 → "FAIR" (white)
if > 1.2 → "EXPENSIVE" (red)
5. Print report:
=== VALUATION HEAT MAP: HK ===
Reference: HK.00700 → PE=22.5 PB=5.2 (Market avg PE=15.3)
Sector | Median PE | Median PB | vs Market | Signal
Technology | 28.5 | 6.2 | 1.86x | 🔴 EXPENSIVE
Financials | 12.3 | 1.1 | 0.80x | 🟢 CHEAP
Real Estate | 10.1 | 0.8 | 0.66x | 🟢 CHEAP
Healthcare | 18.5 | 3.0 | 1.21x | 🔴 EXPENSIVE
Consumer Goods | 15.2 | 2.5 | 0.99x | ⚪ FAIR
TOP 5 CHEAPEST STOCKS (by PE percentile):
Rank | Stock | PE | PB | Market Cap | Industry
1 | HK.00001 | 5.2 | 0.6 | 185B | Real Estate
2 | HK.00005 | 6.8 | 0.9 | 220B | Financials
TOP 5 MOST EXPENSIVE STOCKS:
Rank | Stock | PE | PB | Market Cap | Industry
1 | HK.00700 | 28.5 | 6.2 | 3,800B | Technology
- No valuation data for some stocks (PE=0 or negative) → filter out, log count
- Screen results > 200 → first page only (configurable page_count)
- Industry grouping yields empty groups → skip, continue
- Reference stock not in plate → print "No plate data for reference stock"
- Market median PE = 0 (all stocks negative earnings) → use PB as primary
Given: market = "HK", reference = "HK.00700"
When: script runs
Then: prints sector heat map with ≥3 sectors
Then: each sector shows median PE, PB, vs market ratio
Then: signal column shows CHEAP/FAIR/EXPENSIVE
Then: top 5 cheapest AND most expensive stocks listed
Then: total runtime < 60s
examples/114_valuation_heatmap/
├── main.py
├── screen_builder.py # StockScreenRequest construction
└── README.md
None — read-only.
| Order | # | Name | Est. Effort | Complexity | Dependencies |
|---|---|---|---|---|---|
| 1 | 110 | Dividend Capture Calendar | 1 session | Low | pandas |
| 2 | 108 | Short Squeeze Risk Scanner | 1 session | Low | pandas |
| 3 | 107 | Earnings Surprise Dashboard | 1 session | Medium | pandas, dict parsing |
| 4 | 109 | Insider Activity Monitor | 1–2 sessions | Medium | pandas, pagination |
| 5 | 113 | Institutional Flow Tracker | 1–2 sessions | Medium | pandas, pagination |
| 6 | 114 | Valuation Heat Map | 2 sessions | High | StockScreenRequest, pandas |
| 7 | 112 | Option Flow Screener | 2 sessions | High | OptionScreenRequest, pandas |
| 8 | 111 | Company Health Score | 2 sessions | High | multi-source aggregation |
Rationale: Calendar and Short Squeeze are straightforward data fetch + compute. Earnings and Insider add dict parsing and pagination. Valuation and Option require protobuf request builders (complex API). Health Score is the most complex — aggregates 4 different return types into a composite score.
def extract_field(data: dict, path: list[str], default=None):
"""Safely traverse nested dict, e.g. extract_field(d, ["income_statement", 0, "eps"])"""
current = data
for key in path:
if isinstance(current, dict):
current = current.get(key, default)
elif isinstance(current, (list, tuple)) and isinstance(key, int):
current = current[key] if key < len(current) else default
else:
return default
return currentdef unpack_short_interest(result):
"""Unpack get_short_interest 3-tuple safely."""
if result is None:
return None
ret, us_df, hk_df = result
if ret != ft.RET_OK:
return None
return us_df if us_df is not None and not us_df.empty else Nonedef build_option_screen_request():
from futu import OptionScreenRequest, OptMarketCategory
from futu import OptionFilterType, OptionSortType, OptionRetrieveType, UnderlyingRetrieveType
req = OptionScreenRequest([OptMarketCategory.US])
req.add_option_filter(OptionFilterType.VOLUME, lower=100)
req.add_option_filter(OptionFilterType.OPEN_INTEREST, lower=500)
req.add_option_filter(OptionFilterType.IMPLIED_VOLATILITY, lower=0.1, upper=1.0)
req.add_sort(OptionSortType.IMPLIED_VOLATILITY, desc=False)
req.add_option_retrieve(OptionRetrieveType.IMPLIED_VOLATILITY)
req.add_option_retrieve(OptionRetrieveType.HISTORY_VOLATILITY)
req.add_option_retrieve(OptionRetrieveType.DELTA)
req.add_underlying_retrieve(UnderlyingRetrieveType.STOCK_NAME)
return reqdef build_valuation_screen_request(market):
from futu import StockScreenRequest, SimpleFilterField, SecurityType
from futu import RetrieveSimpleField, SortDirection, SortPropertyType
req = StockScreenRequest()
req.add_simple_field(SimpleFilterField.MARKET, [market])
req.add_simple_field(SimpleFilterField.SECURITY_TYPE, [SecurityType.STOCK])
req.add_retrieve_simple(RetrieveSimpleField.PE)
req.add_retrieve_simple(RetrieveSimpleField.PB)
req.add_retrieve_simple(RetrieveSimpleField.MARKET_CAP)
req.add_retrieve_simple(RetrieveSimpleField.STOCK_NAME)
req.add_retrieve_simple(RetrieveSimpleField.INDUSTRY)
req.set_sort(SortDirection.DESC, SortPropertyType.SIMPLE,
[str(RetrieveSimpleField.MARKET_CAP.value)])
return reqtry:
ctx = create_quote_context()
# ... example logic ...
except KeyboardInterrupt:
logger.info("Interrupted by user")
except Exception:
logger.exception("Unhandled error")
finally:
ctx.close()import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import futu as ft
from connect import create_quote_contextEach example main.py must:
- Accept
--codeor--marketCLI args (with sensible defaults) - Return exit code 0 on success, 1 on failure
- Print all output to stdout (not files)
- Complete within 120s