)
1. 為什么需要認真對待 finviz-api 的篩選器做量化選股或者美股數(shù)據(jù)研究的人大概率都繞不開 finviz 這個站點。它的篩選器Screener能把基本面、技術(shù)面、行業(yè)屬性幾十個維度組合起來做條件過濾頁面響應(yīng)快、字段全手動點幾下就能篩出一批標的。但手動操作有兩個繞不過去的坎一是每次都要重復(fù)點選二是沒法把篩選結(jié)果直接喂給下游的分析腳本。finviz-api 這個庫就是來解決這個問題的。它把 finviz 的篩選參數(shù)封裝成 Python 對象讓你用代碼描述篩選條件然后一次性把結(jié)果拉回來直接變成結(jié)構(gòu)化數(shù)據(jù)。標題里說的從URL到數(shù)據(jù)指的正是這個鏈路你構(gòu)造的篩選條件最終會拼成一個帶查詢參數(shù)的 URL庫去請求這個 URL解析返回的 HTML 表格最后交給你一個可以繼續(xù)處理的列表。這篇文章面向的是已經(jīng)會用 pandas 做基礎(chǔ)數(shù)據(jù)處理、想把手動篩選流程自動化的朋友。我會把篩選器的參數(shù)體系拆開講清楚重點放在那些文檔里一筆帶過、但實際用起來很容易踩坑的高級用法上比如多條件組合的邏輯、字段值的精確寫法、分頁與限流、以及怎么把結(jié)果穩(wěn)定地落成 DataFrame。全文基于我自己的實操經(jīng)驗代碼可以直接抄去改。2. 篩選器的參數(shù)體系與核心思路拆解2.1 篩選器本質(zhì)上是一組鍵值對finviz 的篩選器 URL 長這樣https://finviz.com/screener.ashx?v111fcap_large,fa_div_o,sh_avgvol_o1000ft4o-marketcap。拆開看v111是視圖編號f后面跟的是篩選條件ft是信號過濾o是排序字段。finviz-api 做的事情就是讓你不用手拼這個字符串而是用finviz.Filters里的常量去組合。理解這一點很關(guān)鍵你寫的每一個篩選條件最終都會變成 URL 里的一個 token。所以當結(jié)果不符合預(yù)期時第一件事不是懷疑庫有 bug而是把庫實際請求的 URL 打印出來看看 token 拼得對不對。我習慣在調(diào)試階段把screener.url打出來核對這個習慣幫我省了大量排查時間。2.2 為什么用常量而不是裸字符串finviz-api 把每個篩選條件都定義成了Filters類下的屬性比如Filters.MarketCap.LARGE、Filters.Dividend.YIELD_OVER_3。這些常量的值其實就是 finviz 內(nèi)部的 token 字符串。用常量的好處有三個一是 IDE 能補全不用去背 token二是拼錯了會直接報 AttributeError而不是悄悄拼出一個無效 URL 返回空結(jié)果三是版本升級時 token 變了庫會跟著更新你的代碼不用動。我見過有人圖省事直接寫字符串cap_large短期能跑但一旦 finviz 調(diào)整了某個字段的命名這種硬編碼就會靜默失效——返回的是空列表而不是報錯排查起來非常痛苦。所以我的建議是能用常量就用常量實在找不到對應(yīng)常量再去查 token。2.3 視圖編號決定了返回哪些列v這個參數(shù)很多人會忽略但它直接決定了你拿到的數(shù)據(jù)有哪些列。常用的幾個視圖視圖編號含義典型列111概覽Overview代碼、公司名、行業(yè)、市值、PE、價格、漲跌幅、成交量121估值Valuation加上 P/B、P/S、PEG、EPS 等131財務(wù)Financial加上股息、ROA、ROE、利潤率等141所有權(quán)Ownership機構(gòu)持股、內(nèi)部持股比例151表現(xiàn)Performance各周期漲跌幅161技術(shù)TechnicalRSI、均線、Beta、ATR如果你需要股息率就必須用 131 或更靠后的視圖用 111 是拿不到這個字段的。這一點在文檔里沒有強調(diào)但實際做篩選時經(jīng)常需要根據(jù)目標字段反推視圖編號。我的做法是先確定要哪些列再選最小的能滿足需求的視圖編號避免拉回一堆用不上的數(shù)據(jù)。3. 高級篩選條件的寫法與實操要點3.1 多條件組合的默認邏輯是 AND當你傳入多個篩選條件時finviz 默認用 AND 連接也就是所有條件都要滿足。比如同時篩大盤股和股息率大于3%返回的是兩個條件都成立的標的。這個邏輯符合直覺但要注意同一個維度內(nèi)的多個值也是 AND這往往不是你想要的。舉個例子如果你想篩市值是大盤或者中盤直覺上會寫[Filters.MarketCap.LARGE, Filters.MarketCap.MID]但這樣拼出來的 URL 是cap_large,cap_midfinviz 會理解為市值既是大盤又是中盤結(jié)果必然是空。正確的做法是用f參數(shù)之外的方式或者干脆分兩次請求再合并。這是新手最容易踩的坑之一。3.2 數(shù)值型條件的區(qū)間寫法很多篩選條件支持區(qū)間比如市值、價格、成交量。finviz-api 里對應(yīng)的常量通常有_UNDER、_OVER、_BETWEEN這類后綴。以成交量為例如Filters.AverageVolume.OVER_1000K表示日均成交量超過 100 萬股。注意這里的單位是 K千股不是金額。如果你需要自定義區(qū)間比如價格在 10 到 50 之間標準常量可能覆蓋不到這時候有兩個辦法一是用最接近的常量組合二是直接構(gòu)造 token。我一般優(yōu)先用常量因為區(qū)間邊界 finviz 是固定的幾檔自己拼 token 容易拼出無效值。實測下來finviz 對價格區(qū)間的檔位劃分比較粗如果你的策略需要精細區(qū)間建議拉回數(shù)據(jù)后用 pandas 二次過濾而不是在篩選器層面死磕。3.3 信號過濾ft 參數(shù)的獨立用法ft是信號過濾和f是兩套體系。f篩的是基本面和技術(shù)面的靜態(tài)屬性ft篩的是當前是否出現(xiàn)某個信號比如今日跳空高開突破 50 日均線RSI 超賣等。這兩個參數(shù)可以同時用效果是疊加。finviz-api 里信號過濾通過finviz.Filters.Signal或者直接傳ft參數(shù)實現(xiàn)。我常用的一個組合是f篩出基本面達標的池子ft再篩出其中技術(shù)面出現(xiàn)買點的標的。這樣兩步走比把所有條件堆在一起更清晰也方便你分別調(diào)試——先確認基本面池子對不對再確認信號過濾有沒有生效。注意信號過濾的結(jié)果和當天行情強相關(guān)同一個腳本今天跑和明天跑結(jié)果可能完全不同。做回測時不要依賴ft它只反映此刻的狀態(tài)沒有歷史快照。3.4 排序與分頁的正確處理o參數(shù)控制排序比如o-marketcap表示按市值降序oprice表示按價格升序。負號代表降序這個規(guī)則要記牢。排序字段必須是當前視圖里存在的列否則會報錯或者被忽略。分頁是另一個容易被忽略的點。finviz 每頁默認返回 20 條超過的部分需要翻頁。finviz-api 的Screener對象在迭代時會自動處理分頁但如果你用screener.data直接取可能只拿到第一頁。我的做法是明確用循環(huán)迭代screener對象讓庫自己去翻頁這樣不會漏數(shù)據(jù)。同時要注意翻頁過程中如果篩選結(jié)果在變化比如盤中可能出現(xiàn)重復(fù)或遺漏做嚴肅分析時建議在收盤后跑。4. 從篩選到 DataFrame 的完整實操流程4.1 環(huán)境準備與基礎(chǔ)調(diào)用先裝庫然后跑一個最小可用的例子。這里我用一個大盤股 股息率超 3% 日均成交量超 100 萬的組合來演示。from finviz.screener import Screener filters [ cap_large, fa_div_o3, sh_avgvol_o1000, ] stock_list Screener(filtersfilters, tableValuation, order-marketcap) for stock in stock_list: print(stock[Ticker], stock[Price], stock[Dividend])這里tableValuation對應(yīng)視圖 121order-marketcap是降序。跑通之后你會看到一批標的和它們的估值字段。如果返回空先檢查 filters 里的 token 是不是有效——把stock_list.url打出來手動在瀏覽器里打開看看 finviz 怎么響應(yīng)。4.2 把結(jié)果轉(zhuǎn)成 DataFrameScreener對象本身可以迭代每個元素是一個字典。轉(zhuǎn) DataFrame 最直接的方式是import pandas as pd df pd.DataFrame(stock_list.data) print(df.shape) print(df.columns.tolist())但這里有個細節(jié)stock_list.data只包含當前已加載的頁。如果你要全部數(shù)據(jù)得先確保迭代完成或者用stock_list.data之前手動觸發(fā)加載。我的習慣是寫一個輔助函數(shù)把迭代結(jié)果收集成列表再轉(zhuǎn) DataFrame這樣最穩(wěn)rows [dict(stock) for stock in stock_list] df pd.DataFrame(rows)轉(zhuǎn)完之后數(shù)值列還是字符串需要做類型轉(zhuǎn)換。比如Price、Market Cap、Volume這些列finviz 返回的是帶單位或百分號的字符串得自己清洗。這一步?jīng)]有捷徑只能針對每個列寫轉(zhuǎn)換邏輯。4.3 數(shù)值列的清洗與單位換算finviz 返回的數(shù)值有幾個常見的坑市值帶B十億或M百萬后綴成交量帶K或M百分比帶%漲跌幅帶正負號。清洗邏輯我一般寫成這樣def parse_number(val): if pd.isna(val) or val -: return None val str(val).replace(%, ).replace(,, ) multiplier 1 if val.endswith(B): multiplier 1e9 val val[:-1] elif val.endswith(M): multiplier 1e6 val val[:-1] elif val.endswith(K): multiplier 1e3 val val[:-1] try: return float(val) * multiplier except ValueError: return None這個函數(shù)能覆蓋大部分情況但要注意有些列本身就是純數(shù)字字符串不帶單位直接 float 就行。建議先df.head()看一眼原始值再決定清洗策略不要盲目套用。4.4 篩選條件的動態(tài)構(gòu)造實際項目里篩選條件往往不是寫死的而是根據(jù)策略參數(shù)動態(tài)生成。比如用戶輸入一個市值下限你要把它映射到 finviz 的檔位。這時候就需要一個映射表用戶輸入億美元對應(yīng) token 3cap_micro3 - 20cap_small20 - 100cap_mid100 - 1000cap_large 1000cap_mega這種映射的好處是把連續(xù)參數(shù)離散化到 finviz 支持的檔位避免拼出無效 token。代價是精度損失但篩選器本來就是粗篩精篩交給 pandas 做這個分工是合理的。5. 常見問題與排查技巧實錄5.1 返回空結(jié)果的排查順序空結(jié)果是最常見的問題排查要按順序來不要亂試。我的固定流程是打印screener.url手動在瀏覽器打開確認 finviz 本身有沒有結(jié)果。如果瀏覽器有結(jié)果但代碼沒有檢查是不是分頁沒迭代完。如果瀏覽器也沒結(jié)果逐個刪減篩選條件定位是哪個條件導(dǎo)致的。檢查視圖編號和排序字段是否匹配比如用 111 視圖卻按dividend排序就會出問題。這個順序能覆蓋 90% 的空結(jié)果場景。剩下 10% 通常是網(wǎng)絡(luò)問題或者 finviz 臨時調(diào)整了頁面結(jié)構(gòu)等一會兒再試或者升級庫版本。5.2 字段缺失與列名變化finviz 偶爾會調(diào)整返回的列名比如把P/E改成PE或者某個字段在某些視圖下不返回。這時候stock[P/E]會拋 KeyError。穩(wěn)妥的寫法是用.get()pe stock.get(P/E) or stock.get(PE)另外不同視圖返回的列不一樣如果你在代碼里硬編碼了某個列名換視圖就會崩。建議在轉(zhuǎn) DataFrame 之后先打印df.columns確認一遍再寫后續(xù)邏輯。5.3 請求頻率與穩(wěn)定性finviz 對請求頻率是有隱性限制的短時間內(nèi)大量請求可能被臨時拒絕。我的經(jīng)驗是單次篩選盡量把條件寫全減少請求次數(shù)而不是分很多次小請求。如果確實需要多次請求中間加個time.sleep(1)到2秒的間隔實測下來比較穩(wěn)。還有一個技巧把常用的篩選結(jié)果緩存到本地 CSV下次跑的時候先讀緩存只有緩存過期或者條件變了才重新請求。這樣既省請求又加快了調(diào)試速度。5.4 常見問題速查表現(xiàn)象可能原因解決方向返回空列表條件互斥或 token 無效打印 URL 手動驗證逐個刪條件KeyError列名變化或視圖不匹配用 .get()確認視圖編號數(shù)據(jù)只有 20 條分頁未迭代用 for 循環(huán)迭代 Screener 對象數(shù)值無法計算字符串帶單位寫清洗函數(shù)處理 B/M/K/%請求被拒頻率過高加 sleep合并請求用緩存結(jié)果與網(wǎng)頁不一致盤中數(shù)據(jù)變化收盤后跑或固定時間點跑5.5 幾個我踩過的坑第一個坑是把信號過濾當成靜態(tài)條件。有次我寫了個腳本用ft篩突破均線的標的回測時發(fā)現(xiàn)結(jié)果好得離譜后來才意識到ft反映的是運行那一刻的狀態(tài)回測里根本不該用它。這個教訓讓我養(yǎng)成了區(qū)分靜態(tài)篩選和動態(tài)信號的習慣。第二個坑是忽略視圖編號導(dǎo)致字段缺失。早期我圖省事全用 111 視圖結(jié)果想算股息率時發(fā)現(xiàn)根本沒這列白白多寫了一套請求邏輯。后來我固定用 131 視圖做基本面篩選字段全省心。第三個坑是數(shù)值清洗不徹底。有一次市值列里混進了-表示無數(shù)據(jù)我的清洗函數(shù)沒處理導(dǎo)致整列變成 object 類型后續(xù)排序全亂。從那以后我的清洗函數(shù)第一行就是判斷空值和-。6. 把篩選器接入實際工作流的思路6.1 篩選器在量化流程中的位置篩選器適合做粗篩也就是從幾千只標的里快速縮小到一個幾百只的候選池。它的優(yōu)勢是快、字段全、不用自己維護數(shù)據(jù)源劣勢是條件粒度粗、無法做復(fù)雜計算。所以合理的分工是finviz 粗篩出候選池pandas 做精細過濾和因子計算最后再交給策略邏輯。我自己的流程是這樣的每天收盤后跑一次篩選器把候選池存成 CSV第二天開盤前讀 CSV用本地數(shù)據(jù)做二次過濾盤中只做監(jiān)控不再請求篩選器。這樣既利用了 finviz 的數(shù)據(jù)廣度又避免了實時請求的不穩(wěn)定。6.2 多組篩選條件的批量執(zhí)行如果你有多個策略每個策略對應(yīng)一組篩選條件可以寫成一個配置列表循環(huán)執(zhí)行strategies [ {name: dividend, filters: [cap_large, fa_div_o3]}, {name: growth, filters: [cap_mid, fa_epsyoy_o20]}, {name: value, filters: [cap_large, fa_pe_u20]}, ] for s in strategies: result Screener(filterss[filters], tableValuation) rows [dict(x) for x in result] pd.DataFrame(rows).to_csv(f{s[name]}.csv, indexFalse)這種批量執(zhí)行要注意請求間隔別一口氣跑十幾個策略容易被限流。我一般每個策略之間 sleep 兩秒跑完一輪也就幾十秒可以接受。6.3 結(jié)果落庫與增量更新候選池如果每天都要用建議落成本地數(shù)據(jù)庫而不是 CSV。CSV 的問題是每次全量覆蓋沒法做歷史對比。用 SQLite 就很合適輕量、無需額外服務(wù)還能按日期查詢歷史候選池。落庫的時候加一個snapshot_date字段這樣你可以對比昨天篩出來的和今天篩出來的有什么變化哪些是新進池的、哪些被剔除了。這個變化本身就是一個有用的信號比單看某一天的快照信息量大得多。6.4 一個完整的可復(fù)用腳本骨架把上面的東西串起來一個可復(fù)用的腳本大概長這樣import time import pandas as pd from finviz.screener import Screener def run_screen(filters, tableValuation, order-marketcap): result Screener(filtersfilters, tabletable, orderorder) rows [dict(x) for x in result] return pd.DataFrame(rows) def clean_df(df): # 針對具體列做清洗這里省略細節(jié) return df if __name__ __main__: filters [cap_large, fa_div_o3, sh_avgvol_o1000] df run_screen(filters) df clean_df(df) df.to_csv(candidates.csv, indexFalse) print(f共篩出 {len(df)} 只標的)這個骨架的好處是職責清晰run_screen只管拉數(shù)據(jù)clean_df只管清洗主流程只管編排。后續(xù)要加策略、加緩存、加落庫都在主流程里擴展不會把拉數(shù)據(jù)和清洗的邏輯攪在一起。7. 關(guān)于篩選器精度與數(shù)據(jù)時效的個人體會finviz 篩選器的檔位設(shè)計是它的特點也是它的限制。市值、成交量、股息率這些字段都是分檔的不是連續(xù)值。這意味著你沒法精確表達市值在 50 億到 80 億之間這種需求只能選最接近的檔位然后拉回來用 pandas 精篩。我一開始覺得這是缺陷用久了反而覺得合理——篩選器的定位就是快速縮小范圍精度交給下游各司其職。數(shù)據(jù)時效方面篩選器反映的是請求那一刻的狀態(tài)盤中會變。如果你的策略對時效敏感建議固定在一個時間點跑比如收盤后半小時這時候數(shù)據(jù)已經(jīng)穩(wěn)定。我試過盤中跑和收盤后跑結(jié)果差異不小尤其是涉及漲跌幅和成交量的條件。所以做嚴肅分析時我堅持收盤后跑把結(jié)果存下來第二天用存下來的數(shù)據(jù)而不是第二天早上重新請求。最后分享一個小技巧如果你需要某個字段但不確定它在哪個視圖里可以先用 111 視圖拉一只標的看返回的字典有哪些鍵再對照 finviz 網(wǎng)頁上的列名去猜視圖編號。這個方法比翻文檔快實測幾次就能摸清字段和視圖的對應(yīng)關(guān)系。