.env.example
MX_APIKEY=your_mx_api_key POLYGON_API_KEY=your_polygon_api_key
redyuan43/my-skills-repo · GitHub
Financial data science workflows for market data acquisition, Polygon.io queries, MX API A-share research, reproducible Python/Jupyter exploration, valuation scripts, Buffett-style investment review, critical review, reasoning trace generation, and optional Notion upload. Use when Codex needs to fetch financial data, analyze stocks, build investment memos, run the bundled finance scripts, or inspect the imported data-science references from data-science.zip.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add redyuan43/my-skills-repo --skill data-science설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
.env.exampleMX_APIKEY=your_mx_api_key POLYGON_API_KEY=your_polygon_api_key
.gitignore.env .env.local scripts/config/api_keys.json out/ **/__pycache__/ *.pyc
agents/openai.yamlinterface: display_name: "Data Science" short_description: "金融数据采集、估值分析、Jupyter探索与投资报告生成" default_prompt: "Use $data-science to fetch market data and draft a reproducible investment analysis."
assets/templates/financial-analysis/investment_memo_template.md--- 日期:yyyy-mm-dd 研究对象:[公司名称] - [股票代码] 当前股价:[本币真实股价] 主题结论:[一句话核心论点] --- ## 1. 核心结论与预期差 **市场共识:** [市场如何看待] **预期差:** [你的不同观点] **交易本质:** [一句话总结] ## 2. 商业模式与投资效率 ### 单体经济学 ### ROIC 拆解 ### 护城河压力测试 ## 3. CFO级财务体检 ### 资产质量辨析 ### 自由现金流真实性(关键:扣SBC前后差异) ### SBC深度检查:占比、增速vs营收、稀释率 ## 4. 管理层与股东回报 ### 真实股东收益率 公式:`(回购 + 分红 - SBC新增价值) / 市值` 若回购<SBC → 股东净稀释 ### 红旗警报 - SBC >10%营收为高风险 - 频繁更换CFO、关联交易、糟糕并购 ## 5. 估值与安全边际 ### 反向 DCF **格式和内容必须严格按照 `references/reverse-dcf.md` 第7节「投资备忘录中的标准输出格式」执行。** 必须包含: - Step 1: 参数预估依据(WACC/g_terminal/FCF基期/搜索范围,每项标注来源) - Step 2: 输入参数汇总表 - Step 3: 方案A — 标准FCF基期(含核心公式、7档试算表、二分法Python代码、隐含增速) - Step 4: 方案B — 正常化FCF(如有) - Step 5: 方案C — 隐含PE反推法(FCF为负时的备用) - Step 6: 判读结论(历史参考 + 矛盾分析 + 交叉验证) 参考案例: CROX 投资备忘录 5f 节 ### EPV / ARV ### ★ 格林沃尔德第三段 FV(护城河企业专用) ``` ROIC-WACC差 = ?% b = ?% n = ?年 FV = NOPAT × b × (ROIC-WACC)/WACC × [1-1/(1+WACC)^n]/WACC ARV/股 + EPV/股 + FV/股 = 内在价值/股 安全边际 = ? ``` ### 同业估值对比 ## 6. 下行风险预演 ## 7. 催化剂与击球区 ## 8. 三维扫描 | 维度 | /10 | 理由 | |------|-----|------| | 商业确定性 | | | | 资产健康度 | | SBC是扣分项 | | 估值吸引力 | | |
references/buffett-review/01-thinking-frameworks.md# Thinking Tools and Mental Models > When to read this file: when you need a thinking framework entry point, when you're unsure how to approach a complex investment decision, or when discussing the underlying logic of investment philosophy. Buffett's investment success is half from stock selection, half from **the way he thinks about problems**. These thinking tools are the underlying reason he makes the right decisions. --- ## Circle of Competence **Core: only make decisions in domains you truly understand.** > "I don't try to jump over 7-foot bars. I look around for 1-foot bars that I can step over." **What "truly understand" means:** - You can clearly explain the profit logic of the business - You understand the key variables that affect the industry (2–3, not 20) - You can anticipate the rough direction of the competitive landscape over 5–10 years - You know what circumstances would make your judgment wrong **The boundaries of the circle matter more than its size:** > "You only have to be able to evaluate companies within your circle of competence. The size of that circle is not very important; knowing its boundaries, however, is vital." - Admitting "I don't know" is a competitive advantage - Analysis outside your circle of competence is more dangerous than no analysis (it creates false confidence) - **Act decisively inside the circle, walk away outside it**: hesitating within your area of understanding is the real mistake - The circle can be expanded, but it takes time and real experience — not reading a few articles **Buffett's honest reflection on missing Amazon:** > "I was too dumb. I studied Amazon but didn't buy it. I underestimated the degree to which Bezos could accomplish what he set out to do." This is not a failure — it is honesty. He believed Bezos's achievements were beyond the range he could reliably predict. --- ## Inversion The thinking tool Munger emphasizes most: **don't ask "how do I succeed" — first ask "how do I guarantee failure."** > "Invert, always invert." — Charlie Munger **Applying inversion in investing:** Don't ask: why will this company go up? Ask first: **in what ways could this investment lose me money?** Five failure paths: 1. The moat is eroded (technological disruption, regulation, competition) 2. Management integrity problems 3. Financial leverage blows up at the bottom of a cycle 4. Purchase price too high — you lose money even if the company is great 5. Outside your circle of competence — you simply lack the judgment **Only when all failure paths are difficult to make a case for should you consider the investment thesis.** --- ## Mr. Market Graham's metaphor, used by Buffett throughout his life. **Imagine an emotional business partner who quotes you a buy/sell price every day:** - Sometimes wildly optimistic, quoting far above the true value of the business - Sometimes deeply pessimistic, quoting far below the true value of the business - You are under absolutely no obligation to accept his quote **Key insights:** - Mr. Market is a tool that serves you, not a teacher you should follow - When he is fearful, he offers you a buying opportunity - When he is greedy, he offers you a selling opportunity - His quotes are driven by emotion, not related to the intrinsic value of the business > "If your neighbor quoted you a price on his house every day, would you feel depressed just because he was in a bad mood and quoted low today?" > "Mr. Market doesn't mind being ignored. If his quote doesn't interest you today, he'll be back with a new one tomorrow." **Historical cases:** - **1973 Washington Post**: The Watergate scandal made Mr. Market deeply pessimistic; Buffett bought in for $10.6 million and it grew to hundreds of millions (the intrinsic value never changed) - **2008 Financial Crisis**: Mr. Market panicked to the extreme; Buffett deployed $5 billion into Goldman Sachs and $3 billion into GE, securing terms far above normal **The algorithmic era is even more favorable:** High-frequency trading makes short-term market swings more violent than in Graham's era — Mr. Market is now more manic than ever, representing a greater opportunity for patient investors. **In practice:** When the market drops sharply, first ask "has the intrinsic value of the company changed?" — not "should I cut my losses?" --- ## Long-term Orientation > "If you aren't willing to own a stock for ten years, don't even think about owning it for ten minutes." **Why a long-term perspective is a competitive advantage:** - Most market participants are driven by quarterly results, creating an arbitrage opportunity for the long-term view - The mathematical power of compounding: 10% annualized returns — 2.6x in 10 years, 17x in 30 years - **Berkshire's track record (1965–2022, 58 years):** Annual compounding of 19.8% vs. S&P 500's 9.9% — seemingly just a 10-percentage-point gap, but the cumulative result is **3,787,464%** vs. **24,708%** — this is the true face of compounding over time - Short-term volatility creates noise; long-term trends reflect fundamentals - Frequent trading generates friction costs (taxes, commissions, bad decisions) **Scale is the enemy of compounding growth:** > "Given our current size, such returns are impossible to sustain. Anyone who thinks otherwise should go into sales, but please, not into mathematics." — 1983 letter The larger the assets under management, the harder it is to generate excess returns. This is a mathematical law, not modesty. **Three prerequisites (all must be met simultaneously):** 1. The business has a durable moat and competitive advantage 2. Management is both capable and honest, with strong capital allocation ability 3. Purchased at a reasonable price below intrinsic value > "When we own portions of outstanding businesses with outstanding managements, our favorite holding period is forever." — 1988 letter **When it does not apply:** When the moat is destroyed, management deteriorates, or the price is severely overvalued. For mediocre businesses, time is an enemy, not a friend. > "What Coca-Cola and American Express have taught us is this: when you find a truly wonderful business, stick with it. Patience pays, and one wonderful business can offset the many mediocre decisions that are inevitable." — 2023 letter --- ## Munger's Lattice of Mental Models Munger's greatest influence on Buffett: **looking at the same problem from the perspective of multiple disciplines.** > "You need to have a latticework of mental models in your head, and then hang your actual experience on that latticework." — Munger **The most useful cross-disciplinary models in investing:** | Discipline | Model | Investment Application | |-----|-----|---------| | Physics | Critical mass | When network effects explode | | Biology | Ecological niche | Efficient scale moat | | Psychology | Social proof, loss aversion | Opportunities during market panic | | Mathematics | Compounding, probability | Long-term return and risk quantification | | Economics | Opportunity cost | Every investment competes with the best alternative | | Chemistry | Catalyst | What trigger is needed to unlock value | **Munger's core warning:** > "To the man with only a hammer, every problem looks like a nail." Having a diverse set of mental models is what prevents the blind spots of a single perspective. --- ## Independent Thinking > "You're not right because others agree with you, and you're not wrong because others disagree with you. You're right because your data and reasoning are right." **The danger of groupthink:** - Wall Street consensus is often right, but wrong at critical moments (extreme fear or extreme greed) - The best investment opportunities usually arise when most people are afraid to buy - Ideas that are mocked by the crowd are often the source of excess returns **How to maintain independence:** - Analyze for yourself first, then look at others' views - Don't change a well-reasoned judgment just because "everyone says so" - But also don't reject contrary opinions just because "I want it to be right" **Buffett's method:** Working in Omaha, not on Wall Street — physical distance helps him stay away from market noise and herd sentiment. --- ## Opportunity Cost Awareness Every investment decision is implicitly competing with the best alternative investment you could make. > "I always have a hurdle rate in my head. Every investment opportunity has to compete against it." **In practice:** - Holding cash is also a decision — it has an opportunity cost - Holding a mediocre stock means giving up the opportunity to hold an outstanding one - When buying back shares, the comparison is "buyback" vs. "a better acquisition opportunity" **Munger's standard:** > "Our standard for decisions is not 'is this good enough?' but 'is this the best option available?'" --- ## Patience as Edge > "The stock market is a device for transferring money from the impatient to the patient." **Buffett's waiting cases:** - 1969: Dissolved his partnership fund, believing the market offered no reasonably priced opportunities, and chose to wait - 2008 Financial Crisis: Made large purchases at the peak of fear (Goldman Sachs, GE) - Held Coca-Cola for over 30 years, unmoved by stock price swings or analyst recommendations **The right mindset for waiting:** - It is not idleness — it is waiting in a state of "ready to act at any moment" - When the opportunity arrives, act decisively and size up, rather than lowering your standards because "you've waited too long" ---
references/buffett-review/02-investment-philosophy.md# Core Investment Philosophy > When to read this file: When you need to understand Buffett's most fundamental investment beliefs, assess "intrinsic value," judge market efficiency, or address the question of concentration vs. diversification. ## Focus Investing **Core: Concentrate large amounts of capital in a small number of businesses you truly understand, rather than spreading it across dozens or hundreds.** Buffett believes that broad diversification is protection against ignorance. If you truly understand the businesses you invest in, diversification only reduces returns. > "We simply try to find outstanding businesses at sensible prices — not sensible businesses at outstanding prices. I cannot find a hundred businesses that meet my investment criteria. But concentrating in a handful of attractive opportunities, I feel quite comfortable." — 1976 Buffett Letter to Shareholders **The root cause of concentration is a high standard of stock selection criteria, not a deliberate strategy:** Buffett's four stock selection criteria (1976): 1. Favorable long-term economic characteristics 2. Competent and honest management 3. A purchase price attractive by private-ownership standards 4. An industry within one's circle of competence **Opportunities that simultaneously satisfy all four criteria are extremely rare — that is the fundamental reason for concentration.** **How focus investing reduces risk (a contrarian view):** > "We believe that a policy of portfolio concentration may well decrease risk if it raises, as it should, both the intensity with which an investor thinks about a business and the comfort level he must feel with its economic characteristics before buying into it." — 1993 Buffett Letter to Shareholders Here Buffett redefines risk: not as stock price volatility (the academic beta coefficient), but as the probability of permanent capital loss. Holding five or six deeply understood stocks carries less risk than holding a hundred stocks you barely know. **Rebuttal of beta theory:** > "Under this beta-based theory, a stock that has dropped sharply compared to the market — as had Washington Post when we bought it in 1973 — becomes 'riskier' at the lower price than it was at the higher price. Would that description have then made any sense to someone who was offered the entire company at a vastly-reduced price?" — 1993 Buffett Letter to Shareholders **Concentration vs. diversification — it depends on the investor:** > "When 'dumb money' acknowledges its limitations, it ceases to be dumb." — 1993 Buffett Letter to Shareholders Buffett clearly distinguishes between two types of investors: - **Investors capable of deep business research**: concentrate in a small number of excellent businesses - **Ordinary investors lacking analytical ability**: broad diversification (index funds) is the best choice **Proving conviction through action:** > "More than 99% of my net worth is in Berkshire, and I'm quite comfortable continuing to hold it — as I would be having my wife or foundation do the same." — 2003 Buffett Letter to Shareholders **Mocking over-diversification:** > "If you have a harem of forty women, you never get to know any of them very well." — 1984 Buffett Letter to Shareholders (quoting Billy Rose) > "I am not an advocate of Noahic diversification." — 1963 Buffett Letter to Partners **Practical examples:** - Partnership era: typically held heavy positions in five or six undervalued stocks (each 5%–10% of total assets); control-type investments were even more concentrated in a single enterprise - Berkshire era: in 1976, Washington Post, GEICO, and Kaiser Industries made up the bulk of the portfolio - Recent: Apple at one point comprised nearly 50% of Berkshire's equity portfolio **Common misconceptions:** - Concentration is not gambling — concentration is a rational decision after deep research; gambling is a blind bet made with insufficient information - Focus investing has prerequisites: truly understanding the business + sufficient financial strength to withstand volatility + psychological resilience --- ## Efficient Market Theory **Core: Buffett is the most famous long-term critic of the Efficient Market Hypothesis (EMH) — not in words, but proven false by 60 years of excess returns.** The Efficient Market Hypothesis (EMH) was systematized by Eugene Fama in the 1960s. Its central claim: stock prices already fully reflect all publicly available information, so no one can consistently achieve excess returns. **Buffett's critical distinction — the vast difference between "often" and "always":** > "Observing correctly that the market was frequently efficient, they went on to conclude incorrectly that it was always efficient. The difference between these propositions is night and day." — 1988 Buffett Letter to Shareholders Buffett never denies that markets are reasonably efficient "most of the time" — what he opposes is the extreme claim that they are "always" efficient. **63 years of excess returns as empirical rebuttal:** Graham-Newman Corporation (1926–1956) achieved an unleveraged average annual arbitrage return of 20%; subsequently Buffett continued the same principles at his partnership and Berkshire for 63 consecutive years of outperformance (the market averaged roughly 10% annually over the same period): > "A group that follows this approach, investing $1,000 with all earnings reinvested, would grow that sum to $400,000 at a 10% annual return or $97,000,000 at a 20% annual return. In our view, this is a statistically significant difference that belies a bit of name-calling." — 1988 Buffett Letter to Shareholders **The Superinvestors of Graham-and-Doddsville:** In 1984 Buffett delivered his lecture at Columbia Business School, "The Superinvestors of Graham-and-Doddsville," specifically to rebut the claim that long-term excess returns are merely luck: if the winners were randomly distributed across the entire country, you could call it luck; but if the winners are disproportionately concentrated in the same "intellectual village" — followers of the Graham-and-Dodd school — then it is no coincidence. **Academia's ostrich mentality:** > "Professors teaching EMH continued to do so with enthusiasm — and, doubtless, sincerity — after most of this flagging had occurred, and they will probably still be doing so after the next crash. A professor who spoke against EMH had roughly the same chance of advancement as Galileo did for becoming Pope." — 2006 Buffett Letter to Shareholders > "I suppose the Flat Earth Society would consider circumnavigation an annoying, but explicable, anomaly." — 2010 Buffett Letter to Shareholders **The spread of EMH actually helps value investors:** > "Thousands of students were sent out into life believing that on every day the price of every stock was 'right'... If you are in the shipping business, it's very helpful to have all of your potential competitors be taught that the earth is flat." — 2006 Buffett Letter to Shareholders Buffett even half-jokingly suggested that the Graham school "should endow several professorships to make sure that EMT is forever taught on campuses." **Practical implications:** - Markets may be efficiently priced 99% of the time, but that 1% of mispricing is the source of opportunity - Efficient markets gave rise to index funds — which is actually a good outcome for ordinary investors - Efficient markets do not mean markets are predictable; opposing EMH does not mean opposing index funds --- ## Market Forecasting **Core: Buffett never predicts the market, viewing it as a futile and harmful activity.** > "I do not forecast market movements or economic fluctuations. If you think I can predict these things, or that it's necessary to forecast them in order to invest, then the partnership is not for you." — 1962 Buffett Letter to Partners This position has been maintained from 1962 to the present, through every major turning point in his investment career — the 1987 crash, the 2000 dot-com bubble, the 2008 financial crisis, the 2020 COVID collapse — his stance has been unwavering: no forecasting, only valuation. **Why not forecast:** > "We have no idea what the market will do in the short term or the medium term — we never have." — 1986 Buffett Letter to Shareholders > "Charlie and I have not learned how to solve difficult business problems. What we have learned is to avoid them... making macro or market bets based on your own views or listening to someone else's macro or market predictions is a waste of time. In fact it is dangerous, because it may blur your vision of the facts that are truly important." — 2013 Buffett Letter to Shareholders **The core fraud of the market-forecasting industry — survivorship bias:** > "If 1,000 managers make a market prediction at the start of the year, it's likely that the calls of at least one will be correct for nine consecutive years. Of course, 1,000 monkeys would be just as likely to produce a seemingly all-wise prophet. But then, a difference would arise: the lucky monkey would not find people rushing to give it money to manage." — 2016 Buffett Letter to Shareholders **The cost of forecasters:** > "Forecasters of market direction fill your ears but never fill your wallet." — 2014 Buffett Letter to Shareholders **Value assessment vs. trend prediction — Buffett does the former, not the latter:** Although Buffett does not predict market movements, he will make a valuation judgment about the overall market at times of extreme valuation: > "Though we never try to predict stock market movements, we do assess, in a very rough way, whether the stock market is fairly valued." — 1997 Buffett Letter to Shareholders The key distinction: he can judge whether the market is broadly overpriced or underpriced, but will not predict when it will rise or fall. In 1999 he warned that the market was overvalued, but did not say "it will fall immediately" — the market rose for another year before peaking. **Practical implications:** - Make buy and sell decisions based on business intrinsic value, not guesses about market direction - Do not attempt market timing; ordinary investors should invest regularly in fixed amounts - The ability to remain calm during panic comes from focusing on value rather than price - Recommends buying stocks only if you intend to hold for at least five years (2014 Letter to Shareholders) --- ## Undervalued & Margin of Safety **Core: Only buy when the price is significantly below intrinsic value — that gap is the "margin of safety," a concept from Graham that Buffett has followed throughout his career.** > "I never attempt to make money on the stock market. I buy on the assumption that they could close the market the next day and not reopen it for five years." — 1958 Buffett Letter to Partners **The true meaning of undervaluation:** "Cheap" does not equal "undervalued." Undervaluation must be built on an independent estimate of intrinsic value: - A low P/E ratio does not mean undervalued (earnings power may be deteriorating) - A 90% stock price drop does not mean undervalued (if intrinsic value has dropped 95%) - True undervaluation: after deep analysis, confirming that the market price is significantly below the business's intrinsic value **The longer undervaluation persists, the more it benefits long-term investors:** > "We don't worry about the fact that the market may not quickly revalue upward the securities that we believe to be undervalued. In fact, we prefer this to not be the case, since in most years we expect to have net inflows of funds for investment in securities. The continued ability to buy at attractive prices is likely, in the end, to prove more beneficial to us than would be an opportunity for short-term gain through a quick market revaluation to levels at which we would not wish to add." — 1978 Buffett Letter to Shareholders **Undervalued stocks will continue to fall — but intrinsic value remains unchanged:** > "When trouble comes, even undervalued securities will be hit — though less so than overvalued ones. Their intrinsic value, however, will not be diminished." — 1958 Buffett Letter to Partners Undervaluation provides long-term protection, not short-term support. **Evolution from "cigar butts" to "quality businesses":** Buffett's early undervaluation investing was influenced by Graham, primarily seeking "cigar butt" stocks priced below liquidation value — stocks so cheaply priced they had one more puff left in them, with a single transaction capturing the final value. This approach has a fundamental limitation: **the companies are of such poor quality that after buying in, you often get trapped in a "you paid for a problem" situation.** Influenced by Charlie Munger, Buffett gradually shifted: > "It's far better to buy a wonderful company at a fair price than a fair company at a wonderful price." — Buffett's core transformation under Munger's influence (marked by the 1972 See's Candies acquisition) **Comparison of the two models:** | Dimension | Cigar Butt (early Graham style) | Quality Business (mature Buffett style) | |-----------|----------------------------------|------------------------------------------| | Basis for purchase | Price below liquidation value | Price below intrinsic business value | | Business quality | Irrelevant — cheap enough is sufficient | Moat and management are central | | Holding period | Sell after value recovery | Prefer to hold permanently | | Scale limitations | Small-cap; limited by fund size | Large-cap quality businesses; can absorb large capital | | Typical examples | Dempster, Sanborn Map | Coca-Cola, American Express | **Extended applications of the undervaluation concept:** - **Buybacks**: when stock price is below intrinsic value, repurchase is the optimal capital allocation — buying more than a dollar of value for a dollar spent - **Bonds**: WPPSS municipal bonds (1984) — a case of Buffett applying the undervaluation concept to non-equity assets - **Overall market**: the market was severely undervalued overall in 1973–1974, and severely overvalued overall in 1999 > "You pay a low price, you get high value. Each undervaluation investment is worth much more than its price, carrying a substantial margin of safety. Each individual holding has a margin of safety, and holding a diversified group forms a portfolio that is both safe and has upside potential." — 1961 Buffett Letter to Partners --- ## Compounding **Core: Continuously reinvesting returns so that money generates more money, producing exponential growth over time.** Einstein is said to have called it "the eighth wonder of the world." Buffett's entire Berkshire business model is built on this principle — for sixty years, virtually no dividends paid, all profits retained and reinvested. > "Reinvestment of earnings plus the power of compounding has produced a remarkable result, and shareholders have grown rich." — 2021 Buffett Letter to Shareholders **The three necessary conditions for compounding:** > "And so we began our journey to 2023 — a bumpy road involving a combination of retained earnings by our shareholders (i.e., reinvestment of profits), the power of compounding, the avoidance of major mistakes — and, most importantly, the American tailwind." — 2022 Buffett Letter to Shareholders 1. **Continuous saving**: profits must be retained rather than consumed (Berkshire has paid only one dividend since 1967) 2. **Avoiding major mistakes**: losing 50% requires earning 100% to recover — one catastrophic mistake can destroy years of compounding 3. **A favorable external environment**: a society with sound rule of law and sustained economic growth **Scale is the enemy of compounding:** > "A fat wallet is the enemy of superior investment returns. Given our current size, such a return is virtually impossible. Those who disagree are suitable for sales but not for mathematics." — 1983 Buffett Letter to Shareholders As capital scale expands, maintaining a high compound growth rate becomes increasingly difficult. Berkshire's annualized compound growth rate has dropped from roughly 23% in the early years to roughly 19% in recent years — this is an iron law of mathematics, not a failure. **The numbers say it all:** - 1965–2022 (58 years): Berkshire per-share market value CAGR of 19.8% vs. S&P 500 (with dividends) at 9.9% - Seemingly only about 10 percentage points more, yet the cumulative result: **3,787,464%** vs. **24,708%** - Coca-Cola: in 1994, paid Berkshire $75 million in dividends → grew to $704 million by 2022 (cost basis roughly $1.3 billion, market value grown to $25 billion) **About Berkshire's one dividend:** Berkshire paid its only ever dividend in January 1967 — 10 cents per Class A share, totaling $101,755. Buffett recalled in his 2024 letter: "I can't remember why I ever proposed this action to Berkshire's board. It was a mistake." **The reverse of compounding — the "anti-compounding" effect:** - Losing 50% requires earning 100% to recover - This is why "never risk permanent capital loss" sits at the top of all investment principles - The first rule of compounding is: do not interrupt the compounding process --- ## Intrinsic Value **Core: What a business is truly worth — not the accounting figure, not the market quote, but the discounted total of all the cash flows it can generate for shareholders over its entire lifetime.** > "Intrinsic value is an all-important concept that offers the only logical approach to evaluating the relative attractiveness of investments and businesses. Intrinsic value can be defined simply: it is the discounted value of the cash that can be taken out of a business during its remaining life. The calculation of intrinsic value, though, is not so simple. As our definition suggests, intrinsic value is an estimate rather than a precise figure, and it is additionally an estimate that must be changed if interest rates move or if forecasts of future cash flows are revised." — 1989 Buffett Letter to Shareholders **The essence of intrinsic value: forward-looking, not historical.** > "The value of any stock, bond, or business today is determined by the cash inflows and outflows — discounted at an appropriate interest rate — that can be expected to occur during the remaining life of the asset." — 1992 Buffett Letter to Shareholders The key distinction between stocks and bonds: a bond's future cash flows are defined by its coupon and maturity date; a stock's future "coupons" must be estimated by the investor themselves — this is why calculating intrinsic value requires business judgment, not merely mathematical ability. **Intrinsic value vs. book value:** > "Book value is an accounting concept, recording the accumulated financial inputs from both contributed capital and retained earnings. Intrinsic value is an economic concept, estimating future cash output discounted to present value. Book value tells you what has been put in; intrinsic value estimates what can be taken out. For most companies, these two figures bear no relationship to each other." — 1993 Buffett Letter to Shareholders - A manufacturer with large amounts of obsolete equipment: high book value, potentially very low intrinsic value - A light-asset brand business (like See's Candies): low book value, intrinsic value potentially far exceeding book - In 2018, Buffett formally abandoned book value as Berkshire's primary measuring metric **Growth does not necessarily increase intrinsic value:** > "Growth is always a component in the calculation of value, constituting a variable whose importance can range from negligible to enormous and whose impact can be negative as well as positive." — 1992 Buffett Letter to Shareholders When growth is meaningful: only when a business can reinvest at a **return above its cost of capital** does growth add value. The airline industry is the counterexample — the more it expands, the more it destroys shareholder value. **Intrinsic value is a range, not a precise number:** > "The calculation of intrinsic value, though all-important, is far from precise. As our definition suggests, intrinsic value is an estimate rather than a precise figure, and it is additionally an estimate that must be changed if interest rates move or if forecasts of future cash flows are revised. Two people looking at the same set of facts will almost inevitably come up with at least slightly different intrinsic value figures." — 2005 Buffett Letter to Shareholders What investors need is "roughly right" rather than "precisely wrong." **Practical applications of intrinsic value:** - **Buy**: when the market price is significantly below intrinsic value (margin of safety) - **Hold**: as long as intrinsic value is growing at a satisfactory rate - **Buyback**: when the stock price is below intrinsic value, repurchase is the optimal capital allocation - **Sell**: when the market price greatly exceeds intrinsic value and no better use of funds can be found > "In the short run, the market is a voting machine; but in the long run, it is a weighing machine." — quoting Benjamin Graham (2017 Buffett Letter to Shareholders)
references/buffett-review/03-business-moat.md# Business Quality: Moat and Business Model
> When to read this file: When evaluating sources of competitive advantage, determining moat type and strength, analyzing business model sustainability, or understanding the true value of goodwill.
## Economic Moat
> "A truly great business must have an enduring moat that protects excellent returns on invested capital."
### The Essence of a Moat
**Competitors have money, talent, and the will — yet still cannot replicate your advantage.**
A moat is not a temporary technological lead, not a single hit product, not a fleeting cost advantage. It is **structural** — it takes years or even decades to build and is extremely difficult to imitate.
### Five Types of Moats
**1. Intangible Assets (Brand, Patents, Licenses)**
> "Buy commodities, sell brands — that has always been the formula for business success."
**Four criteria for assessing a brand moat:**
- **Pricing power**: Consumers are willing to pay a premium for the brand (a candy bar can raise prices; sugar cannot)
- **Perennial demand**: Does the need the brand fulfills transcend economic cycles? (Coca-Cola's liquid refreshment is a timeless necessity)
- **Globalization**: Can the brand cross geographic boundaries? (Truly powerful brands go global)
- **Consistent quality**: The substance delivered behind the brand is stable — Buffett explicitly opposed compromising See's Candies quality in pursuit of short-term profits
**Key distinction:** Recognition ≠ brand moat. Many companies are widely known but have no pricing power (e.g., most airlines).
**Patent moat**: The core is the depth and breadth of a patent portfolio, not any single patent.
**2. Low-Cost Advantage**
| Source | Sustainability | Typical Examples |
|--------|---------------|-----------------|
| Scale advantage | High, but requires maintaining market share | Costco, GEICO |
| Geographic/resource advantage | Very high, non-replicable | Rail networks, specific mines |
| Process/culture advantage | Moderate, risk of imitation | Toyota Production System |
- Key question: Can a competitor match your cost structure by spending $1 billion over 5 years?
**3. Switching Costs**
Customers want to leave, but the cost of leaving is too high.
- Deep historical data integration (ERP systems, medical records)
- Employees require extensive retraining
- Switching vendors would disrupt core business processes
- **Quantitative signal**: Net Revenue Retention (NRR) > 120% indicates strong switching costs
**4. Network Effects**
The more users, the greater the value of the product/service to each user.
- **Direct network effects**: Users connect directly with each other (WeChat, WhatsApp)
- **Indirect network effects**: Both sides of a platform attract each other (Visa: more merchants → more cardholders → even more merchants)
- **Data network effects**: Accumulated data makes algorithms smarter (Google Search)
- **False network effect test**: If your users all migrated to a competing platform tomorrow, how much would they lose?
**5. Efficient Scale**
The market is small enough to support only one or two players; new entrants would cause both parties to lose money.
- Regional monopolies (waste disposal in a specific city, specific flight routes)
- Infrastructure networks (power transmission lines, pipelines)
### Dynamic Moat Assessment
**What matters most is the trend, not the current state.**
> "When the short-term and long-term conflict, widening the moat must take priority."
> "A moat that needs to be constantly rebuilt is ultimately no moat at all."
Widening signals:
- Sustained improvement in gross margins, indicating strengthening pricing power
- Steady market share growth
- Improving customer stickiness metrics (renewal rates, NPS)
Narrowing warnings:
- Sustained decline in gross margins
- Slow but persistent market share loss
- Management begins frequently discussing "intensifying competition"
- Key customers publicly seeking alternatives
- New technology is rendering existing advantages obsolete
**Failure case: Dexter Shoe**
In 1993, Buffett acquired Dexter Shoe for $433 million (paid with Berkshire shares!) believing it had a durable competitive advantage. Within a few years, cheap imported shoes destroyed the supposed moat.
> "Dexter is the worst deal I've made — I used Berkshire stock to pay for it, which made the loss even worse."
**Dual lesson:**
1. "Geographic isolation" and "domestic manufacturing" are not structural moats — they can be destroyed by globalization
2. Exchanging undervalued shares (Berkshire) for an asset whose value has evaporated is a double mistake
---
## Business Model Types
### Franchise Business vs. Commodity Business
This is Buffett's most fundamental distinction in assessing business quality.
**Strict definition of a Franchise Business:**
In his 1991 letter to shareholders, Buffett gave a precise three-part standard:
> "An economic franchise arises from a product or service that: (1) is needed or desired; (2) is thought by its customers to have no close substitute; (3) is not subject to price regulation." — 1991 Buffett Letter to Shareholders
Breaking down each condition:
- **"Needed or desired"**: Demand must be real and enduring — Coca-Cola satisfies humanity's timeless craving for sweet beverages, immune to technological disruption
- **"Thought by its customers to have no close substitute"**: Note it says "thought by its customers," not "actually true" — the power of brand, habit, and psychological perception makes Coca-Cola and Pepsi entirely different things in consumers' minds
- **"Not subject to price regulation"**: This excludes utilities where pricing power rests with regulators
Result: The ability to sustain returns above the cost of capital over the long term, and the capacity to **tolerate poor management**.
> "Franchise businesses can tolerate poor management. Incompetent managers will reduce franchise profitability but will not inflict mortal wounds." — 1991 Buffett Letter to Shareholders
**Characteristics of a Commodity Business:**
- Products/services are highly homogenous
- Prices are set by the market; no ability to charge a premium
- Intense competition; industry-wide returns approximate the cost of capital
- Even excellent management struggles to generate sustained excess returns
> "Airlines, textile mills — a shrewd investor should have recognized the economic character of these industries long ago."
> "Companies selling commodity-type products should attach a warning label to their stock: 'Competition may be hazardous to human wealth.'" — Peter Lynch, quoted in the 1993 Buffett Letter to Shareholders
**How to judge**: If this company raised prices by 10% and competitors held steady, what would happen?
- Customers flee en masse → Commodity business
- Customers largely stay → Franchise business
**The dynamic nature of franchises:** A franchise is not eternal; it can be strengthened or eroded. Newspapers once held an "impregnable" franchise (local retailers had no choice but to advertise in the only paper), and the internet completely changed that landscape. Identifying a franchise is only the first step — continuously monitoring the intensity of that franchise is the key.
> "The days of lush profits from impregnable franchises have ended." — 1991 Buffett Letter to Shareholders (on the media industry)
### Key Characteristics of a Business Model
**Simple and understandable:** Buffett only invests in businesses he can understand — this is an iron rule, not modesty. A simple business model means a clear value chain, transparent revenue sources, and trackable key variables.
**Difficult to replicate:**
> "Others can copy our business model, but they cannot replicate our economics." — 1999 Buffett Letter to Shareholders
GEICO's direct-to-consumer model appears straightforward on the surface, but decades of accumulated low-cost advantage, brand recognition, and operational scale form a barrier that competitors find extremely difficult to overcome. The model can be imitated; the economics cannot be replicated.
**Stable and enduring:** Buffett favors businesses that "do roughly the same thing today as they did five or ten years ago" (1987 Buffett Letter to Shareholders). Industries undergoing violent change make it hard to establish a durable franchise.
**Capital-efficient:** The best business models do not require large amounts of additional capital to sustain growth. See's Candies, with net assets of $25 million, generated over $400 million in pre-tax profit over twenty years.
### Practical Testing Tools for Business Models
**Toll bridge test:** The best businesses are like toll bridges — customers have no choice but to pass through. See's Candies' pricing power on Valentine's Day eve and Coca-Cola's global brand premium are both expressions of the toll-bridge business model.
**Lowest-cost producer test:** When a company sells a product with commodity characteristics, "being the low-cost producer is all-important" (2000 Buffett Letter to Shareholders). GEICO used this to grow its market share from 2.0% to over 12%.
**Capital reinvestment test:** A good business model should allow retained earnings to earn high returns. National Indemnity chose to let revenues decline continuously from 1986 through 1999 rather than compromise on profitable pricing — the discipline embedded in this business model requires extraordinary courage rooted deep in corporate culture.
**Chain letter test (negative):** Buffett warned that a business model built on issuing overvalued shares for serial acquisitions "is like a chain letter — it merely redistributes wealth, never creates it" (2014, Berkshire — Past, Present and Future).
---
## Goodwill: Economic Goodwill vs. Accounting Goodwill
> "My own thinking has changed drastically from 35 years ago when I was taught to favor tangible assets and to shun businesses whose value was dependent largely upon economic Goodwill. This bias caused me to make many significant errors of omission." — 1983 Buffett Letter to Shareholders
This is one of the most intellectually rich concepts in Buffett's investment philosophy, and a key to understanding his transition away from Graham-style "cigar butt" investing.
### Two Fundamentally Different Forms of Goodwill
**Accounting Goodwill:**
The premium paid over the fair value of the acquired entity's net assets, recorded on the balance sheet, historically amortized over 40 years (changed to impairment testing under GAAP in 2002). This is a purely accounting concept and reflects no economic reality.
**Economic Goodwill:**
The capitalized value of a company's excess earning power — when a business can consistently earn returns on its net tangible assets well above the market average, that capacity for excess returns is itself an economic value.
> "When a business earns on unleveraged net tangible assets rates of return that are significantly in excess of current market rates, it makes sense to think of the business as having two parts: a 'normal' business that earns returns matching the opportunity cost of capital, and an 'excess' business that earns above-market returns. The capitalized value of this excess return is the economic Goodwill." — 1983 Buffett Letter to Shareholders
**The core paradox:**
> "Accounting Goodwill diminishes regularly from the moment of purchase, while economic Goodwill grows in an irregular but very substantial manner." — 1983 Buffett Letter to Shareholders
See's Candies is the most compelling illustration: at the time of acquisition in 1972, net tangible assets were approximately $8 million and after-tax profit was approximately $2 million (after-tax ROE of roughly 25%). By 1983, net tangible assets were approximately $20 million and after-tax profit was $13 million. Accounting goodwill was being amortized down year by year, while economic goodwill continued to grow.
### Three Primary Sources of Economic Goodwill
Explicitly identified by Buffett in his 1991 letter:
- **Consumer franchise** (primary source): The long-term goodwill a brand builds in the minds of consumers
- **Government-granted franchises not subject to profit regulation**: Such as historical television stations
- **A durable position as the low-cost producer in an industry**: Structural cost advantage
### Accounting Goodwill Amortization Should Be Ignored in Analysis
Buffett proposed two key analytical principles:
1. **When assessing operating results**: Amortization charges should be ignored. "What a business can be expected to earn on unleveraged net tangible assets, excluding any charges against earnings for amortization of Goodwill, is the best guide to the economic attractiveness of the operation."
2. **When assessing the wisdom of an acquisition**: Amortization charges should equally be ignored. Goodwill purchased should always be viewed at its full cost, with no amortization applied.
### Economic Goodwill Is a Natural Ally in an Inflationary Era
This is Buffett's most penetrating insight about goodwill:
When prices double, asset-heavy businesses must commit large amounts of new capital (corresponding to a doubling of net tangible assets) just to maintain nominal profits; whereas asset-light businesses with high economic goodwill (such as See's Candies) require minimal additional capital, and most nominal profits can be freely distributed or reinvested.
> "In an inflationary world, a toll-bridge style business that requires little incremental investment is the gift that keeps on giving." — 1983 Buffett Letter to Shareholders
> "Of course, large gains in real wealth were made by some holders of businesses whose economic goodwill did not, in fact, erode and who also were in industries, such as media, that required relatively little reinvestment of earnings." — 1983 Buffett Letter to Shareholders
**The inversion of conventional wisdom:** On the surface, "heavy assets = inflation hedge," but in an inflationary environment it is actually the asset-light, high-economic-goodwill businesses that win. Asset-heavy businesses typically earn very low rates of return — so low that they are often barely sufficient to fund the inflationary needs of the existing business, leaving nothing to support real growth and nothing to distribute to owners.
### Distinguishing Real from Fake Goodwill
**Real goodwill**: Stems from genuine excess earning power — brand loyalty, cost advantages, consumer franchise, and other structural sources.
**Fake goodwill ("capitalized adrenaline")**: When management overpays in a foolish acquisition, the premium is equally recorded as goodwill, but it is merely "capitalized managerial impulse."
> "In view of the lack of managerial self-control that created this account, perhaps it should be labeled 'no-goodwill' in such cases." — 1983 Buffett Letter to Shareholders
**Practical judgment:**
- High goodwill + high ROE sustained over many years → Likely genuine economic goodwill
- High goodwill + mediocre ROE → Likely accounting goodwill piled up through overpriced acquisitions
- Goodwill amortization dragging down earnings → Add it back in analysis to see true operating capability
---
## The Durability of Competitive Advantage
> "A durable competitive advantage heads the list of qualities we seek in a business." — 2017 Buffett Letter to Shareholders
**Durability > Scale.** A small but durable competitive advantage is far more valuable than a large but fleeting one.
**Moat vs. Competitive Advantage — the distinction:**
- Moat is a metaphor (the water surrounding a castle)
- Competitive advantage is the specific content (low cost, brand, switching costs, network effects, efficient scale)
- Both describe the same thing: moat emphasizes "defensibility," competitive advantage emphasizes "source"
**Multiple sources of competitive advantage (by priority):**
1. **Low-cost producer position**: "When a company is selling a commodity-type product, being the low-cost producer is all-important." (2000 Buffett Letter to Shareholders) GEICO's low-cost advantage existed as far back as 1951 and persisted for nearly 50 years — this is the ultimate proof of "durability."
2. **Brand/consumer franchise**: Coca-Cola's and Gillette's brand power gives them "a share of mind around the world." The distinctive feature of brand-based competitive advantage is its irreversibility — the taste memories and emotional connections billions of consumers build from childhood are nearly impossible for competitors to replicate.
3. **Financial strength itself as a competitive advantage**: In insurance, Berkshire's unrivaled financial strength is itself a competitive advantage — "When a large policyholder seriously considers which company can comfortably pay a $10 million claim five or ten years from now... he finds there are very few companies he can trust." (1987 Buffett Letter to Shareholders)
4. **Corporate culture and management structure**: This is the hardest competitive advantage to replicate. "Our rare and hard-to-replicate management structure gives Berkshire a real competitive advantage." (2007 Buffett Letter to Shareholders)
### Practical Tests for Competitive Advantage
**The "hypothetical competitor" test**: When evaluating Nebraska Furniture Mart, Buffett used a highly intuitive method — imagining himself as a competitor:
> "I ask myself one question when evaluating a business: If I had ample capital and talent, would I want to compete with it? I'd rather wrestle a grizzly bear than compete with Mrs. B and her descendants." — 1983 Buffett Letter to Shareholders
If a rational competitor would decline to enter even with unlimited resources, the company's competitive advantage is truly durable.
**Distinguishing "apparent advantage" from "real advantage"**: The textile business was a firsthand lesson for Buffett. The 1985 Buffett Letter to Shareholders recalled: Southern textile mills were thought to have a significant competitive advantage (labor costs), but it proved extremely fragile — the fundamental economics of the entire industry were too poor for any local advantage to withstand global competition.
**Growth in scale can erode competitive advantage:**
> "Our major problem is the sheer size of our capital base, which constrains our investment universe: In the early years, we needed only good ideas, but now we need both good and large ideas. Unfortunately, finding such ideas grows ever harder as our financial success increases, and this problem continues to erode our competitive advantage." — 1995 Buffett Letter to Shareholders
> "Truly great businesses usually resist being bought, meaning the acquisition market is mostly populated with mediocre companies lacking competitive advantage." — 2020 Buffett Letter to Shareholders
The larger the acquisition, the harder it is to find targets with genuine competitive advantage.
---
references/buffett-review/04-management-governance.md# Management, Culture, and Corporate Governance
> When to read this file: When evaluating management quality (integrity/capital allocation/owner mentality), identifying institutional imperative, assessing acquisition rationale, or evaluating corporate governance structure.
## Management Evaluation
> "We've never succeeded in making a good deal with a bad person."
### Three-Dimensional Evaluation Framework
**Dimension One: Integrity (One-Strike Disqualifier)**
> "In looking for people to hire, you look for three qualities: integrity, intelligence, and energy. And if they don't have the first, the other two will kill you."
Integrity is not just "not lying" — it means **proactively and honestly confronting bad news**. Buffett's core test: **"Is this person willing to tell me what they don't know?"**
**Newspaper Test:**
For any business decision, ask: if this appeared on the front page of a national newspaper tomorrow, would I feel ashamed? If a behavior approaches the moral boundary, treat it as having already crossed it — do not test the line.
**Salomon Brothers Warning (1991):**
A compliance issue that was originally manageable nearly destroyed the entire company because management delayed coming clean.
> "We can't afford to lose reputation — not even a shred of it." — 2010 Letter
**Signals of Integrity Problems:**
- Financial reports full of optimistic language, never discussing failures; bad news always buried in footnotes
- Frequent use of "adjusted" metrics to mask real operating issues
- Mistakes are never acknowledged proactively; blame is always shifted to external factors
- Frequent related-party transactions with non-transparent terms
> "A CEO who misleads others in public will eventually mislead himself in private."
---
**Dimension Two: Capital Allocation Ability**
> "Most bosses rise to the top because they excel at marketing, engineering, or other areas. Once they become CEO, they face a brand-new responsibility — capital allocation."
Evaluation method:
- Capital allocation track record over the past 10 years: M&A / buybacks / dividends / reinvestment — has value consistently been created for shareholders?
- Is there discipline in acquisitions? Has the company ever overpaid just to "do a deal"?
- Are buybacks conducted only when the stock price is below intrinsic value? (Rather than to maintain EPS)
- Is there an empire-building impulse (pursuing size rather than per-share value)?
---
**Dimension Three: Owner Mentality**
The management trait Buffett values most is "owner mentality" — the manager treats the business as if it were their family's sole asset.
> "He manages the company as though it were 'his own' business — and that attitude is exactly what we prize most highly at Berkshire." — 1996 Buffett Letter to Shareholders
> "For good reason, I've been praising the accomplishments of our operating managers. They are truly All-Stars who run their businesses as if they were the only asset their families will ever own." — 2011 Buffett Letter to Shareholders
**Concrete expressions of owner mentality:**
- Does management treat the company as if running their own business?
- Is the focus on "growth in per-share intrinsic value" rather than simply EPS?
- Are they willing to close businesses that don't create value, even if it shrinks the company?
- Is personal wealth genuinely tied to shareholder interests (shares held, not options)?
**Buffett's "Hands-Off Management" Philosophy:** Precisely because the entry filter is extremely strict (only selecting people with owner mentality), once selected, maximum autonomy is granted.
> "At Berkshire, managers can focus on running their businesses: they don't have to attend headquarters meetings, worry about financing, or deal with Wall Street harassment. They simply receive a letter from me every two years, and call when something comes up. We trust people, not process." — 2010 Buffett Letter to Shareholders
**Buffett's Most Favored Acquisition Model:** The "Blumkin-Friedman-Heldman type" — the founder sells and yet continues to operate the business, retaining meaningful equity, carrying forward passion and accountability intact. This type of "owner-operator" is Berkshire's ideal partner.
### Institutional Imperative
This is one of Buffett's most important original concepts, and one that business schools almost never teach.
> "I was not taught anything about institutional imperative in business school... It wasn't until I had practical experiences of my own that I understood how destructive it can be." — 1989 Letter
**Definition of Institutional Imperative:** An invisible organizational pressure that compels managers to make decisions they would never make in a rational environment.
**Four Specific Manifestations:**
1. **Herd Expansion:** A leader proposes an idea, and the organization automatically generates analysis to support it ("there's always a reason to acquire")
2. **Size Worship:** Institutions resist any direction that differs from the status quo, preventing companies from contracting or divesting poor assets
3. **Peer Comparison Trap:** When peers are all doing something (excessive borrowing, aggressive acquisitions), managers feel that "not doing it would be strange"
4. **Busyness Bias:** CEOs feel "doing nothing" doesn't count as work — but waiting in the absence of good opportunities is the most difficult and most correct decision
**Practical Judgment:**
- A company has acquired a series of expensive competitors → possibly herd behavior driven by institutional imperative
- Management compensation benchmarked to peer comparisons → institutional imperative pushes all CEO pay upward
- A CEO faces "pressure to deploy cash" (too much cash accumulated, analysts pushing for action) → observe whether he resists
### Supporting Judgment Methods
- **Look at long-term track records (10+ years)**, not short-term performance. One or two years of results may be luck.
- **Read annual shareholder letters / MD&A sections in annual reports**, comparing commitments against outcomes.
- **Watch for management departures**: CFO departures in particular are worth investigating carefully.
- **Related-party transactions**: Frequent and non-transparent related-party transactions are a common signal of integrity problems.
- **Beware of "1 year of experience repeated 20 times"**: Long tenure ≠ rich experience — evaluate the quality of judgment, not the number of years.
---
## Corporate Culture
> "Our final advantage is the pervasive, hard-to-replicate corporate culture that pervades Berkshire. In the business world, culture matters enormously." — 2010 Buffett Letter to Shareholders
### Culture as an "Operating System"
Corporate culture is not a mission statement or an employee handbook — it is **the collective habits by which every person in the organization makes decisions when no one is watching**. It is the operating system of the business machine — it determines the underlying logic behind the countless decisions made in all daily operations and unexpected events.
Good culture makes managers think and act like owners. The word "pervasive" is precise — culture is not imposed top-down; it permeates every corner of the organization like air. "Hard to replicate" captures the essential nature of culture as competitive advantage: systems can be imitated, strategies can be copied, but culture cannot simply be transplanted.
> "Charlie and I believe that Berkshire's intrinsic value will grow at a rate that modestly exceeds the S&P return over time. We are confident in this because we have a group of outstanding businesses, a corps of terrific operating managers, and a shareholder-oriented corporate culture." — 2012 Buffett Letter to Shareholders
### Core Characteristics of Berkshire's Culture
**Decentralized Management:** 25 people manage a corporate empire worth hundreds of billions of dollars. No budgets, no quarterly targets, no bureaucratic hierarchy. Each subsidiary CEO knows that as long as they think and act like an owner, headquarters will not interfere.
> "At Berkshire, managers can focus on running their businesses: they don't attend headquarters meetings, don't worry about financing, and don't have to deal with Wall Street harassment... We trust people, not process. 'Hire well, manage little' — this principle applies to them and to me." — 2010 Buffett Letter to Shareholders
**Shareholder-Oriented Cultural Foundation:** All decisions are made with long-term shareholder interests as the starting point. No company-wide budget targets (to avoid distorting behavior in pursuit of "hitting the numbers"), no pursuit of smooth quarterly earnings.
> "Berkshire has no company-wide budget... Eschewing this type of goal conveys an important message to our many managers, reinforcing the corporate culture we so value." — 2018 Buffett Letter to Shareholders
**Underwriting Discipline Culture (National Indemnity Case):** From 1986 to 1999, National Indemnity's premium volume steadily shrank because it refused to follow the industry's irrational price cuts.
> "Running National Indemnity this way requires deep-seated courage in its corporate culture. You can glance at the table above and skim over those years from 1986 to 1999 in an instant. But to watch your business shrink day after day — while competitors boast of their growth and earn Wall Street's applause — that kind of torture very few managers can endure." — 2004 Buffett Letter to Shareholders
**Culture of Not Disturbing Success:** After Berkshire acquires a company, it preserves the subsidiary's existing culture. This is extremely rare in the business world — most acquirers impose their own systems, with results that typically backfire.
### Cultural Continuity
Buffett is clear-eyed about the greatest test culture faces — whether it survives after the founder departs:
> "Their mission is to ensure that Berkshire's unique corporate culture is preserved for our shareholders and managers after another CEO succeeds me." — 2002 Buffett Letter to Shareholders
> "Berkshire's unique corporate culture is already deeply embedded in each of our subsidiaries, and their operations would not change in the slightest even after my passing." — 2005 Buffett Letter to Shareholders
### Signals for Identifying Culture Quality
**Signs of Good Culture:**
- Employees take pride in the company; turnover is low
- Primarily internal promotions; outside hires are not frequent
- Decision-making is pushed down; frontline staff exercise judgment
- Long-term orientation: employees do not sacrifice long-term correct actions for quarterly targets
- Bad news travels quickly upward and is not suppressed out of fear of "shooting the messenger"
**Signs of Bad Culture:**
- Frequent turnover in the senior management layer
- Sales culture takes precedence over product/service quality
- Internal politicization; resource allocation is non-transparent
- The way employees are treated contradicts external messaging
- Management only discusses successes, never honestly acknowledges mistakes
---
## Acquisition Logic (Acquisition Criteria)
Buffett applies extremely rigorous discipline to acquisitions and has repeatedly warned that "the history of corporate mergers is a history of value destruction."
**Berkshire's Six Acquisition Criteria ("Business Wanted" Notice):**
1. Meaningful size (after-tax earnings of at least several tens of millions of dollars)
2. Consistent and demonstrable earning power (not interested in future projections; "turnaround stories" are not his preference)
3. Good returns on equity with little or no debt
4. Excellent and honest management (Berkshire will not manage them after acquisition)
5. Simple, understandable business model
6. A sensible asking price (no time spent in idle discussions when sellers have no realistic price expectation)
**Price Discipline in Acquisitions:**
> "It's far better to buy a wonderful company at a fair price than a fair company at a wonderful price."
**Berkshire's Greatest Competitive Advantage: The "No-Exit" Strategy:**
> "Unlike leveraged buyout operators and private equity firms, we have no 'exit' strategy — we buy to keep. That's also why Berkshire is often the preferred buyer for sellers and their managers — sometimes the only choice." — 2002 Buffett Letter to Shareholders
This "hold forever" commitment has tremendous appeal for family business founders — they care not only about price, but also about whether the culture and operating style they built over a lifetime will be preserved after the sale.
**Minimal Acquisition Process:** The Forest River acquisition example — from receiving a two-page fax to issuing an offer in less than 48 hours. This efficiency stems from clear knowledge of what to look for: having long known what he wants, the only task is to judge whether the business meets the criteria.
**Post-Acquisition Management Principles:**
> "We must honor our commitments; avoid leveraging up acquired businesses; grant managers extraordinary autonomy; and stand by the businesses we acquire through good times and bad." — 2008 Buffett Letter to Shareholders
**Common Acquisition Pitfalls:**
- Overly optimistic synergy assumptions ("1+1>2" is often only a number on a slide deck)
- Paying a premium for "empire building" (a classic product of institutional imperative)
- Integration costs are severely underestimated
- Using undervalued shares (e.g., Berkshire's) to acquire assets that subsequently lose value is a double mistake (the Dexter Shoe lesson)
- Management departs after a costly acquisition, and the business loses its core driving force
---
## Corporate Governance and Shareholder Orientation
> "Our standard is to tell you the business facts that we would want to know if our positions were reversed — all of them, without reservation."
**Core Philosophy:** Berkshire's form is a corporation, but its attitude is that of a partnership — shareholders are the true owners, and management are the operating partners.
### Alignment of Interests: The Essence of Governance
**Buffett's Core Criticism:** Most corporate governance is form over substance. "Independent directors" are often not truly independent.
- True independence is about interests, not labels — directors who rely on board fees dare not offend the CEO
- True independence = owning substantial company stock purchased with one's own money
- Options ≠ alignment of interests (gains when price rises, no loss when it falls — asymmetric relative to shareholders)
**Berkshire's Governance Model:**
- 11 directors, each with more than half of their family net worth in Berkshire shares
- No directors' and officers' liability insurance (genuine alignment of interests)
- Management compensation tied to long-term per-share intrinsic value, not short-term stock price
**Systemic Problems with Compensation Committees:**
> "Compensation consultants have an incentive to please the CEO, not to antagonize them. The result is that the most aggressive compensation packages become the industry template, in an upward spiral."
### Treatment of Shareholders
**Fair Treatment of All Shareholders:**
- No stock splits (filtering out short-term speculators, attracting long-term owners)
- No quarterly earnings guidance (avoiding misleading the market)
- Equal treatment for all — no special information access for institutional investors
**Transparent Communication (Identification Signals):**
- Proactively disclosing bad news rather than waiting for others to discover it
- Shareholder letters written in plain language, without jargon or boilerplate disclaimers
- Openly acknowledging mistakes rather than making excuses
- Opposite signals: optimistic report language + bad news buried in footnotes + "adjusted" metrics
### Practical Evaluation Checklist
Evaluating a company's governance and shareholder orientation quality:
- Is the board genuinely capable of independently evaluating CEO performance?
- Does management explain "why they did it," not just "what they did"?
- Are they willing to close or divest businesses that don't create value (even if it shrinks the company)?
- Does the compensation structure genuinely align management interests with shareholders (shares held vs. options)?
references/buffett-review/05-financial-metrics.md# Financial Analysis and Key Metrics > When to read this file: When you need to analyze financial statements, calculate owner earnings, assess ROIC/ROE quality, or understand the concept of look-through earnings. ## Core Principle: See Through Accounting to Economic Reality GAAP financial statements are the starting point, not the destination. Buffett always asks: **How much does this business truly earn?** --- ## Owner Earnings Buffett introduced this concept in his 1986 shareholder letter as the best tool for measuring a business's true earning power. ``` Owner Earnings = Net Income + Depreciation, Depletion, and Amortization (D&A) − Maintenance CapEx required to maintain competitive position − Any required net increases in working capital ``` **Key Distinction — Two Types of Capital Expenditure:** | Type | Nature | Treatment | |-----|-----|---------| | Maintenance CapEx | Must be spent or competitive position deteriorates | Must be deducted from owner earnings | | Growth CapEx | Optional, used to expand new capacity | Not deducted (this portion generates future returns) | **Why It Is More Reliable Than EBITDA:** EBITDA adds back depreciation, implicitly assuming assets do not wear out. But assets do age, and maintaining operations requires real cash outlays. **Methods for Estimating Maintenance CapEx:** - Management sometimes discloses it - Approximation: Depreciation × industry rule-of-thumb multiplier (close to 1:1 for asset-light businesses, potentially higher for capital-intensive ones) - Compare against industry peers --- ## Key Financial Metrics ### Profitability **ROE (Return on Equity)** ``` ROE = Net Income / Average Shareholders' Equity ``` - Buffett's standard: 10-year average >15%, with no excessive leverage - Must decompose the source: margin expansion / asset turnover improvement / financial leverage increase - ROE inflated by leverage is a trap, not an advantage **ROIC (Return on Invested Capital)** ``` ROIC = NOPAT (Net Operating Profit After Tax) / Invested Capital (Equity + Interest-Bearing Debt − Cash) ``` - More honest than ROE; removes the distortion of capital structure - Sustained >15% is excellent; >20% is exceptional - **Value creation condition: ROIC > WACC (Weighted Average Cost of Capital)** ### Cash Flow Quality **Cash Conversion Ratio** ``` Cash Conversion Ratio = Operating Cash Flow / Net Income ``` - Sustained >90%: High earnings quality, not reliant on accounting techniques - Persistently <70%: Requires deep investigation (may indicate inflated receivables or aggressive revenue recognition) **Free Cash Flow (FCF)** ``` FCF = Operating Cash Flow − Capital Expenditures (total) ``` Note: This includes growth CapEx, making it more appropriate for asset-light companies. For high-CapEx companies in growth phases, the owner earnings lens is fairer. ### Balance Sheet Health **Debt Safety:** - Debt / EBITDA < 2x: Comfortable range - Interest Coverage Ratio (EBIT / Interest Expense) > 5x: Sufficient buffer **Worst-Case Scenario Test (mandatory):** Assume revenue falls 30% — can the company: 1. Repay maturing debt? 2. Sustain core business operations? 3. Avoid dilutive financing? ### Earnings Quality Signals **Red Flag Checklist (investigate further if any appear):** - [ ] Accounts receivable growth persistently exceeds revenue growth - [ ] Inventory growth persistently exceeds revenue growth - [ ] Operating cash flow persistently below net income - [ ] Frequent use of "adjusted earnings" with large divergences from GAAP - [ ] Large goodwill balances from high-premium acquisitions with mediocre returns - [ ] Frequent related-party transactions with non-transparent terms - [ ] Auditor changes, or audit opinions with qualifications - [ ] Management selling large amounts of shares --- ## Look-Through Earnings Applicable to: **analyzing holding companies, investment holding companies, or businesses with substantial equity stakes not consolidated on the balance sheet** An original concept Buffett first proposed in 1989 and first systematically calculated in 1990, addressing a major deficiency in GAAP's ability to reflect the true earning power of investee companies. **Complete Calculation Formula:** ``` Look-Through Earnings = Berkshire's reported operating earnings (excluding capital gains and non-recurring items) + Berkshire's proportionate share of retained earnings from major investees − Estimated income taxes that would be owed if those retained earnings were distributed to Berkshire as dividends ``` **Actual Calculation Example from 1990 (Buffett's own words):** > "Take $250 million, which is approximately our share of the 1990 retained operating earnings of our investees; subtract $30 million, for the incremental taxes we would have owed had that $250 million been paid to us as dividends; and add the remainder, $220 million, to our reported operating earnings of $371 million. Thus, our 1990 'look-through earnings' were about $590 million." — 1990 Buffett Letter to Shareholders **Why It Matters:** GAAP only allows recognition of dividends received when ownership is below 20%, causing retained earnings to disappear from the books. The Cap Cities/ABC example: in 1990, Berkshire's proportionate earnings exceeded $82 million, but only about $530,000 appeared on the GAAP books (dividends net of taxes). GAAP severely understates true earning power. **The Link Between Look-Through Earnings and Intrinsic Value Growth:** > "For Berkshire's intrinsic value to grow at an average of 15% per year, our look-through earnings must also grow at approximately the same rate." — 1989 Buffett Letter to Shareholders **Why Retained Earnings Are Sometimes More Valuable Than Dividends:** > "You must judge whether these undistributed earnings are as valuable to you as those already reported. We believe they are — and, in fact, may even be more valuable. The reason for this 'bird-in-the-bush-may-be-worth-two-in-the-hand' conclusion is that our investees are employing their retained earnings under the direction of managers who are talented and shareholder-oriented, and who sometimes find uses for capital within their own businesses that are superior to anything we could find for it." — 1989 Buffett Letter to Shareholders **The Flaw of GAAP's "20% Threshold":** Buffett criticized this arbitrary threshold as early as 1982: "The value of retained earnings to all shareholders depends on the efficiency with which they are deployed, not on the size of your ownership stake." **Practical History:** Calculated and publicly reported for 8 consecutive years from 1990 to 1997; suspended in 1998 following the General Re acquisition; since 2018, replaced by a disclosure comparing investee dividends received versus retained earnings, carrying on the same spirit. > "While no single figure captures everything perfectly, we believe look-through earnings more accurately reflect Berkshire's true earning power than the GAAP number." — 1992 Buffett Letter to Shareholders **This framework must be used when analyzing Berkshire, and should be considered when analyzing any company that holds substantial non-controlling equity interests.**
references/buffett-review/06-valuation-capital.md# Valuation, Capital Allocation, and Special Instruments
> When to read this file: When calculating intrinsic value, determining margin of safety, evaluating capital allocation decisions (buybacks/dividends/retained earnings), or analyzing arbitrage and special investment opportunities.
## Intrinsic Value Estimation
> "Intrinsic value is the discounted estimate of the cash flows that can be taken out of a business during its remaining life. For most companies, this has nothing to do with book value." — 1993 Letter
### Method 1: Owner Earnings Multiple (Most Common)
Applicable to: **stable, predictable, mature businesses**
```
Intrinsic Value ≈ Normalized Owner Earnings × Appropriate Multiple
```
**Normalization**: Exclude one-time items, use a 3–5 year average, reflecting the earning power of a "normal operating year."
**Reference Multiples:**
| Business Characteristics | Appropriate Multiple |
|---------|---------|
| Wide moat + 5%+ growth + asset-light | 20–25x |
| Solid moat + moderate 3–5% growth | 15–20x |
| Average moat + low growth | 10–15x |
| Narrow moat + competitive threats | 8–12x |
| Cyclical / declining business | < 8x |
### Method 2: Discounted Cash Flow (Simplified)
Applicable to: **businesses with a clear growth trajectory**
Buffett does not use complex DCF spreadsheets, but performs simple mental arithmetic:
```
Intrinsic Value = Sum of present values of owner earnings over the next 10 years (estimated year by year)
+ Terminal value after year 10 (using a perpetual growth model)
```
Discount rate: Risk-free rate + 3–5% (company-specific risk premium)
**Practical note:** If the calculated intrinsic value is highly sensitive to the discount rate (changing by 1% causes a large swing), the valuation is unreliable and a larger margin of safety is required.
### Method 3: Historical Earnings Power Method (Graham Tradition)
Applicable to: **cyclical industries, or when a quick estimate is needed**
- Use average earnings over the past 7–10 years, not the most recent year
- Apply a conservative multiple (10–15x)
- Well suited for industries with pronounced cycles: banking, resources, retail
---
## Margin of Safety
> "The three most important words: margin of safety, margin of safety, margin of safety." — Benjamin Graham
**In practice: Purchase price = Intrinsic value × 60–70%**
**Three key elements:**
1. **Humility about cognitive limits**: Even with extensive experience, estimates of future cash flows can be wrong. Margin of safety compensates for unavoidable errors.
2. **Not the same as "cheap"** (a common misconception): A low P/E ratio does not equal a margin of safety. The key is the relationship between **price and true value**, not the price level itself. A company whose moat is narrowing has no margin of safety even at 8x earnings.
3. **Dynamic, not static**: The same business at different times commands different prices and therefore different margins of safety. In 1997, Buffett publicly warned that prevailing price levels had "significantly eroded the margin of safety" — a judgment validated three years later when the dot-com bubble burst.
**The required margin of safety increases with business uncertainty:**
- Highly certain, excellent businesses (Coca-Cola): 20–30% discount
- Generally excellent businesses: 30–40% discount
- Businesses with uncertain factors: 40–50% discount
- Businesses whose intrinsic value cannot be reliably estimated: do not invest
---
## The Relationship Between Purchase Price and Intrinsic Value
**Core proposition: Price determines return**
The gap between the purchase price and intrinsic value determines the ultimate fate of an investment. Even if a business's intrinsic value grows steadily, an excessive purchase price can wipe out years of strong performance.
> "For the investor, a too-high purchase price for the stock of an excellent company can undo the effects of a subsequent decade of favorable business developments." — 1982 Buffett Letter to Shareholders
> "Sometimes the price offered for a fine business is so high that even a fine business can turn into a terrible investment — if not permanently, at least painfully long." — 2018 Buffett Letter to Shareholders
**The intellectual shift from "cigar butts" to "great companies at fair prices":**
Early Buffett pursued extremely low purchase prices (Graham-style); later, influenced by Munger, he shifted to buying excellent businesses at fair prices.
> "It's far better to buy a wonderful company at a fair price than a fair company at a wonderful price. Charlie understood this early; I was a slow learner." — 1989 Buffett Letter to Shareholders
> "More than 50 years ago, Charlie told me that it is far better to buy a wonderful business at a fair price than a so-so business at a wonderful price. Despite the compelling logic of his position, I have sometimes reverted to my old habit of bargain-hunting, with results ranging from mediocre to disastrous." — 2012 Buffett Letter to Shareholders
**Four dimensions of a fair price (1976 stock selection criteria):**
1. Favorable long-term economic characteristics
2. Competent and honest management
3. **A purchase price that is attractive by private-ownership standards** (the third leg)
4. An industry within one's circle of competence
**The principle of locking in profit at purchase:**
> "Whether or not we are active or passive investors in a control situation, there is one thing we must do: we must make our profits through purchase price. The prerequisite to buying a controlling interest is an attractive purchase price." — 1963 Buffett Letter to Partners
**The long-term interplay between purchase price and retained earnings:**
> "If the business earns at a reasonable rate on the price paid, the market almost has to eventually recognize retained earnings' cumulative effect. Occasionally you even receive frosting on the cake — market appreciation far in excess of accumulated retained earnings since purchase." — 1980 Buffett Letter to Shareholders
**Common misconceptions:**
- "Fair price" ≠ "high price": Even the best businesses can be priced too high
- "Cheap" ≠ "below intrinsic value": A $100 stock can be cheaper than a $10 stock (what matters is the gap to intrinsic value)
- "Conservative" ≠ "holding well-known blue-chip stocks": True conservatism means buying at a fair price; talking about conservatism without mentioning purchase price is meaningless
---
## Capital Allocation Analysis
Evaluating management's "wisdom with capital." Buffett views capital allocation as a CEO's most important responsibility:
> "The heads of many companies are not skilled in capital allocation. Their inadequacy is not surprising. Most bosses rise to the top because they have excelled in an area such as marketing, production, engineering, administration or, sometimes, institutional politics. Once they become CEOs, they face new responsibilities. They now must make capital allocation decisions, a critical job that they may have never tackled and that is not easily mastered." — 1987 Buffett Letter to Shareholders
**The "One Dollar Test" (Buffett's standard):**
> "Unrestricted earnings should be retained only when there is a reasonable prospect — backed preferably by historical evidence or, when appropriate, by a thoughtful analysis of the future — that for every dollar retained by the corporation, at least one dollar of market value will be created for owners." — 1984 Buffett Letter to Shareholders
**The complete five-path capital allocation framework:**
| Path | When to Prioritize | Key Decision Criteria |
|-----|------------|------------|
| Reinvest in existing business | High ROIC, moat-backed, reliable organic growth | Does incremental return exceed cost of capital? |
| Acquire businesses (full ownership) | Fair price, excellent management, understandable business | Fair price is "the third leg" — non-negotiable |
| Purchase minority stakes in public companies | Price below intrinsic value, no suitable acquisition targets | Berkshire's unique "both/and" advantage |
| Repurchase stock | Share price clearly below intrinsic value | See the buyback analysis section below |
| Pay dividends | When none of the above options offer better returns | Management should honestly acknowledge no better use exists |
> "Charlie and I have truly easy jobs at Berkshire. We do almost nothing except allocate capital. And we're not even particularly diligent." — 1998 Buffett Letter to Shareholders
**Berkshire's "both/and" advantage:**
The ability to acquire businesses outright and also purchase minority stakes in the public market. This flexibility gives Berkshire an entire additional universe of investment opportunities compared to companies that can only do controlling acquisitions. "The world is more varied than most people realize. A bisexual greatly increases the probability of a date on Saturday night." (quoting Woody Allen, 2013 Letter)
**The curse of scale (an important limitation):**
> "There are only a handful of companies in this country capable of truly moving the needle at Berkshire, and they are constantly being evaluated by us and by others. In summary, we are not able to have an out-of-sight performance." — 2023 Buffett Letter to Shareholders
**Key questions for evaluating management's capital allocation quality:**
- Does the incremental return on retained earnings consistently exceed the cost of capital?
- Is management engaged in "empire-building" acquisitions in areas where they have no edge?
- When repurchasing shares, is there a clear intrinsic value basis for doing so?
- When no good opportunities exist, does management have the patience to hold cash?
---
## Share Buyback Analysis
**The only legitimate reason: price is clearly below intrinsic value**
> "Saying that you plan to repurchase shares 'to offset dilution from option grants' or because management has 'faith in the company' is not sufficient reason. Unless the repurchase price is below intrinsic value, continuing shareholders are harmed. The first law of capital allocation — whether the money is slated for acquisitions or share repurchases — is that what is smart at one price is stupid at another." — 2011 Buffett Letter to Shareholders
**The structural advantage of buybacks over acquisitions:**
> "If a fine business can be bought in the market at a price well below what it would cost to acquire the entire business in a negotiated deal, why should the company not repurchase its shares aggressively? Competitive bidding in the acquisition market almost ensures that the full price — and frequently more — must be paid for an entire business." — 1980 Buffett Letter to Shareholders
**The indirect value of buybacks by investees (often overlooked):**
When investees such as Coca-Cola, Apple, and American Express repurchase their shares, Berkshire's ownership percentage rises automatically, with no additional outlay.
> "The lower the price, the more advantageous these repurchase programs are to our benefit: when a company's stock is cheap, it can buy back more shares with the same amount of money, indirectly increasing our ownership percentage." — 1997 Buffett Letter to Shareholders
> "In 2022, we increased your share of our exceptional collection of businesses via repurchases of Berkshire shares as well as similar moves at Apple and American Express. At Berkshire, we directly increased your interest in our unique collection of businesses by repurchasing 1.2% of our outstanding shares." — 2022 Buffett Letter to Shareholders
**The tax logic of buybacks vs. dividends:**
Coca-Cola repurchasing its own shares vs. paying dividends for Berkshire to then reinvest: under the dividend route, taxes must be paid first before reinvesting, resulting in fewer shares purchased. "Yet, paradoxically, if Berkshire takes the less efficient route, our reported earnings would be higher." (1990 Letter)
**The GEICO classic case study (compounding effect of sustained buybacks):**
Between 1976 and 1980, Berkshire invested $47 million to acquire approximately one-third of GEICO. GEICO then continuously bought back its own shares on a large scale, and Berkshire's ownership percentage grew to approximately 50% with no additional purchases. In 1996, Berkshire acquired the remaining 50% for $2.3 billion — roughly 50 times the initial cost.
**The evolution of Berkshire's own buyback program:**
- 2012: Set 120% of book value as the buyback trigger (reasonable, since intrinsic value far exceeded that level)
- Post-2018: Eliminated the fixed multiple; adopted a flexible standard of "when both Buffett and Munger believe the price is below a conservative estimate of intrinsic value"
- 2020: Repurchased approximately 5% of shares outstanding; continued active buybacks in 2022
**Three key questions for evaluating buyback discipline:**
1. Is there a clear intrinsic value estimate underlying the decision, or is management simply saying "the stock is cheap"?
2. Do buybacks accelerate when the stock price falls (correct), or does the company buy heavily when prices rise (incorrect)?
3. After buybacks, does the company still maintain adequate liquidity and a margin of safety?
---
## Dividends, Retained Earnings, and Tax Efficiency
### The One Dollar Test
**The core standard for judging whether retaining earnings is justified:**
> "Unrestricted earnings should be retained only when each dollar retained generates at least one dollar of market value." — 1984 Letter
```
Every $1 of retained earnings → should create ≥ $1 of market value increase
If the ratio is consistently below 1:1 → the money should be returned to shareholders
```
**Restricted earnings vs. unrestricted earnings (an important distinction):**
- **Restricted earnings**: The portion a business must reinvest to maintain its competitive position — effectively "held hostage" by the business; nominally earnings but in reality a capital requirement
- **Unrestricted earnings**: The truly discretionary portion, and the only amount relevant to dividend decisions
- During high-inflation periods, all reported earnings of capital-intensive businesses may be "restricted" — paying dividends under such conditions is effectively eroding the capital base
**The decision logic for dividends vs. retained reinvestment:**
- Management can deploy capital at a return exceeding shareholders' opportunity cost → retain
- No better investment opportunities exist → pay dividends or buy back shares
**Why Berkshire has never paid a dividend:**
Buffett believes he can create more than one dollar of value for every dollar of retained earnings. Once that condition no longer holds, he has pledged to begin paying dividends. Berkshire ranked first in the U.S. in retained earnings in both 2015 and 2016, "by a margin of several billion dollars over the runner-up. Those re-invested funds must earn their keep." (2016 Letter)
**Admiration for dividend growth at investee companies:**
> "In 1994, we received $75 million in cash dividends from Coca-Cola. In 2022, Coca-Cola paid us $704 million. Growth occurred every year, just as certain as birthdays. All Charlie and I were required to do was cash Coca-Cola's quarterly dividend checks." — 2022 Buffett Letter to Shareholders
### Book Value and Its Limitations
**Definition:** Total assets − Total liabilities = Shareholders' equity (book value)
**Buffett's evolving view of book value:**
- **1965–1993**: Used growth in book value per share as the core metric for measuring Berkshire's performance
- **Post-1993**: Recognized that the gap between intrinsic value and book value was widening
- **2018**: Formally abandoned book value as the lead metric on the annual report's first page
> "This scorecard is becoming increasingly disconnected from economic reality." — 2018 Letter
**Three reasons for the disconnect:**
1. Wholly owned subsidiaries are carried at historical acquisition cost, far below their actual value
2. Insurance float is recorded as a liability on the balance sheet, obscuring its hidden value
3. Share buybacks above book value reduce book value while increasing intrinsic value
**Practical implication:** Book value is a quick reference, not a basis for valuation. Any investment thesis based on book value must explain the true earning power that underlies it.
### P/E Ratio: The Correct Way to Use It
> "A price-earnings ratio, per se, tells you nothing about value unless it can be tied to the company's future stream of cash flows."
**Key insight: The same P/E ratio represents entirely different value depending on the business:**
- A business with a franchise: a 25x after-tax P/E may be reasonable
- An ordinary cyclical business: 25x represents serious overvaluation
**Principles for using P/E ratios:**
- Interpret in the context of the interest rate environment (a lower-rate environment justifies higher multiples)
- Do not make cross-industry comparisons
- Focus on earnings quality, not the multiple itself
- Historical P/E ratios do not guide future value
> "Guessing the business value at which the public will capitalize the earnings of any particular business has nothing to do with being conservative." — 1961 Letter
### Tax Efficiency
**Core insight: Tax deferral = an interest-free loan from the government**
> "In economic substance, this liability resembles an interest-free loan from the U.S. Treasury — one with a repayment date entirely of our choosing." — 1989 Buffett Letter to Shareholders
**The "Rip Van Winkle" comparison (a classic example from the 1989 Letter):**
Both scenarios: $1 doubles every year for 20 years (a 20-fold increase):
- **Long-term hold (no selling)**: Final after-tax value of approximately $692,000
- **Sell and reinvest annually (frequent trading)**: Final after-tax value of approximately $25,250
A roughly 27:1 difference, caused solely by the timing of tax payments. The government collects the exact same percentage of tax in both cases, but "has to wait much longer to receive it."
**The tax advantage of long-term holding (mechanism):**
- During the holding period: not selling = not triggering capital gains tax; the tax dollars remain in the account and continue compounding
- At the time of sale: only the gain is taxed, in a single event
- Frequent switching: every switch triggers a tax payment, continuously shrinking the compounding base
**The tax logic of not paying dividends:**
Shareholders can choose when to sell shares and realize gains (paying tax on their own schedule), whereas dividends force a taxable event regardless of whether the shareholder needs the cash.
> "For a taxpaying investor, the 'cash dividend' approach would be inferior to — and usually far inferior to — the 'sell a little stock' approach." — 2012 Buffett Letter to Shareholders
**Buffett's attitude:** He "proudly pays substantial taxes" — because Berkshire's success depends on the institutional framework the United States provides. Strategic tax planning is fundamentally different from tax evasion.
---
## Special Investment Instruments
### Arbitrage / Workouts
One of three investment pillars during Buffett's partnership era, dedicated to profiting from **publicly announced corporate events** (mergers, reorganizations, liquidations).
**The fundamental difference from ordinary investing:**
- Ordinary investing: returns depend on market sentiment and long-term business value
- Arbitrage: returns depend on whether a specific event occurs, independent of overall market moves
**Four questions for evaluating an arbitrage opportunity (Buffett's framework):**
1. What is the probability that the event occurs as announced?
2. How long will the capital be tied up?
3. Is there a possibility of a better outcome (e.g., the deal price being raised)?
4. How severe are the consequences if the event fails?
**Execution principles:**
- Only engage in arbitrage based on public announcements; never rely on inside information
- Concentrate in a small number of large transactions rather than spreading across 50 small ones
- Use as an efficient substitute for idle cash
**Historical performance:** 63 years of practice, unleveraged annual returns exceeding 20%, well above the market.
### Bonds
**Buffett's basic stance on bonds:** Over the long run, bonds are inferior to equities. But they are an important tool in specific situations.
**Core principles:**
1. **Understand the inverse relationship between interest rates and prices**
The longer a bond's duration, the more sensitive its price is to changes in interest rates. A 20-year bond's price swings far more than a 2-year bond's.
2. **Inflation is the silent killer of bonds**
> "The purchasing power of bond coupons is quietly eroded by inflation."
Long-term bondholders are bearing a substantial risk they may not have fully appreciated.
3. **Contrarian opportunities during crises**
In 2002, Buffett purchased approximately $8 billion of junk bonds; during the 2008 crisis, he made large investments in Goldman Sachs and General Electric convertible preferred shares — fixed-income investments during periods of panic can offer extremely high risk-adjusted returns.
4. **Beware of unfair callable provisions**
Bonds where the issuer retains the right to call early are disadvantageous to holders — they get called away during good times and are stuck during bad times.
### Convertible Securities
A hybrid instrument combining fixed-income downside protection with equity upside participation, frequently used by Buffett as an investment structure during crises.
**Buffett's principles for using convertibles:**
- First, the security must be attractive purely as a fixed-income instrument (downside protection)
- On top of that, the equity conversion right is an additional bonus
- Only transact with management that is trusted and understood
**Notable examples:**
- 1987–1991: Invested nearly $2 billion in five large convertible preferred stock positions (Gillette, Coca-Cola, US Air, etc.)
- 2008 financial crisis: Deployed $14.5 billion into Goldman Sachs and General Electric (preferred stock + warrants)
**An important moment of self-reflection:**
> "I could have bought 60 million shares of Gillette common... but I thought I was being so clever."
Choosing convertible preferred over common stock ultimately left a great deal of money on the table. The lesson: do not let a "cleverer structure" cause you to miss an obviously superior opportunity.
references/buffett-review/07-risk-behavior.md# Risk Identification and Investment Psychology > When to read this file: When deciding when to sell, identifying value traps, assessing leverage/inflation/derivatives risk, or recognizing your own behavioral biases. --- ## When to Sell (Clear Criteria) Buffett's ideal is to "hold forever," but that ideal has preconditions. The following four conditions warrant considering a sale: **Condition One: Intrinsic Value Is Severely Overestimated** - The current price far exceeds a reasonable estimate of intrinsic value - The market is offering a premium you cannot refuse - Capital can be redeployed to opportunities with greater margin of safety > "We are willing to hold a stock on the premise that we bought an excellent business at a reasonable price. If the market offers a price far exceeding intrinsic value, selling is rational." **Condition Two: The Moat Has Been Fundamentally Destroyed** - Not temporary competitive pressure, but structural change - Technological disruption rendering the core competitive advantage obsolete - A fundamental shift in the regulatory environment - Key distinction: differentiating "temporary difficulty" from "permanent impairment" **Condition Three: Management Integrity Is in Question** - Accounting fraud or material deception - Serious self-dealing in capital allocation decisions - Once discovered, sell immediately. Integrity problems do not fix themselves. > "When I realize I've hired a dishonest person, I have no interest in thinking about how smart he might be." **Condition Four: A Significantly Better Opportunity Exists** - Only switch positions when the gap is very clear - The friction costs of frequent switching (taxes, commissions, poor decisions) erode returns - You should not casually replace a high-quality holding based on short-term valuation differences **Reasons You Should NOT Sell (Common Mistakes):** - The stock price has fallen (a lower price does not mean value has changed) - The overall market has declined (a change in Mr. Market's mood) - An analyst has downgraded the stock (unless they've uncovered a material issue you missed) - Pessimistic macroeconomic forecasts --- ## Value Traps **Definition:** Appears cheap, but is continuously destroying value. **The Most Common Characteristics of Value Traps:** | Trap Type | Manifestation | How to Identify | |---------|-----|---------| | Structural industry decline | Revenue in persistent decline, no signs of recovery | Ask: Will this industry still exist in 10 years? | | Commodity-type business | No pricing power; prices set by the market | Ask: What happens if we raise prices by 5%? | | Poor management quality | Even with a decent business, capital allocation destroys value | Review historical capital allocation decisions | | Heavy assets, low returns | Requires continuously high investment to maintain mediocre returns | Is ROIC persistently below WACC? | | Accounting quality issues | Profits look high, but cash flow is poor | Is the cash conversion rate persistently low? | **Buffett's Textile Industry Lesson:** > "When a management with a reputation for brilliance tackles a business with a reputation for bad economics, it is the reputation of the business that remains intact." The textile industry's high asset intensity, lack of differentiation, and global competition made it a permanent value trap — no amount of good management could overcome it. --- ## Leverage Risk > "Only when the tide goes out do you discover who's been swimming naked." **Why Buffett Is Extremely Cautious About Leverage:** - Economic cycles are inevitable; the timing of crises is unpredictable - Leverage amplifies gains in good times, amplifies losses in bad times, and threatens survival - A single liquidity crisis can destroy decades of accumulated wealth **Buffett's Core Argument: Asymmetry of Risk** > "To get something you don't need, you risk something you do need and truly have — that's just crazy. Over the years, our aversion to leverage has indeed dragged on our returns. But Charlie and I sleep well." — 2017 Letter > "A string of wonderful numbers multiplied by a single zero always equals zero. Don't count on being able to get rich twice." — 2022 Letter The deeper meaning: if you use leverage to make money 99 times out of 100, but go bankrupt on the 100th, the previous 99 successes are meaningless — you never get another chance. **Even When It Works Most of the Time, Don't Use It:** > "Many managers will disagree with this policy, arguing that large amounts of debt can enhance shareholder returns. And these more risk-tolerant CEOs are right most of the time." — 2018 Letter The key phrase is "most of the time." Buffett's philosophy is to prepare for those few "exceptions" — the very moments that will utterly destroy those who use leverage. **Safe Leverage Parameters:** - Debt / EBITDA < 2x - Interest coverage ratio (EBIT / Interest) > 5x - Even so, the position must still pass the "worst-case scenario test" **Worst-Case Scenario Test:** Assume revenue declines 30% and remains depressed for 2 years. Can the company: 1. Repay maturing debt? 2. Avoid dilutive financing? 3. Keep the core business intact? **Leverage Effects Should Be Stripped Out When Evaluating Management Performance:** > "The primary test of managerial economic performance is the achievement of a high earnings rate on equity capital employed (without undue leverage, accounting gimmickry, etc.) and not the achievement of consistent gains in earnings per share." — 1979 Letter Many companies improve their return on equity by adding debt, which does not represent management's operating ability. True capability is achieving high returns under low leverage. **Berkshire's Leverage Philosophy:** - Always maintain a large cash reserve (typically >$20 billion) - This drags short-term returns, but guarantees offensive capability in a crisis - > "Never put yourself in a position where you are forced to sell good assets." > "We rarely use much debt and, when we do, we attempt to structure it on a long-term fixed-rate basis. We would rather forfeit attractive opportunities than over-leverage our balance sheet." — 1983 Letter **Berkshire's "Good Leverage" — Insurance Float:** Berkshire actually employs a special form of "leverage," but it is fundamentally different from conventional leverage: it has no fixed maturity date (it cannot be called in), its cost is often below zero (due to underwriting profits), and the total amount grows over the long term. This allows Berkshire to enjoy the benefits of leverage without bearing the fatal risk of conventional leverage (being forced to repay debt at the worst possible moment). --- ## The Impact of Inflation **Inflation Is a Hidden Tax:** > "High rates of inflation create a tax on capital that makes much corporate investment unwise — at least if measured by the criterion of a positive real return to owners. A situation that produces a 12% return on equity for a high-tax corporation, for example, will provide its owners with only about a 4% real return." — 1980 Letter The visible tax (income tax) is levied on nominal income, not real income. Therefore, when inflation is high, investors may earn money nominally but lose in purchasing power terms, while still owing income taxes on those "losses" — a double blow to investors. **Inflation's Differentiated Impact on Different Businesses:** **Protected Businesses (Good Businesses):** - Have pricing power; can pass costs on to customers - Asset-light; do not require large capital reinvestment to sustain operations - Examples: Coca-Cola, Gillette — branded consumer goods can raise prices with inflation > "The favored business must have two characteristics: (1) an ability to increase prices rather easily (even when product demand is flat and capacity is not fully utilized) without fear of significant loss of either market share or unit volume, and (2) an ability to accommodate large dollar volume increases in business (often produced more by inflation than by real growth) with only minor additional investment of capital." — 1981 Letter A classic example: See's Candies can raise prices every year without affecting sales, and does not require large capital investment to maintain capacity. The textile industry was the opposite — it required continuous capital investment to replace equipment and replenish inventory, yet could not pass costs on to buyers. **Businesses That Are Harmed (Bad Businesses):** - Capital-intensive industries: equipment depreciation is calculated at historical cost, but replacement cost rises with inflation - Commodity-type businesses with no pricing power: costs rise, prices set by the market - Service industries with a high labor component **Fixed-Income Investments Are Most Vulnerable to Inflation:** > "Over the past century these instruments have destroyed the purchasing power of investors in many countries, even as the holders continued to receive timely payments of interest and principal. This ugly result, moreover, will forever recur. Governments determine the ultimate value of money, and systemic forces will sometimes cause them to gravitate to policies that produce inflation." — 2011 Letter > "Paper money can become worthless if fiscal behavior gets out of hand… Bonds payable in a given currency can be ruined by runaway inflation." — 2024 Letter **Berkshire's Inflation-Resistance Is "Far From Perfect":** > "Berkshire also can do something to resist the inflation that might someday get out of hand, but this property is far from perfect. Huge and entrenched fiscal deficits have consequences." — 2022 Letter **Investment Priorities in an Inflationary Era:** High ROIC + Asset-light + Pricing power → Most inflation-resistant Low ROIC + Heavy assets + No pricing power → Value eroded by inflation --- ## Derivatives Risk > "Charlie and I are of one mind in how we feel about derivatives and the trading activities that go with them: We view them as time bombs, both for the parties that deal in them and the economic system." — 2002 Letter > "Derivatives are financial weapons of mass destruction, carrying dangers that, while now latent, are potentially lethal." — 2002 Letter **Why They Are Dangerous:** - Leverage is embedded in contract terms, making it difficult to transparently assess true exposure - Counterparty risk concentrates and erupts during a crisis - Mark-to-market accounting creates violent swings in reported profits and losses, distorting judgment - Complexity makes fraud and errors harder to detect **The Accounting Problem of "Mark-to-Myth":** > "Contracts involving multiple reference items and distant settlement dates offer the most ample room for fanciful assumptions… In extreme cases, mark-to-model degenerates into what I would call mark-to-myth." — 2002 Letter The book value of derivatives is built on complex mathematical models, not real market prices. Traders — motivated by bonuses — and CEOs — motivated by stock prices — both have incentives to overstate book values. The truth often only emerges when the contracts are finally settled, potentially years or even decades later. **Systemic Domino Effects:** > "A participant may see himself as prudent, believing his large credit exposures are diversified and therefore not dangerous… Under certain circumstances, however, an exogenous event that causes the receivable from Company A to go bad will also affect those from companies B through Z. History teaches us that a crisis often causes problems to correlate in a manner undreamed of in more tranquil times." — 2002 Letter This passage almost perfectly predicted the 2008 financial crisis — AIG's credit default swap positions triggered a cascading collapse across the entire financial system. **Derivatives Are "Easy to Enter, Hard to Exit":** > "Like Hell, it's easy to enter and almost impossible to exit." — 2004 Letter The winding down of General Re Securities took years: 10 months after the decision to cease operations was made, 14,384 contracts were still outstanding involving 672 counterparties. This painful experience left Buffett with a deeply memorable lesson. **Derivatives Checklist When Analyzing a Company:** - Does the company have substantial off-balance-sheet derivatives exposure? - Is the net exposure clearly disclosed? Who are the counterparties? - If a counterparty runs into trouble, how severely is the company impacted? **Berkshire's Iron Rule:** > "We never will operate in a manner that requires us to suddenly need large sums of cash. That means we won't expose Berkshire to short-term debt maturities of size nor will we enter into derivative contracts or other business arrangements that could require large collateral calls." — 2014 Berkshire Past, Present and Future **Buffett's Selective Use:** Berkshire does use derivatives, but only when: - The risk can be clearly understood and priced - No margin calls are involved (not forced to close positions at the worst moment) - Large premiums are collected upfront (essentially receiving free float) - Positions will not trigger systemic risk > "We have long invested in derivatives contracts that Charlie and I think are mispriced, just as we try to invest in mispriced stocks and bonds." — 2009 Letter --- ## Common Behavioral Biases (Psychological Traps in Investing) **1. Confirmation Bias** After buying, you only seek out information that supports your judgment and ignore contrary evidence. - Countermeasure: Actively seek evidence that can falsify your thesis; listen to people who hold the opposite view. **2. Sunk Cost Fallacy** Continuing to hold because the stock "has already fallen a lot" or because "I've already bought it." - Countermeasure: Ask yourself, "If I had no position today, would I buy at this price?" **3. Anchoring Effect** Using the purchase price, historical high, or some arbitrary number as a reference point for decisions. - Countermeasure: Focus only on the relationship between intrinsic value and the current price. **4. Recency Bias** Overweighting recent events and underweighting long-term trends. - Countermeasure: Think in a 10-year framework, not "the last few quarters." **5. Action Bias** Feeling like you "should do something," even when the best decision is to do nothing. - Buffett's answer: Inaction is also a conscious decision. > "We don't need to be smarter than everyone else. We just need to make fewer stupid mistakes than everyone else."
references/buffett-review/08-industry-playbooks.md# Industry Analysis Playbooks > When to read this file: When analyzing companies in a specific industry, look up the corresponding evaluation framework, key metrics, historical cases, and macro sensitivities by industry. Different industries operate on fundamentally different economic logic. Analyzing a bank the way you would analyze a consumer goods company, or analyzing a utility the way you would analyze a technology company, will lead to wrong conclusions. --- ## Insurance Buffett's most familiar and beloved industry, and the core engine of Berkshire. Acquired National Indemnity for $8.6 million in 1967; float has since grown from $19 million to over $164 billion (a more than 8,000-fold increase), and National Indemnity's net worth now stands at $111 billion — more than any other insurance company in the world. ### The Industry's Unique Logic **Insurance is a "collect now, pay later" business.** Between premium income and claims payouts, insurers hold large amounts of freely investable "float." An excellent insurance company = access to **free or even negative-cost** capital for long-term investment. The industry's average ROE is only 8.5%, far below the Fortune 500 average of 14%. The reason is that most insurers write business at a loss in pursuit of market share. Berkshire's principle is exactly the opposite: walk away from business rather than write at a loss. ### Core Evaluation Metrics | Metric | Description | Excellence Standard | |-----|-----|---------| | Combined Ratio | (Claims + Operating Expenses) / Premium Revenue | < 100% (underwriting profit) | | Underwriting Profit/Loss | Premium Revenue − Claims − Expenses | Consistently positive | | Float Size | Total funds available for investment | Larger is better | | Float Growth Rate | Trend in float growth | Steady growth | | Portfolio Quality | How float is invested | Conservative, highly liquid | ### Underwriting Discipline (Most Important) > "We would rather reduce our volume than accept an unattractive risk." **Berkshire's core principle:** Float only has value when its cost is below the return on investment. If you accept underwriting losses to chase float volume, you end up borrowing money at a higher cost. In 2008, Berkshire achieved underwriting profit for the sixth consecutive year, and the cost of $58.5 billion in float was below zero — holding that float actually generated $2.8 billion in income. **Methods to test underwriting discipline:** - Combined ratio history: Was discipline maintained even during periods of intense competition? - Performance after catastrophe years (hurricanes, earthquakes) - Has the company ever priced aggressively to grab market share? ### GEICO's Sources of Competitive Advantage **Cost advantage**: Direct distribution model, no agent intermediaries; operating costs are 15–20% lower than peers. This cost advantage is passed on to consumers in the form of lower prices, creating price competitiveness that in turn supports scale expansion. ### Macro Sensitivities Insurance's sensitivity to the macro environment is primarily felt on the investment side rather than the underwriting side: - **Rising interest rates**: Improves investment returns on float, especially beneficial for insurers with large long-duration bond allocations - **Inflation**: Increases claims costs (medical, repair, replacement); compresses margins if underwriting pricing cannot keep pace - **Economic recession**: P&C premium volumes shrink as assets decline, but catastrophe insurance demand is inelastic; life insurance claims tend to be counter-cyclical - **Capital market crashes**: Affects portfolio values; Berkshire holds large equity positions, so market volatility directly impacts book value - **Catastrophe events (earthquakes/hurricanes)**: Short-term underwriting losses, but often bring pricing hardening opportunities — disciplined companies can expand aggressively as competitors retreat > "Property/casualty insurance is the core business of Berkshire's prosperity and growth. We've operated in this industry for 57 years. Although we've grown from $17 million to $83 billion — nearly a 5,000-fold increase — we still have significant room to grow." — 2023 Letter --- ## Underwriting Discipline Underwriting discipline is the core of insurance industry analysis and deserves a dedicated, in-depth treatment. ### The Essence: The Ability to Say "No" Underwriting discipline means that an insurer strictly adheres to sound pricing standards when accepting risk, and would rather forgo premium income than write business priced inadequately. National Indemnity's founder Jack Ringwalt upheld the principle of "underwriting must be profitable" from 1941 onward — "everyone in the industry talks about it; very few actually practice it" (1969 Letter). **The root cause of why underwriting discipline is hard to maintain**: When the industry is profitable, capital floods in and competitors cut prices to grab market share. Holding the line on proper pricing at that point means a sharp drop in volume — which requires enormous courage and organizational culture to sustain. ### Three Key Principles > "1. They accept only those risks that they are able to properly evaluate (staying within their circle of competence), and after having evaluated all relevant factors — including worst-case loss scenarios — they determine that those risks have a positive expected profit. These insurers never target market share, and they happily step aside when competitors take business at foolish prices or terms. > 2. They strictly limit the business they accept so that a single event or series of related events cannot produce a concentration of losses that threatens the company's solvency." — 2001 Letter ### The Relationship Between Underwriting Discipline and Float Cost Underwriting discipline determines the cost of float: - Combined Ratio < 100%: Float has a negative cost (others are paying you to invest) - Combined Ratio = 100%: Float is free - Combined Ratio > 100%: Float has a positive cost, equivalent to high-interest borrowing Berkshire achieved underwriting profit for 12 consecutive years from 2003 to 2014. Float grew from $41 billion to $84 billion, and cumulative pre-tax underwriting profit over that period totaled $22 billion — free use of tens of billions in capital while earning a return on top of it. ### Organizational Culture Is the Foundation > "We have enormous respect for the underwriting discipline at National Indemnity — a discipline instilled by founder Jack Ringwalt from the very beginning — and we know that once this discipline is broken, it can never be fully restored." — 1980 Letter Berkshire never uses premium growth as a metric for evaluating management. Insurance managers know that a drop in volume because they refused to write inadequately priced business will never draw criticism from headquarters — this culture is the bedrock on which sustained underwriting discipline rests. ### General Re: Discipline Collapse and Rebuilding After acquiring General Re in 1998, underwriting discipline broke down. Under Joe Brandon's leadership, that discipline was rebuilt — proving that even in a large organization, underwriting discipline can be restored, provided top management is unwaveringly committed. **Warning against misconceptions:** - Size and brand are not equivalent to underwriting discipline — many of the largest, most well-known insurers have long delivered mediocre results - "Making up underwriting losses with investment income" is the most dangerous illusion in insurance — in effect, it means gambling with policyholders' money --- ## Insurance Float ### Definition Float is the money an insurer temporarily holds between collecting premiums and paying claims. This money does not legally belong to the insurer, but it can be invested during the holding period. In some workers' compensation or asbestos litigation cases, payouts may span decades, creating extremely long float holding windows. > "Unless you understand this concept, you simply cannot make a sensible judgment about Berkshire's intrinsic value." — 1997 Letter ### Growth Trajectory | Year | Float Size | |-----|---------| | 1967 (National Indemnity acquisition) | $19 million | | 2008 | $58.5 billion | | 2021 | $147 billion | | 2022 | $164 billion | From 1967 to 2022, float grew more than 8,000-fold. ### Three Structural Advantages of Float **1. Cannot be "run on"**: P&C insurance contains no redemption clauses; even if premium income falls, float decreases only very slowly — a stark contrast to the fragility of bank deposits. **2. Near-permanent capital**: Even though individual policies constantly flow in and out, total float remains remarkably stable relative to premium income. In economic effect, it resembles permanent capital rather than short-term borrowing. **3. Understated book value**: Float is recorded as a liability on the balance sheet, which actually reduces net assets in a standard book value calculation. But for Berkshire — which can obtain float at zero cost — this "liability" is a hidden asset. > "Although not recognized in our financial statements, this float is a magnificent asset for Berkshire." — 2022 Letter ### How Float Transmits Competitive Advantage Float provides Berkshire with three layers of competitive advantage: 1. Low-cost or zero-cost investment capital — enabling large-scale investment without borrowing 2. Acts as "buyer of last resort" during financial crises (emergency financing to Goldman Sachs and GE in 2008) 3. Its long-term, stable nature allows investment horizons unconstrained by short-term liquidity pressures **The irreplicability of float**: Three conditions must be met simultaneously — superior underwriting discipline (driving cost toward zero), massive scale (providing a substantial investment base), and outstanding investment capability (generating excess returns). All three are indispensable. --- ## Banking Buffett has held large positions in bank stocks (Wells Fargo, Bank of America, etc.) and has also learned hard lessons (Salomon Brothers, Irish banks). He believes excellent banks can be outstanding investments, yet understands that leverage makes banking one of the most dangerous industries of all. ### The Industry's Special Nature - **Inherently high leverage**: Banks typically operate at 15–20x leverage; modest credit losses can destroy shareholder equity - **Credit quality is the lifeline**: The quality of the loan portfolio determines long-term fate; problems typically surface only years later - **Management integrity is critically important**: Banking's complexity makes fraud and concealment easier ### Core Evaluation Metrics | Metric | Description | Excellence Standard | |-----|-----|---------| | ROE | Return on Equity | Consistently > 15% | | ROA | Return on Assets | > 1.2% (high-quality banks) | | NPL Ratio | Non-Performing Loans / Total Loans | < 1% | | Provision Coverage Ratio | Loan Loss Reserves / NPLs | > 200% | | CET1 | Core Equity Tier 1 / Risk-Weighted Assets | > 10% (with buffer above regulatory minimum) | | NIM | Net Interest Margin (loan rate − deposit cost) | Compare vs. industry; watch trend | | Efficiency Ratio | Operating Expenses / Revenue | < 55% | ### Buffett's Warnings on Banking - Avoid banks with opaque loan portfolios - Avoid banks overly reliant on capital markets activities (as opposed to traditional lending and deposit-taking) - If management says "loan quality is very good" but cannot elaborate, that is a red flag - During banking crises, "looks cheap" is often a trap — because real losses have not yet surfaced > "Banking doesn't have to be a bad business. If management can manage loans well and control costs, ROE can reach 20% or higher. But if management makes serious mistakes — which is not uncommon in banking — shareholders can lose everything." — 1991 Letter ### Wells Fargo's Long-Term Appeal (Historical) Large purchase in 1990 during a panic: California's largest deposit base (extremely low-cost funding source) + management (Carl Reichardt) renowned for efficiency and discipline + market overly panicked about the California real estate crisis. Cost advantage (low-cost deposits) + cross-selling capability + conservative credit culture = sustained high ROE But Wells Fargo's later fake-accounts scandal demonstrated another banking risk — once corporate culture corrodes, the consequences are extremely severe. ### Case Study in Failure: Irish Banks > "In 2008 I spent $244 million buying two Irish banks that appeared cheap to me. By year-end, I had lost 89% of my investment. In tennis terms, what I did is called an 'unforced error.'" — 2008 Letter ### Why He Ultimately Moved Away from Banking The "black box" nature of banking makes it nearly impossible for outside investors to truly assess risks — loan quality, derivatives exposure, and trader behavior can all accumulate fatal vulnerabilities beneath a calm surface. After 2020, Buffett substantially reduced almost all bank holdings, including Wells Fargo, held for many years. ### Macro Sensitivities Banking is one of the industries most sensitive to the macro environment: - **Rising interest rates**: Net interest margins expand in the short term, but long-duration bond portfolios incur unrealized losses, and rising deposit costs can erode spreads - **Economic recession**: Loan default rates rise; credit losses are amplified (under high leverage, modest losses can wipe out equity) - **Deflation/falling asset prices**: Collateral values decline, loan quality deteriorates sharply (the core mechanism of the 2008 crisis) - **Tightening regulation**: Higher capital requirements compress ROE; business scope restrictions constrain revenue diversification - **Liquidity crisis**: Bank runs occur far faster than in other industries; a collapse in market confidence can destroy an apparently healthy institution within days --- ## Consumer Brands & Retail One of Buffett's most successful investment areas (Coca-Cola, Gillette, See's Candies, Nebraska Furniture Mart). ### See's Candies: The Turning Point in Investment Philosophy Acquired See's for $25 million in 1972, fundamentally changing Buffett's approach — from Graham-style "buying cheaply" to Munger-style "buying great businesses at fair prices." See's taught Buffett three things: (1) brand power can generate returns far beyond tangible assets; (2) businesses with pricing power can withstand inflation; (3) asset-light, high-cash-flow business models are far superior to capital-intensive ones. > "Owning See's taught us much about the evaluation of franchises. We've made significant gains on certain stock investments that we would not have made had it not been for the lessons we learned at See's." — 1991 Letter By 2007, See's had contributed more than $1.3 billion in pre-tax profit, with almost no additional capital required. **Asset-light + pricing power = compounding machine.** ### Coca-Cola: Eternal Consumer Logic Began buying heavily in 1988, ultimately investing $1.3 billion. People all over the world drink more Coca-Cola every day — a trend that won't change for decades. Has held the stake for more than 35 years; annual dividends now far exceed the original purchase cost. ### Nebraska Furniture Mart: The Retail Success Template Mrs. B (Rose Blumkin) immigrated to America from Russia and built the largest home furnishings store in the U.S. from scratch, constructing a moat through thin margins, high volume, and extremely low-cost operations. > "I'd rather wrestle grizzlies than compete with Mrs. B." **Flywheel logic:** Cost advantage (low rent + direct purchasing) → lower prices → greater scale → lower costs ### Consumer Brand Moat Characteristics - **Repeat purchase**: Daily/weekly, habitual, no need to re-decide each time - **Emotional bond**: Consumers form emotional attachments to brands; rational factors recede - **Pricing power test**: Raise prices 5–10%, with almost no change in volume → brand moat is real - **Global expansion**: Same product distributed globally, with declining marginal costs ### Risks in Consumer Retail > "Retailers have to be smart every day. Competitors are always copying you, and then surpassing you." - **Brand aging**: Tied to a specific age group while losing younger consumers - **Health/cultural trends**: Carbonated beverages under pressure from health consciousness - **Channel disruption**: E-commerce has rewritten the rules of distribution - **Moats in retail are usually narrower than in consumer brands**: Many retailers once grew astonishingly fast, then reversed sharply - **Dexter Shoe lesson**: Some acquired retail businesses performed poorly, demonstrating the extreme importance of choosing the right management in retail ### Macro Sensitivities - **Economic recession**: Staple consumer products like Coca-Cola and See's are relatively resilient; discretionary consumption (jewelry, furniture) sees significant demand decline - **Inflation**: Consumer brands with pricing power can pass through costs; rising raw material prices particularly test pricing capability - **Rising interest rates**: Discount values of high-multiple consumer brands decline; discretionary consumption under pressure from higher credit costs - **Currency fluctuations**: Global consumer brands (Coca-Cola's worldwide sales) are affected by foreign exchange, but also form a natural hedge --- ## Media & Publishing Buffett once held large positions (The Washington Post, Buffalo News), then largely exited. In 2020, newspaper assets were sold to Lee Enterprises, marking a formal exit from the media industry. ### The Former "Toll Bridge" In the 1970s and 1980s, a city's monopoly daily newspaper was like a "toll bridge" — advertisers had no choice but to go through it to reach local consumers. These businesses shared common characteristics: minimal capital requirements, strong pricing power, and stable, spring-like cash flows. - **Local newspaper monopoly**: A city typically had only one mainstream newspaper; advertisers had no alternative - **Stable classified ad revenue**: Job listings, real estate, automotive advertising - **High barriers to entry**: Printing facilities, distribution networks ### Buffalo News: A Classic in Monopoly Profits Acquired in 1977 after a fierce competitive battle; after defeating the competition, it became Buffalo's only daily newspaper. Highly profitable during the monopoly period, but it ultimately could not escape the industry's decline. This case perfectly illustrates the core philosophy that "industry fate outweighs management capability." ### The Process of Moat Erosion > "The economics of newspapers, television stations and magazines have eroded somewhat — though much of that erosion has been leisurely — and these businesses are now far less exceptional than they were 25 years ago." — 1991 Letter (a landmark warning) The internet systematically destroyed this moat: - Craigslist / classified listing sites eliminated classified advertising (once the lifeblood of newspaper profits) - Digital media broke local monopolies - Readers migrated to free online content; the commoditization of news caused subscription revenue to plummet **Buffett's media investment lesson:** > "Technological disruption can destroy a moat built over decades in a matter of years. Industry analysis must include thinking about 'what technological threats could render this moat irrelevant.'" The **durability** of a moat matters more than its current **width** — this is the deepest lesson left by the media industry. ### Modern Media / Platform Analysis Framework **New moat sources (digital era):** - Subscription stickiness (Netflix, Spotify) - Content ecosystem (Disney's IP portfolio) - Data network effects (algorithm-driven recommendation platforms) **Key question:** Is this company's revenue derived from genuine user value, or from monetizing user attention (advertising)? The latter has a more fragile moat. ### Macro Sensitivities - **Economic recession**: Advertising spending is highly pro-cyclical; ad budgets are the first to be cut in downturns; subscription revenue is relatively stable - **Inflation**: Paper, printing, and distribution costs rise; traditional media no longer has sufficient pricing power to pass through costs - **Technological disruption**: The internet's impact on newspapers is structural, not cyclical — this is not a macro problem but an industry extinction event --- ## Energy & Utilities Berkshire Hathaway Energy (BHE, formerly MidAmerican Energy, acquired in 2000) is an important capital allocation vehicle for Buffett. Under Greg Abel's management, it grew from an Iowa electric utility into a vast utility empire spanning the United States and the United Kingdom. ### Industry Characteristics - **Capital-intensive, long-cycle**: Payback periods for power grids, pipelines, and power plants are 20–40 years - **Regulated, stable returns**: Utilities are regulated; return rates are set by regulators - **Stable cash flows**: Demand is inelastic — people don't cut electricity because rates rise 5% ### Why Buffett Likes Regulated Utilities - Stable, predictable cash flows - Can absorb large amounts of capital (Berkshire's capital needs deployment) - Regulation provides implicit protection (competition cannot eliminate them) - Renewable energy investment has policy-supported long-term growth potential > "Charlie and I are enthusiastic about utility businesses. In this capital-intensive industry, new project investment sizes are often very large. Such projects give Berkshire the opportunity to put large sums of money to work at decent returns." — 2008 Letter ### Renewable Energy: The Iowa Case > "When it comes to wind, Iowa is the Saudi Arabia of the United States." — 2016 Letter Wind power generation in Iowa grew from zero to nearly 20% of total installed capacity, making it one of the states with the highest wind energy penetration in the U.S. The resulting low-cost renewable electricity attracted large numbers of data centers (technology companies with enormous power consumption) to build facilities in the state. ### Oil Investments: Opportunism and Lessons **PetroChina (success)**: Bought a 1.3% stake for $488 million in 2002, believing the market value was far below the value of its oil reserves. Sold in 2007 for approximately $4 billion, roughly an 8x gain. **ConocoPhillips (lesson)**: Made a large purchase at peak oil prices in 2008; oil prices subsequently collapsed. Buffett admitted he "bought a large amount of ConocoPhillips stock when oil and gas prices were near their all-time highs," calling it an error in judgment. **Occidental Petroleum (recent)**: Provided $10 billion in preferred stock financing in 2019 to support its acquisition of Anadarko, then continued building a common stock position in the open market — reflecting confidence in the long-term value of U.S. oil and gas assets. ### Evaluation Points - Regulatory friendliness (historical rate approval track record) - Sustainability of capital expenditure plans (aging transmission networks, new energy construction) - **Interest rate sensitivity**: High leverage + capital-intensive assets = valuation pressure when rates rise - Economic growth trends in the service territory (affects electricity demand) ### Macro Sensitivities - **Rising interest rates**: Utilities are viewed as "bond substitutes"; rising rates cause significant valuation pressure; high debt increases interest expense - **Inflation**: Capital expenditure costs (steel, equipment, labor) rise; regulatory approval of rate adjustments lags, compressing short-term profits - **Energy price volatility**: For power generators, fuel cost is the key variable; the regulated model allows pass-through via rates, but with a time lag - **Policy and regulatory changes**: Renewable energy tax incentives affect investment returns; shifts in the regulatory stance directly affect allowed rates of return --- ## Railways In 2009, Buffett acquired BNSF Railway outright for $34 billion — Berkshire's largest single acquisition in its history. Railways were once Wall Street's nightmare — capital-intensive, heavily regulated, powerful unions, poor returns. But after deregulation and consolidation in the 1980s, North American railways consolidated from more than seventy companies into six major systems, fundamentally improving the competitive landscape. > "This is an all-in wager on the economic future of the United States." — Buffett, 2009 ### Sources of Competitive Advantage - **Irreplicability**: The major U.S. rail network took 100 years to build; it simply cannot be rebuilt today. BNSF's assets have a book value of $70 billion; replicating those assets would require at least $500 billion and decades of time - **Efficient scale**: Major trunk lines are already saturated; new entrants would find it unprofitable - **Cost advantage**: A single train can move the same freight as trucks while using one-third the fuel; as energy costs rise and environmental pressures increase, this efficiency advantage will only become more pronounced > "Railroads are essential to the future of the U.S. economy. From cost, fuel consumption, and carbon intensity, rail is clearly the most efficient way to move heavy freight to distant destinations. Without railroads, this country simply doesn't work." — 2023 Letter ### The Other Side of Railways: Capital Consumers Buffett candidly acknowledges railways' weakness — just to maintain BNSF's current operating level, annual capital expenditures must substantially exceed depreciation. In the 14 years since acquisition, cumulative capital expenditure above and beyond depreciation has totaled $22 billion. This means the actual distributable profit of a railway company is far below reported earnings. ### Core Evaluation Metrics | Metric | Description | Excellence Standard | |-----|-----|---------| | Operating Ratio | Operating Costs / Revenue | < 65% (industry-leading) | | Freight Volume Trend | Correlation with GDP | Tracks economic activity | | Fuel Efficiency | Fuel is the primary variable cost | Continuously improving | | Capital Expenditure Intensity | Required investment to maintain safety and efficiency | Compare vs. depreciation | **Long-term value judgment:** The more prosperous the U.S. economy, the greater the freight volume, and the more railways benefit — perfectly aligned with Buffett's long-term belief in "betting on America." > "A century from now, BNSF will still be a vital asset for this country and for Berkshire. Count on it." — 2023 Letter ### Macro Sensitivities - **Economic cycle**: Freight volume is highly correlated with GDP; in a recession, coal, grain, and automotive volumes fall, and the operating ratio rises - **Energy prices**: Fuel is the primary variable cost; rising oil prices directly compress margins, though railways can pass some through via fuel surcharges - **Rising interest rates**: Capital-intensive industry; high debt increases financing costs; infrastructure valuations are pressured by higher discount rates - **Trade policy**: Import/export freight volumes are affected by tariffs and trade frictions; coal exports are constrained by international energy policy - **Decarbonization trend**: Long-term positive for railways (lower carbon footprint than trucks); may shift freight share toward rail --- ## Technology & Internet Buffett long avoided technology stocks, but began buying Apple aggressively in 2016. This shift reveals his framework for analyzing technology companies. ### Buffett's Framework for Analyzing Technology **Key question: Is the moat a temporary technology lead, or a structural advantage?** | Moat Type | Durability | Example | |---------|------|-----| | Technology leadership | Low (can be surpassed/copied) | Early Nokia | | Network effects | High (stronger with more users) | WeChat, Visa | | Switching costs | High (high migration cost) | SAP, Apple ecosystem | | Data flywheel | High (data accumulation is irreversible) | Google Search | | Brand | High (emotional bond) | Apple hardware | > "We don't own technology stocks, though we think technology products and services will profoundly change our society just as much as you do. Our problem is that we don't know which of the many participants in the technology industry will end up with truly durable competitive advantages." — 1999 Letter ### IBM: A Failed Attempt In 2011, Buffett unexpectedly made a large investment in IBM, committing more than $13 billion, believing IBM had deep customer stickiness and stable cash flows. But the cloud computing revolution was eroding IBM's traditional businesses, and Buffett failed to foresee this structural shift. He gradually sold out most of the IBM position in 2017–2018, frankly acknowledging that "my assessment of IBM was wrong." This case validates his early caution — even Buffett can misjudge the technology sector. ### Why He Bought Apple (An Important Case Study) Buffett **redefined Apple as a consumer goods company, not a technology company**: - Moat = brand emotional bond + iOS ecosystem switching costs - The psychological and economic cost for users to switch phones is extremely high (photos, apps, habits, device interconnectivity) - Can be understood through the consumer brand analysis framework (pricing power, repeat purchase, emotional value) Began buying in 2016; by 2023, Apple accounted for nearly 50% of Berkshire's equity portfolio — the most profitable stock investment Berkshire has ever made. > "Apple is a remarkable company. People buy iPhones because of iPhones, just like people drink Coca-Cola because of Coca-Cola." ### The Valuation Challenge for Technology Companies - Growth-stage companies typically have no current earnings; traditional valuation methods break down - Alternative metrics like user counts, GMV, and MAU require caution — they are still far removed from real profitability - Buffett's principle: **if intrinsic value cannot be reliably estimated, don't invest** — no matter how impressive the company looks ### Honest Reflection on Missing Amazon and Google > "I was too dumb. I studied Amazon but didn't buy. I understood what Bezos was doing, but I underestimated how far he could take it." GEICO spends large amounts on Google advertising every year; Buffett should have seen the power of Google's business model earlier than almost anyone. This is a classic "miss within the circle of competence" — more painful than missing something outside the circle. Better to miss than to overreach. ### Macro Sensitivities - **Rising interest rates**: High-growth tech company valuations rely heavily on discounting far-future cash flows; rising rates are devastating (validated in 2022) - **Economic recession**: Ad-tech companies (Google, Meta) are pro-cyclical; SaaS subscription models are relatively resilient; consumer tech (gaming, streaming) is partly counter-cyclical - **Regulation**: Antitrust investigations and data privacy legislation (GDPR, etc.) pose structural risks to platform companies - **Geopolitics**: Geopolitical risks around supply chains (semiconductors) and market access (China) are increasingly prominent --- ## Industries to Avoid (Counter-Examples) ### Airlines > "Since Kitty Hawk, the net return from all airlines' efforts has been minus zero. Investors have poured money into a bottomless pit, attracted by the lure of big margins in a boom and comforted by the knowledge that the great Orville Wright had done it first. Had a farsighted capitalist been present at Kitty Hawk, he would have done his successors a huge favor by shooting Wilbur down." — 1992 Letter **Structural industry problems:** - Seats are a commodity: routes can be replicated by any competitor with no differentiation - High fixed costs + strong cyclicality + uncontrollable fuel prices - Powerful unions; labor costs are rigid - Once capacity is reduced, competitors immediately fill the void **A history of three painful encounters:** 1. **1989**: Invested $358 million in USAir preferred stock. From 1990 to 1994, the company accumulated $2.4 billion in losses; preferred stock fell to 25% of cost before eventually returning principal — a harrowing experience. 2. **2016**: Concluded the industry had oligopolized; bought all four major U.S. airlines (Delta, Southwest, United, American). 3. **2020**: COVID outbreak, sold at a loss, admitted it was a mistake. > "Richard Branson was asked how to become a millionaire. He said: 'There's really nothing to it. Start as a billionaire and then buy an airline.' Your chairman has ignored this advice." — 1996 Letter **The one exception — NetJets:** Fractional jet ownership, 75% market share, a genuine moat. It is the **business model** that is different, not the industry itself, that determines investment value. ### Macro Sensitivities (Airlines) - Demand is highly cyclical; business travel is the first to be cut in half during recessions - Fuel costs (30–40% of operating costs) fluctuate sharply with oil prices - Black swan events (pandemics, 9/11, wars) cause structural demand shocks - Rising interest rates increase aircraft financing costs --- ### Textiles Buffett's most important counter-example, and the origin of Berkshire (1965–1985). > "A good managerial record is far more a function of what business boat you get into than it is of how effectively you row." — 1985 Letter **Four core lessons:** 1. **Industry economics trump management capability** Ken Chace and Gary Morrison were rated by Buffett as equal to the managers of his most profitable businesses — yet they could not fight industry structure. > "When a management with a reputation for brilliance tackles a business with a reputation for bad economics, it is the reputation of the business that remains intact." — 1985 Letter 2. **The "Standees at a Parade" Trap** Each textile mill, viewed in isolation, had a rational case for each new capital investment. But when everyone did it, overcapacity drove down prices across the industry — all participants invested more capital, yet returns remained flat. > "The promised benefits from these textile investments are illusory... It's like everybody standing up to get a better view at a parade. Each time a company invested and got the promised cost reduction, all that happened was that the ante was raised for all the other textile producers." — 1985 Letter 3. **Supply shortages are extremely brief** > "The most recent time it appeared that the textile business would have a period of subnormal supply, the period of subnormal supply lasted the better part of a morning." — 1982 Letter 4. **Scale cannot rescue a bad industry** Burlington Industries was the largest company in textiles: $1.2 billion in sales in 1964 (24 times Berkshire's), growing to $2.8 billion by 1985, with cumulative capital expenditure over the period of approximately $3 billion. The result: sales volume declined in real dollar terms, margins and returns far below 20 years prior, and stock price adjusted for inflation was only one-third of the original. **Buffett's candid admission:** > "I wanted it to work, so I kept making investment decisions that were more optimistic than the facts warranted. (I even bought another New England textile company.) Wishful thinking has no place in business; in business it is a poison." — 2012 Letter **The significance of the transformation:** The textile operations were closed in 1985. The 1967 use of textile business cash to acquire National Indemnity proved to be the most pivotal turning point in Berkshire's history. These 20 years of struggle directly gave birth to the philosophical shift toward "buying excellent businesses at fair prices." **Main failed companies:** - Berkshire Hathaway Textile Mills (New Bedford): operated 1965–1985 - Burlington Industries: $3 billion in cumulative investment over 20 years; negative shareholder returns (inflation-adjusted) - Waumbec Textile Mills: acquired by Buffett in an attempt to generate synergies; ultimately closed in 1980 **The universality of the "Standees at a Parade" effect:** This effect is not unique to textiles but is the common curse of all commodity-like industries — airlines, retail, banking, telecommunications. Any industry where products lack differentiation and competitors can freely enter faces the same economic predicament.
references/financial-analysis/china-a-share-data-sources.md# 中国 A 股数据源指引
> ⚠️ 关键信号:当用户问「妙想的 skill」时,指的是 mx-* 前缀的全部技能(mx-data / mx-search / mx-xuangu / mx-zixuan / mx-moni),而非特定某个。切勿回答「没有妙想 skill」。
## 妙想 (mx-*) 技能全景
东方财富妙想 Skills Hub 提供了 5 个 mx-* skill,覆盖整个 A 股分析工作流:
| Skill | 文件名 | 用途 | 在 CFO 分析中的角色 |
|-------|--------|------|-------------------|
| **mx-data** | `mx-data/SKILL.md` | 金融数据查询(行情、财务、关联关系) | **主力数据源** — 覆盖 yfinance 缺失的 A 股 SBC/回购/分红/细分行业 ROE |
| **mx-search** | `mx-search/SKILL.md` | 妙想搜索 — 新闻、公告、研报、政策、事件解读 | **时效性信息** — 财报解读、机构观点、行业政策变化 |
| **mx-xuangu** | `mx-xuangu/SKILL.md` | 智能选股 — 按行情/财务指标筛选股票 | **筛选管道** — 初筛后由 financial-analysis 做深度分析 |
| **mx-zixuan** | `mx-zixuan/SKILL.md` | 自选股管理 — 查询/添加/删除 | **跟踪管理** — 维护观察列表 |
| **mx-moni** | `mx-moni/SKILL.md` | 模拟组合管理 — 买卖/撤单/持仓/资金查询 | **策略验证** — 模拟盘验证投资假设 |
**所有 mx-* 技能共用同一 API Key**:`MX_APIKEY`(从 https://dl.dfcfs.com/m/itc4 获取)。
调用方式 — 用 skill_view 加载后直接使用 mx_data.py / mx_search.py / mx_xuangu.py 等 Python 脚本,通过自然语言查询。
**在 CFO 分析中的推荐调用顺序:**
1. `mx-xuangu` → 初筛候选标的
2. `mx-search` → 获取标的近期新闻/研报/公告(补充管理层评估)
3. `mx-data` → 拉取精确财务数据(替代/校验 yfinance)
4. `mx-zixuan` → 将关注标的加入自选跟踪
5. `mx-moni` → 验证交易策略(可选)
> 适用场景:CFO 分析 A 股时选择最佳数据通道
> 版本:1.0 — 2026-05-13 实战经验总结
## 数据源质量分级
| 等级 | 数据源 | 覆盖范围 | 速度 | 海外可用 | 适用场景 |
|------|--------|---------|------|---------|---------|
| 🥇 | **mx-data (东方财富妙想API)** | A股全量 | 1-3s | ✅ 稳定 | **首选手工查询** |
| 🥇 | **yfinance** | 美股+A股基础 | 1-2s | ✅ | 美股主力,A股兜底 |
| 🥈 | **akshare (同花顺通道)** | A股财务 | 0.3-2s | ⚠️ 有时限 | A股财务数据 |
| 🥉 | **qt.gtimg.cn (腾讯行情)** | A/H/U 三市 | 0.1s | ✅ **最稳定** | 实时行情兜底 |
| ❌ | **push2.eastmoney.com** | A股实时 | — | ❌ 海外常被屏蔽 | **不要依赖** |
## 关键发现(macOS 海外/非大陆网络环境)
### push2.eastmoney.com 被屏蔽
这是 akshare A股数据的主路径。在海外网络/特定 VPN 下 SSL 握手失败。
**诊断命令:**
```bash
python3 -c "
import requests
try:
r = requests.get('https://push2.eastmoney.com/api/qt/stock/get?secid=1.605499&fields=f43,f44,f45', timeout=5)
print('OK' if r.ok else f'HTTP {r.status_code}')
except Exception as e:
print(f'BLOCKED: {str(e)[:80]}')
"
```
### qt.gtimg.cn(腾讯行情)始终可用
作为 push2 兜底,无论在哪都能 0.1s 返回。支持三市场:
- A股:`sh605499` / `sz300059`
- 美股:`usBZ` / `usPDD`
- 港股:`hk00700`
```bash
curl -s "https://qt.gtimg.cn/q=sh605499" | iconv -f GBK -t UTF-8
```
### MX_APIKEY 是官方解法
东方财富妙想 Skills Hub 提供官方 API(`mkapi2.dfcfs.com`),绕过 push2。
- 申请:https://dl.dfcfs.com/m/itc4(需东方财富APP领取)
- 配置:`export MX_APIKEY=your_key`
- 测试:
```bash
curl -s -X POST "https://mkapi2.dfcfs.com/finskillshub/api/claw/query" \
-H "Content-Type: application/json" \
-H "apikey: $MX_APIKEY" \
-d '{"toolQuery":"605499 最新价 总市值 PE"}' | python3 -m json.tool
```
## CFO 分析 A 股时的数据获取策略
按优先级选择:
```
A股手工查询 → 用 mx_data.py(自然语言,覆盖最全)
↓ 不可用时
A股自动采集 → yfinance(基础数据)+ akshare 同花顺通道(财务数据)
↓ 行情缺失
腾讯 qt.gtimg.cn 兜底(实时价/市值)
```
### mx_data.py 用法
```bash
cd /home/ivan/github/my-skills-repo/mx-data
MX_APIKEY=your_key python3 mx_data.py "东鹏饮料 最新价 总市值 PE ROE 营收增速"
```
输出:
- 终端预览前20行
- Excel 文件存储到 `~/.mx/mx-data/output/`
- 原始 JSON 到 `~/.mx/mx-data/output/mx_data_{query}_raw.json`
### mx-data 原始 JSON 数据提取指南
脚本的输出文件 `mx_data_{query}_raw.json` 的 JSON 结构为三层嵌套:
```python
d['data']['data']['searchDataResultDTO']['dataTableDTOList']
```
每个 `dataTableDTO` 包含:
| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | str | 数据标题 |
| `entityName` | str | 证券全称 |
| `table` | dict | 表格数据 — `headName` 为行标签(年份),其他 key 为数据列 |
| `nameMap` | dict | 列名映射(编码→中文名) |
| `field` | dict | 指标元信息 |
**提取示例:**
```python
tables = d['data']['data']['searchDataResultDTO']['dataTableDTOList']
for t in tables:
head_names = t['table'].get('headName', []) # 行标签(如:['2025年报','2024年报','2023年报'])
name_map = t.get('nameMap', {})
for key, vals in t['table'].items():
if key != 'headName':
col_name = name_map.get(key, key)
# vals 是对应年份的值列表
```
> ⚠️ `data` 字段在 JSON 中是嵌套的:顶层是 `{"success": true, "data": {"data": {...}}}`。
> 所以完整路径是 `d["data"]["data"]["searchDataResultDTO"]["dataTableDTOList"]`。
### 双地上市公司的数据陷阱(A+H 重大差异)
中国移动等 A+H 双地上市公司,A 股报表与港股报表的**折旧摊销(D&A)口径完全不同**:
| 项目 | 600941 A股 | 0941.HK 港股 |
|------|-----------|-------------|
| 折旧摊销(2024) | ¥66.3亿(仅无形资产摊销) | HK$2,064亿(含固定资产折旧+无形资产摊销) |
| 差异原因 | A股只披露"摊销"行,折旧藏在营业成本中 | 港股按 HK GAAP 单独列出折旧与摊销 |
| CapEx(2024) | ¥1,560亿 | HK$2,000亿 |
**教训:分析双地上市的中国公司时,永远不要只信 A 股披露的 D&A 数字。** A 股利润表中的"折旧摊销"可能只含摊销,不含固定资产折旧。必须交叉验证港股报表或从资产负债表推算。
**快速推算方法:**
```python
# 从固定资产原值变动估算年折旧
pp_e_begin = 22946 # 上年末固定资产原值
pp_e_end = 23617 # 本年末固定资产原值
additions = 1570 # 当年 CapEx(购建固定/无形资产支付的现金)
implied_depreciation = pp_e_begin + additions - pp_e_end
# = 22946 + 1570 - 23617 = 899亿(仅为部分折旧,含处置)
# 更准确:查看港股披露的"折旧与摊销"行
# 港股0941.HK 的"折旧与摊销"是完整数字
```
### 数据完整性注意事项
- **收益率格式**:A股返回百分比(4.06=4.06%),美股返回小数
- **SBC/回购/分红**:A股在 yfinance 上大多缺失,需用 mx-data 补充
- **行业分类**:yfinance 对 A 股的行业映射不准(多为 Beverages - Non-Alcoholic 等大分类),mx-data 返回东方财富的精确行业分类
- **ROE 数据**:yfinance 对 A 股的 ROE 可能异常(如东鹏饮料 yfinance 返回 8.4%,实际 32%),mx-data 返回更准确
## mx-data 安装检查
如果 mx-data 查询失败:
1. 确认 `MX_APIKEY` 环境变量已设置
2. 确认 `/home/ivan/github/my-skills-repo/mx-data/scripts/mx_data.py` 存在
3. 确认输出路径是 `~/.mx/mx-data/output/`
4. 测试连通性:见上方 curl 命令
references/financial-analysis/greenwald-arv-adjustments.md# Greenwald ARV 调整细节 本文件记录格林沃尔德 ARV(资产重置价值)计算中的特定调整规则,这些规则来自用户提供的《格林沃尔德三要素估值法与AI执行指南》。在手动计算 ARV 或复核 `calculate_reproduction_value.py` 的输出时参考。 ## 流动资产调整 | 科目 | 格林沃尔德规则 | 常见错误 | |------|--------------|---------| | **现金与等价物** | 运营现金 = 营收 × 1%;超额现金 = 账面现金 - 运营现金。运营ARV中**仅含运营现金**,超额现金在股权转换时加回 | 把全部现金计入运营ARV | | **应收账款** | 调整后A/R = 账面A/R + 坏账准备。竞争对手获取同等客户群必须承担相同的坏账摩擦 | 直接用账面净值(已扣除坏账准备)| | **存货 (LIFO)** | 调整后 = 账面存货 + LIFO 准备金 | 用账面值(通胀下被低估)| | **存货 (FIFO)** | 直接使用账面值 | 无需调整 | ## 长期资产调整 | 科目 | 格林沃尔德规则 | 常见错误 | |------|--------------|---------| | **固定资产(PP&E)** | **⚠️ 致命错误:严禁使用 Net PP&E(扣除折旧后的净值)。** 新进入者购买设备需支付当前市价原值。调整值 = 账面原值(Gross PP&E) + 通胀调整 - 技术过时贬值 | 用 Net PP&E(严重低估重置成本)| | **商誉** | **强制赋值为 0** | 保留账面商誉(收购溢价不应计入重置成本)| | **隐性无形资产** | 将过去数年被费用化的 R&D 和 SG&A 资本化加回:重置研发成本 + 渠道搭建成本 + 人员培训成本 | 忽略(认为无形资产不反映在账面)| ## 负债扣减规则 | 负债类型 | 在运营ARV中扣除? | 在股权ARV中扣除? | |---------|-----------------|-----------------| | 自发性负债(应付/预收/应付工资/应交税费) | ✅ 扣除 | — | | 带息债务(银行贷款/债券) | **❌ 禁止扣除** | ✅ 扣除 | | 环境负债/特定负债 | ❌ 不扣除 | ✅ 扣除 | **原则:** 运营 ARV 衡量的是**资产的重置成本**,与资本结构(融资方式)无关。带息负债属于资本结构问题,只在转股权 ARV 时处理。 ## ARV 计算流程 ``` 运营 ARV = (调整后流动资产 + 调整后长期资产) - 自发性负债 股权 ARV = 运营 ARV + 超额现金 + 非核心投资 - 带息债务 - 环境/特定负债 ``` ## 检查清单(生成 ARV 前逐项确认) - [ ] 运营现金已从账面现金中剥离(超额现金单独列示) - [ ] 应收账款已加回坏账准备 - [ ] 存货已按 LIFO/FIFO 正确调整 - [ ] PP&E 使用的是 **Gross PP&E** 而非 Net PP&E - [ ] 商誉已赋值为 0 - [ ] R&D 和 SG&A 中的隐性无形资产已资本化加回 - [ ] 运营 ARV 中未扣除带息债务 - [ ] 股权 ARV 中已扣除带息债务 - [ ] ARV 展示了至少 3 年对比表
references/financial-analysis/greenwald-valuation.md# 格林沃尔德三要素估值法 **来源:** Bruce Greenwald, *Value Investing: From Graham to Buffett and Beyond* --- ## 1. 核心理念 **三要素独立与市价对比,不求和、不合并为单一内在价值。** ``` 要素① ARV(资产重置价值) ← vs 市价 → 一级安全边际(深度价值) 要素② EPV(收益能力价值) ← vs 市价 → 二级安全边际(收益覆盖价格) 要素③ 预期收益率 ← 仅当 EPV > ARV 时执行 → 判定市场定价是否合理 ``` --- ## 2. 要素①:ARV — 资产重置价值 ### 2.1 流动资产调整 | 科目 | 调整规则 | |------|---------| | **现金与等价物** | 运营现金 = 营收 × 1%;超额现金 = 账面现金 - 运营现金。超额现金在运营ARV中剥离,股权转换时再加回 | | **应收账款** | 调整后 = 账面A/R + 坏账准备(竞争对手需承担同等坏账成本) | | **存货** | LIFO: 账面+LIFO准备金;FIFO: 直接使用账面值 | ### 2.2 长期资产调整 | 科目 | 调整规则 | |------|---------| | **固定资产(PP&E)** | **严禁使用 Net PP&E。** 使用 Gross PP&E + 通胀调整 - 技术过时贬值 | | **商誉** | 强制赋值为 0 | | **隐性无形资产** | R&D资本化 + 渠道成本 + 培训成本。过去数年被费用化的部分加回 | ### 2.3 负债扣减 ``` 运营 ARV = (调整后流动资产 + 调整后长期资产) - 自发性负债(应付/预收/应付工资/应交税费) ⚠️ 运营ARV中严禁扣除带息债务 股权 ARV = 运营 ARV + 超额现金 + 非核心投资 - 带息债务 - 其他特定负债 ``` ### 2.4 报告格式 必须展示**3年对比表**,逐项资产列示。 --- ## 3. 要素②:EPV — 收益能力价值 ### 3.1 收益流清洗 | 步骤 | 操作 | |------|------| | 1 | **起点 = EBIT**。剔除所有利息收入与利息支出(否则后续加回现金时双重计算) | | 2 | **周期平滑**:平滑营收 × 5-7年平均营业利润率 | | 3 | **隐蔽利润加回**(零增长假设):SG&A中获客支出(通常25%) + R&D中新品支出 → 加回到利润 | | 4 | **NOPAT** = 正常化EBIT × (1 - 平均有效税率) | ### 3.2 CapEx 手术 EPV **仅扣除维持性 CapEx**: ``` 增长性 CapEx = 营收增长额 × (PP&E/营收平均比率) 维持性 CapEx = 总CapEx - 增长性CapEx 调整后收益 = NOPAT + 税后超额折旧 - 维持性CapEx ``` ### 3.3 EPV 终值 ``` 经营资产 EPV = 调整后收益 / WACC WACC 在8%-12%之间 股权 EPV = 经营资产EPV + 超额现金 + 非经营资产 - 有息债务 - 少数股东权益 ``` ### 3.4 报告格式 必须使用 **3年加权平均 NOPAT**(周期行业5:3:2,稳定消费4:3:3),展示 **WACC敏感性表**。 --- ## 4. 要素③:护城河验证与预期收益率 ### 4.1 护城河交叉验证 | EPV vs ARV | 判定 | 动作 | |------------|------|------| | EPV < ARV | 价值毁灭 | 终止买入,进入做空/清算分析 | | EPV ≈ ARV | 效率竞争,无护城河 | 仅当市值 < EPV 时买入 | | EPV > ARV | **存在特许权(Franchise)** | 启动预期收益率计算 | ### 4.2 预期收益率模型 ``` 基准预期回报率 = 现金收益回报 + 内生增长回报 + 主动再投资回报 - 特许权衰减 现金收益回报 = (正常化盈余 / 市价) × 派息率 内生增长回报 = 历史长期营收增速(上限 GDP+3%) 主动再投资回报 = (留存收益/市价) × 价值因子(ROIC>WACC?1:0) 特许权衰减 = 72 / 护城河半衰年数 ``` --- ## 5. 决策树 ``` ① 市值 < 股权ARV? → 一级安全边际(深度价值) ② ELSE IF 市值 < 股权EPV and EPV >= ARV? → 二级安全边际 ③ ELSE IF EPV > ARV and 预期回报率 > WACC+衰减? → 三级安全边际 ④ ELSE → 无安全边际 / 价值陷阱 ```
references/financial-analysis/mckinsey-value-driver-model.md# 麦肯锡价值驱动模型(McKinsey Value Driver Model)
**来源:** McKinsey & Co., *Valuation: Measuring and Managing the Value of Companies*, 第6版/第7版
**重要声明:这是麦肯锡(McKinsey)的估值模型,与格林沃尔德框架无关。** 本模型作为三模型交叉验证中的模型B,与格林沃尔德三要素(模型A)和反向DCF(模型C)并列使用。
---
## 1. 核心哲学
如果说格林沃尔德的三要素法探讨的是资产重置与当期盈利的**静态护城河**,那么麦肯锡的价值驱动模型探讨的则是**动态增长中的价值创造**。
**三个驱动因素硬性绑定:**
麦肯锡模型通过公式将 **增长(g)**、**盈利质量(ROIC)** 和 **现金流(FCF)** 这三个要素强制关联在一起:
$$FCF = NOPLAT \times (1 - \frac{g}{ROIC})$$
如果你假设了一个极高的 \(g\) 而企业的 \(ROIC\) 很平庸,公式会自动推高再投资率(\(\frac{g}{ROIC}\))——甚至使其大于 1——从而立即将当期的自由现金流吸干(变为负数),直接导致估值下降。这种严密的数学制衡,强迫分析师必须回归商业常识去评估企业护城河。
---
## 2. 财务报表重构逻辑(Reorganization)
传统会计准则混合了经营活动和融资活动,麦肯锡模型的第一步必须是"剥离融资影响,纯粹看经营"。
### 2.1 利润表重构:计算 NOPLAT
NOPLAT 衡量的是公司在没有任何债务负担下,核心业务产生的税后现金利润。
| 步骤 | 操作 |
|------|------|
| 1 | 从 **EBIT(息税前利润)** 出发 |
| 2 | 减去 **经营活动产生的实际现金税款**(调整递延所得税) |
| 3 | 剔除所有非经营性损益(投资收益、营业外收支) |
$$NOPLAT = EBIT \times (1 - \text{实际现金税率})$$
### 2.2 资产负债表重构:计算 Invested Capital(投入资本)
这是企业为了产生上述 NOPLAT,所实际"占用"在业务中的所有资金。
**计算路径(资产侧法):**
| 科目 | 说明 |
|------|------|
| 经营性营运资金 (OWC) | = (流动资产 − 溢余现金) − (无息流动负债) |
| 净固定资产 (Net PPE) | = 固定资产原值 − 累计折旧 |
| 其他经营性净资产 | 含资本化的无形资产等 |
$$Invested\;Capital = OWC + Net\;PPE + \text{其他经营性资产}$$
> **注:** 商誉通常不计入增量 ROIC 的投入资本中,但评估历史全口径 ROIC 时需考虑。
---
## 3. 核心驱动力测算步骤
### 步骤 1:测算真实资本回报率 (ROIC)
这是决定企业能否创造价值的"生死线"。
$$ROIC = \frac{NOPLAT}{Invested\;Capital}$$
**逻辑:** ROIC 必须与 WACC 进行对比。如果 \(ROIC < WACC\),企业就是在以高于回报的成本筹集资金,业务扩张越快,价值毁灭越严重。
### 步骤 2:测算再投资率 (Investment Rate, IR)
企业要增长就必须投钱(买设备、垫资)。增长率越高,或者 ROIC 越低,需要的再投资比例就越大。
$$IR = \frac{\text{当年净投入资本}}{NOPLAT} = \frac{g}{ROIC}$$
### 步骤 3:推导自由现金流 (Free Cash Flow, FCF)
麦肯锡模型中的 FCF 不是财务报表上的经营活动现金流,而是从 NOPLAT 中扣除再投资后的剩余。
$$FCF = NOPLAT - \text{净投入资本} = NOPLAT \times (1 - IR)$$
---
## 4. 估值与折现(两阶段模型)
在实际的财务建模中,通常采用两阶段折现法。
### 第一阶段:显性预测期(通常 5-10 年)
不直接预测未来的固定资产或存货,而是基于战略分析,设定未来的 \(ROIC\) 和 \(g\):
1. 基于历史趋势和行业竞争格局,设定每一年的 \(ROIC\) 目标和增长率 \(g\)
2. 通过 \(IR = g / ROIC\) 计算出每年的再投资率
3. 得出每年的 \(FCF_t = NOPLAT_t \times (1 - IR_t)\)
4. 将每年的 \(FCF_t\) 用 \(WACC\) 折现至当期现值
### 第二阶段:计算终值 (Terminal Value)
当企业进入成熟期,竞争优势收敛,假设其进入永续增长状态。此时调用核心价值公式:
$$TV_n = \frac{NOPLAT_{n+1} \times (1 - \frac{g_{\text{永续}}}{ROIC_{\text{增量}}})}{WACC - g_{\text{永续}}}$$
将终值 \(TV_n\) 折现到现在的价值。
### 最终加总
```
核心业务价值 = 显性期 FCF 现值之和 + 终值现值
企业总价值 (EV) = 核心业务价值 + 非经营性资产(如账面多余的现金、未并表的股权投资)
股权价值 = EV - 计息债务总额
```
### 示例:两阶段加总示意
```
n=1 n=2 n=3 n=4 n=5 TV(永续)
FCF FCF₁ FCF₂ FCF₃ FCF₄ FCF₅ TV₅
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│现值1│ │现值2│ │现值3│ │现值4│ │现值5│ │TV现值│
└─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │ │ │
└───────┴───────┴───────┴───────┴───────┘
│
▼
核心业务价值
+
非经营性资产(超额现金等)
=
企业价值 (EV)
-
有息债务
=
股权价值
```
---
## 5. 简化版(作为交叉验证用途)
在投资备忘录的三模型交叉验证场景中,由于时间和数据限制,通常使用 **永续增长简化版**:
$$Value = \frac{NOPLAT \times (1 - \frac{g}{ROIC})}{WACC - g}$$
### 简化版计算步骤
```python
# 步骤1:从财报提取 NOPLAT
NOPLAT = EBIT × (1 - 实际现金税率)
# 步骤2:计算 ROIC
ROIC = NOPLAT / Invested Capital
# 步骤3:设定参数
g = 保守估计的长期增长率(通常 2-5%,不超过名义GDP增速)
WACC = 加权平均资本成本(与格林沃尔德一致)
# 步骤4:计算再投资率
IR = g / ROIC
# 步骤5:价值计算
# 公式检验:若 IR > 1(即 g > ROIC),则 FCF 为负 → 模型失效
# 此时需检查 g 是否过高或 ROIC 是否过低
if IR >= 1:
# 模型失效:增长消耗超过盈利,公司无法产生正FCF
# 改用零增长假设(g=0),退化为类似EPV的框架
Value = NOPLAT / WACC
else:
Value = NOPLAT × (1 - IR) / (WACC - g)
# = NOPLAT × (1 - g/ROIC) / (WACC - g)
# 步骤6:加回非核心资产
EV = Value + 超额现金 + 非经营性投资
Equity = EV - 有息债务
PerShare = Equity / 流通股
```
### 参数设定指南
| 参数 | 说明 | 取值建议 |
|------|------|---------|
| NOPLAT | 正常化税后经营利润 | 同格林沃尔德调整后NOPAT,保持一致 |
| ROIC | 投入资本回报率 | 3-5年平均,避免单年偏差 |
| WACC | 加权平均资本成本 | 与格林沃尔德一致,8-12% |
| g | 长期可持续增长率 | ≤ 名义GDP增速(3-5%);若历史增速更高需逐年衰减至该水平 |
| IR | 再投资率 | 必须 < 1,否则增长在销毁价值 |
### 护城河强度与终值g对照
| 强度 | 特征 | g(永续) |
|------|------|---------|
| 极强 | 国家级垄断、无可替代品牌 | 4-5% |
| 强 | 双寡头、高转换成本 | 3-4% |
| 中等 | 品牌忠诚但可替代 | 2-3% |
| 弱 | 浅护城河 | 1-2% |
| 无 | 无定价权 | 不适用,模型B跳过 |
---
## 6. 模型验证:自我纠错机制
麦肯锡模型内置了一个强大的自我纠错机制:
| 情景 | 结果 | 判定 |
|------|------|------|
| \(IR > 1\)(即 \(g > ROIC\)) | FCF 为负,估值极低 | ❌ 增长不可持续,假设不合理 |
| \(WACC - g < 0\) | 终值发散到无穷 | ❌ 永续增长率必须 < WACC |
| \(ROIC < WACC\) 但 g 为正 | 增长在销毁价值 | ❌ 模型B给出极低估值,与模型A的EPV交叉验证 |
| \(ROIC \gg WACC\) 且 g 合理 | FCF 充盈,估值高 | ✅ 高质量护城河企业特征 |
> **实战警告:** 传统的 DCF 预测很容易陷入"拍脑袋做账"的误区——分析师往往给出了很高的收入增长预测,却忘记了在资产负债表中相应地增加资本开支和营运资金,从而得出了一个虚高、不合逻辑的现金流和估值。
---
## 7. 与格林沃尔德三要素的关系
### 分工明确
| 维度 | 格林沃尔德(模型A) | 麦肯锡(模型B) |
|------|-----------------|----------------|
| 核心问题 | "现在值多少?" | "增长值多少?" |
| 增长假设 | 零增长(EPV) | 显式增长(g) |
| ROIC作用 | 仅用于护城河验证 | 核心驱动因素 |
| 适用场景 | 所有公司 | 仅护城河企业(ROIC > WACC) |
### 在报告中的呈现
模型B在投资备忘录中与模型A**并列展示**,不求和、不合并。模型B给出的价值作为一个**含增长假设的参考值**,与格林沃尔德的无增长基准值进行对比,帮助投资者判断"增长的质量"。
references/financial-analysis/reverse-dcf.md# 反向 DCF(Reverse DCF)模型详解与实操
**来源:** DCF 框架的逆向应用,用户实战方法论。
**版本:** 2.0 — 2026-05-14
---
## 1. 核心逻辑
**不计算「股票值多少钱」,而是计算「市场认为这个公司值多少钱背后的假设」。**
传统 DCF:输入假设 → 算出价值
反向 DCF:输入当前股价 → 算出隐含的增长率
将当前股价视为 DCF 公式中的已知数,反推市场隐含的关键假设(通常是高增长期增速 g1),然后与历史实际数据对比,判断市场定价是否合理。
---
## 2. 为什么需要反向 DCF?(三大痛点)
1. **避免「垃圾进,垃圾出」(GIGO)**:传统 DCF 对增长率、折现率极其敏感,稍微修改参数,估值天差地别。
2. **克服认知偏差**:分析师很容易对喜欢的公司过度乐观,对不喜欢的公司过度悲观。
3. **洞察市场情绪**:快速判断当前市场是理性的,还是陷入了疯狂或恐慌。
---
## 3. 两段式核心计算公式
反向 DCF 将未来分为两个阶段:**第 1-N 年的高增速阶段(隐含增长率 g)**,以及**第 N 年之后的永续增长阶段(永续增长率 gn)**。
```
当前企业价值 (EV) = Σ(t=1→N) [FCF₀ × (1+g)ᵗ / (1+WACC)ᵗ]
+ [FCF₀ × (1+g)ᴺ × (1+gn) / (WACC - gn)] / (1+WACC)ᴺ
```
**已知量:** 当前 EV、基期 FCF₀、WACC、永续增长率 gn、预测期 N(通常 5 年)
**求解量:** 隐含增长率 g
---
## 3a. 参数预估方法论(关键)
反向 DCF 的输出质量取决于参数设定的透明度。**以下四个参数必须逐一说明预估依据,不得直接代入数字。**
### (1) WACC(加权平均资本成本)
WACC 必须与格林沃尔德 EPV 计算中的 WACC 保持一致,且需说明:
```
WACC = E/(E+D) × Ke + D/(E+D) × Kd × (1-t)
Ke = Rf + β × ERP
参数来源:
- Rf (无风险利率): FRED 10年期国债收益率 (中国: ~2.5%, 美国: ~4.3%)
- β (贝塔): 取自 yfinance 5年周线贝塔 (或可比公司β)
- ERP (股权风险溢价): 成熟市场 5-6%, A股 6-7%
- Kd (税后债务成本): 公司最近一期发债利率 × (1-有效税率)
- 资本结构: 按市值计算的目标资本结构
```
**示例(中国移动 600941):**
```
Rf = 2.5% (中国10Y国债)
β = 0.60 (电信运营商, 防御性)
ERP = 6.0%
Ke = 2.5% + 0.60 × 6.0% = 6.10%
Kd = 3.5% (央企发债利率约 3.0-4.0%)
t = 18% (有效税率)
E/(E+D) = 93.3% (权益占比)
D/(E+D) = 6.7% (债务占比)
WACC = 93.3% × 6.10% + 6.7% × 3.5% × (1-18%)
= 5.88%
```
### (2) g_terminal(永续增长率)
**永远不能超过名义GDP增速。** 标准取值:
| 场景 | 参考值 | 适用条件 |
|------|-------|---------|
| 发达国家成熟企业 | 2-3% | 稳定行业龙头 |
| 中国成熟企业 | 3-4% | 受益于GDP增速略高 |
| 衰退行业 | 0-1% | 营收零增长或下滑 |
| 高增长/未验证 | 不适用 | FCF 为负时反向 DCF 无效 |
### (3) g 的试算范围与搜索边界
二分法搜索范围必须根据行业特性设定:
| 行业类型 | 合理搜索范围 | 理由 |
|---------|------------|------|
| 成熟蓝筹/公共事业 | -10% ~ +10% | FCF 波动有限 |
| 消费/科技龙头 | -5% ~ +20% | 有一定增长空间 |
| 高增长科技 | 0% ~ +30% | 市场可能定价高增速 |
| 衰退/困境 | -30% ~ +5% | FCF 可能持续收缩 |
**搜索边界选择的逻辑:** 先跑一轮试算表(如 -6%, -4%, -2%, 0%, +2%...),观察每股价值是否跨越当前股价区间。如果所有试算值都高于/低于市价,说明基期 FCF 或 WACC 设定有问题,应返回检查参数,而非强行扩大搜索范围。
### (4) FCF₀(基期自由现金流)选择
| 选择方式 | 适用场景 | 风险 |
|---------|---------|------|
| 最近一年 FCF | 业务稳定、FCF 波动小 | 单年异常值会扭曲结果 |
| 3年加权平均 FCF | FCF 有周期性波动 | 平滑过度可能掩盖趋势变化 |
| 正常化 FCF | 有明显一次性项目 | 依赖分析师判断 |
**必须同时展示"基期 FCF"和"正常化 FCF"两套结果**,并说明选择理由。当两套结果互相矛盾时(如中国移动案例:2025年FCF¥759亿 → 隐含-2.8%,正常化FCF¥1,178亿 → 隐含-11.6%),意味着基期FCF存在一次性扰动,应以正常化值为辅参判读。
---
## 4. Python 自动化求解(推荐)
使用 scipy 的 root_scalar 自动反推隐含增长率:
```python
from scipy.optimize import root_scalar
def reverse_dcf_objective(g, ev, fcf0, wacc, gn, n=5):
"""计算基于特定增长率 g 的 DCF 估值与当前实际 EV 的差值"""
pv_stage1 = 0
for t in range(1, n + 1):
fcf_t = fcf0 * ((1 + g) ** t)
pv_stage1 += fcf_t / ((1 + wacc) ** t)
# 终值 (Terminal Value)
fcf_n = fcf0 * ((1 + g) ** n)
tv = (fcf_n * (1 + gn)) / (wacc - gn)
pv_tv = tv / ((1 + wacc) ** n)
return (pv_stage1 + pv_tv) - ev
# 使用根寻找算法求解
result = root_scalar(
reverse_dcf_objective,
args=(current_ev, base_fcf, wacc_rate, perpetual_g, forecast_years),
bracket=[-0.5, 0.8] # 假设隐含增长率在 -50% 到 80% 之间
)
if result.converged:
print(f"隐含未来 {forecast_years} 年年化 FCF 增长率: {result.root * 100:.2f}%")
```
### CFO 分析中的推荐实现
当 `scipy` 不可用时,使用以下无依赖的反向 DCF:
```python
def reverse_dcf(g1, g_terminal=0.03, wacc=0.09, fcf0=100, shares=10,
excess_cash=0, debt=0):
"""计算给定增长率下的每股价值"""
pv = 0.0
fcf = fcf0
for yr in range(1, 6):
fcf *= (1 + g1)
pv += fcf / ((1 + wacc) ** yr)
tv = fcf * (1 + g_terminal) / (wacc - g_terminal)
pv_tv = tv / ((1 + wacc) ** 5)
ev = pv + pv_tv
eq = ev + excess_cash - debt
return eq / shares
# 试算不同增长率
for g1 in [-0.05, -0.02, 0.0, 0.02, 0.05, 0.08, 0.10]:
ps = reverse_dcf(g1, ...)
print(f"g1={g1*100:+.0f}% → 每股价值 ¥{ps:.2f}")
# 或使用二分法求解精确隐含增速
lo, hi = -0.20, 0.20
for _ in range(40):
mid = (lo + hi) / 2
if reverse_dcf(mid, ...) > current_price:
lo = mid
else:
hi = mid
implied_g = (lo + hi) / 2
```
---
## 5. 结果判读方法
| 对比 | 结论 |
|------|------|
| 隐含增速 ≈ 历史实际增速 | 市场定价合理 |
| 隐含增速 >> 历史实际增速 | 市场过度乐观(警惕泡沫) |
| 隐含增速 << 历史实际增速 | 市场过度悲观(可能存在机会) |
### 判读三维度
1. **合理性检验 (Sanity Check)**:计算出的增长率是否与公司历史、行业龙头水平相符?成熟行业出现 30% 隐含增长率 → 通常意味着高估。
2. **敏感性与市场假设**:反向 DCF 告诉你市场在「赌」什么。某科技股的股价,是否需要靠新业务暴增才能支撑这个增速?
3. **判断买卖时机**:如果隐含增长率很低(如 2%),且公司仍在健康成长,说明有较高安全边际。
---
## 6. 模型适用性与局限
| 维度 | 评估 |
|------|------|
| ✅ **最适用** | 现金流稳定、成熟、可预测的蓝筹股(消费巨头、公共事业) |
| ❌ **不适用** | 负现金流初创公司、强周期性行业、高债务或频繁并购的公司 |
| ✅ **优势** | 揭示市场预期,强迫思考商业本质,而非死扣具体估值数字 |
| ❌ **局限** | 依然依赖 WACC 的设定;对 FCF 负值的公司完全失效 |
### 在 CFO 分析中的角色
反向 DCF 是**三模型交叉验证的第三极**,定位为定性参考而非精确估值工具:
```
格林沃尔德(资产锚定) → 麦肯锡(增长价值) → 反向DCF(市场心态探测)
```
当 FCF 为负时,反向 DCF 不适用。此时改用**隐含 PE 反推法**:`市值 ÷ 远期 PE = 隐含净利润`,判断市场定价是否合理。
---
## 7. 投资备忘录中的标准输出格式
> 此格式为强制要求。所有投资备忘录中反向 DCF 一节(5f节)必须按以下结构呈现,不得缺漏。
### 标题
```
### 5f. 反向 DCF — 完整参数与计算过程
**方法论版本:** reverse-dcf.md v2.0
```
### Step 1: 参数预估依据(四项逐一说明)
**① WACC 估算** — 列出 Rf/β/ERP/Ke/Kd/资本结构,每项标注来源
```
Rf = X% ← 来源
β = X.XX ← 来源
ERP = X% ← 来源
Ke = X% + X.XX × X% = X%
Kd(税前) = X% ← 来源
t = X%
E/(E+D) = X%
D/(E+D) = X%
WACC = X%
```
**② 永续增长率 g_terminal** — 选定值 + 选择理由 + 两项约束校验
```
g_terminal = X%
选择理由: [一句话说明]
约束校验: g_terminal < WACC? ✅
g_terminal < 名义GDP增速? ✅
```
**③ FCF 基期选择** — 至少展示两套方案,说明选择理由
```
方案A (标准): FCF₀ = $XXX ← 最近一年实际FCF
方案B (正常化): FCF₀ = $XXX ← X年平均
理由: [说明为什么选这个基期, 一次性项目说明]
```
**④ 预测期与试算范围** — 按行业分类表选择
```
N = 5年
g 搜索范围: X% ~ Y%
行业类型: [成熟蓝筹/消费科技/高增长/衰退]
```
### Step 2: 输入参数汇总表
| 参数 | 方案A | 方案B |
|------|------|-------|
| FCF₀ | $XXX | $XXX |
| WACC | X.X% | X.X% |
| g_terminal | X% | X% |
| N | 5年 | 5年 |
| 超额现金 | $XX | $XX |
| 有息债务 | $XX | $XX |
| 流通股 | XXM | XXM |
### Step 3: 方案A — 标准FCF基期
**核心公式:**
```
EV = Σ(t=1→5) [FCF₀ × (1+g)ᵗ / (1+WACC)ᵗ]
+ [FCF₀ × (1+g)⁵ × (1+g_terminal) / (WACC - g_terminal)] / (1+WACC)⁵
每股价值 = (EV + 超额现金 - 有息债务) / 流通股
```
**试算表(至少 7 档增长率,含 FCF₁/FCF₅/预测期PV/终值PV/EV/每股价值/vs市价):**
| g(年化) | FCF₁ | FCF₅ | 预测期PV | 终值PV | EV | 每股价值 | vs市价 |
|---------|------|------|---------|--------|----|---------|-------|
| -X% | ... | ... | ... | ... | ... | **$XX** | +X% |
| **0%** | **...** | **...** | **...** | **...** | **...** | **$XX** | **+X%** |
| +X% | ... | ... | ... | ... | ... | **$XX** | +X% |
**算法: 二分法求精确隐含增速**(展示 Python 代码或伪代码)
```python
def reverse_dcf(g1):
pv = 0; fcf = FCF₀
for yr in range(1, 6):
fcf *= (1 + g1)
pv += fcf / ((1 + WACC) ** yr)
tv = fcf * (1 + g_terminal) / (WACC - g_terminal)
pv_tv = tv / ((1 + WACC) ** 5)
ev = pv + pv_tv
return (ev + 超额现金 - 有息债务) / 流通股
lo, hi = 搜索范围
for _ in range(50):
mid = (lo + hi) / 2
if reverse_dcf(mid) > 市价: hi = mid
else: lo = mid
implied_g = (lo + hi) / 2
```
**结果: 隐含FCF增速 ≈ X%**(加一句话解释含义)
### Step 4: 方案B — 正常化FCF(如有)
同样格式的试算表 + 隐含增速。说明两套方案的矛盾及解读。
### Step 5: 方案C — 隐含PE反推法(FCF为负时的备用)
Forward PE、隐含净利润、与实际利润对比。
### Step 6: 判读结论
**历史参考:**
```
营收CAGR(3年): +X%
FCF CAGR(3年): +X%
核心净利润: $XX (调整后)
```
**三方案矛盾分析:** 对比方案A/B隐含增速趋势,判断基期FCF是否含一次性扰动。
**最终判读:** 与 EPV/麦肯锡结论交叉验证,给出市场定价是否合理的综合判断。
references/financial-analysis/telco-analysis-pitfalls.md# 电信运营商分析特化规则 > 适用场景:分析中国移动、中国电信、中国联通等电信运营商时适用。 > 创建于:2026-05-13,基于中国移动(600941)实战分析经验。 ## 1. 维护性 CapEx 假设 — EPV 最脆弱的假设 运营商是资本密集型企业(CapEx/营收 15-25%),EPV 对维护性 CapEx 占比**极度敏感**。 ### 中国移动案例 | 假设 | 维护CapEx | EPV/股 | vs ¥97.89 | |------|----------|--------|-----------| | D&A > 总CapEx → 无需调整 | ¥0 (已覆盖) | ¥100.37 | +2.5% | | 维护CapEx = 总CapEx的65% | ¥1,020亿 | ¥65.04 | -33.6% | | 维护CapEx = D&A×1.3 | ¥2,280亿 | ¥56.14 | -42.6% | > ⚠️ EPV 的核心脆弱点:**当 D&A 与 CapEx 的对比关系模糊时,EPV 结果可以在合理范围内上下摆动 40%+。** 不要只给一个 EPV 数字——必须展示敏感性范围。 ### 电信运营商维护CapEx估算规则 - **黄金标准:** 查阅港股年报的"折旧与摊销"行(HK GAAP 单独披露) - **A 股参考:** A 股利润表的"折旧摊销"通常仅含无形资产摊销,**严重低估**真实 D&A - **经验法则:** 成熟期运营商维护CapEx ≈ D&A × 1.0-1.2(因设备老化维护成本上升) - **建设期例外:** 4G→5G→6G 建设高峰年,增长CapEx占比可达 40-50% - **信号检测:** 当 D&A(港股) < 总CapEx 时,超出部分为增长CapEx ## 2. D&A 数据源选择 — 永远不要只信 A 股 ### A+H 双地上市场景 | 场景 | 正确做法 | |------|---------| | 仅有 A 股数据 | 从资产负债表推算:期初PP&E原值 + CapEx - 期末PP&E原值 = 年折旧(近似) | | A+H 均有数据 | **优先使用港股披露的"折旧与摊销"行**(完整,含固定资产+无形资产) | | 仅有 H 股数据 | 直接用,HK GAAP 披露更完整 | **从 A 股推算折旧的 Python 代码:** ```python # 从固定资产原值变动推算 pp_begin = 22946 # 上年末原值 pp_end = 23617 # 本年末原值 capex = 1570 # 购建固定/无形资产支付的现金 disposals = 0 # 处置,可从附注获取 depreciation = pp_begin + capex - disposals - pp_end ``` ## 3. 现金流分析特殊注意事项 运营商 OCF 波动大,不能单看一年 FCF: - **FCF 波动原因:** 税费支付节奏(大额集中支付)、应收账款季节性 - **正常化 FCF:** 取 3-5 年滚动平均更可靠 - **信号规律:** OCF 持续下降 + 应收账款持续上升 = 真实恶化信号 - **反向 DCF 陷阱:** 若以异常低 FCF 年份为基准,反向 DCF 会高估市场悲观程度 ## 4. 6G 投资周期预警 - 预计 2028-2030 年启动 6G 建设期 - CapEx 可能再次跳升 30-50%(参考 4G→5G 周期) - 这将显著压低 FCF 和 ROIC - 估值时必须标注"CapEx 周期性回升风险" ## 5. 运营商特有的估值指标 标准框架外,电信运营商应额外关注: | 指标 | 说明 | 健康范围 | |------|------|---------| | CapEx/营收 | 资本开支强度 | <20%(成熟期) | | FCF/净利润 | 现金转化率 | >80% | | EV/EBITDA | 企业价值倍数 | <6x(价值区域) | | 分红支付率 | 利润用于分红比例 | 60-80%(可持续) | | 应收账款/营收 | 回款周期指示 | <8% | ## 6. 与标准 Greenwald 框架的偏差 运营商的分析偏差与 SaaS 企业**方向相反**: | 偏差 | SaaS 企业 | 电信运营商 | |------|----------|-----------| | ARV | 严重低估(无形资产不入表) | 基本合理或略高估(回收率争议大) | | EPV | 高增长导致权重争议 | 维护CapEx假设导致大幅波动 | | FV | 高增长价值是核心 | 增长价值有限(低增长行业) |
references/financial-data-acquisition/adr-currency-pitfalls.md# ADR 币种陷阱 — 当财务数据与股价在不同货币时
## 问题描述
中国 ADR 股票(如 BZ/ATHM/LI 等)在 yfinance 上的一个系统性问题:
| 数据 | yfinance 返回的币种 | 原因 |
|------|------------------|------|
| 利润表/资产负债表/现金流量表 | **CNY** (人民币) | 公司的 functional currency 是 CNY |
| 股价 (`info["currentPrice"]`) | **USD** (美元) | 股票在 NASDAQ/NYSE 以 USD 交易 |
| 市值 (`info["marketCap"]`) | **USD** | 同上 |
**所有分析脚本**(ARV、压力测试、ROIC-WACC、利润率分解等)都从 `yf.Ticker("XXX").info["currentPrice"]` 读取股价,得到的是 **USD**。但它们直接把这个值当作 **CNY**(¥)显示和计算,导致:
- ARV/ADS = ¥52(CNY) vs 股价 = $13.65(显示为 ¥13.65)→ **错!** 应该 ¥99.6
- 每股净现金 ¥50.3 vs 股价 $13.65 → 比较的是不同币种
## 对所有 ADR 的影响
| 脚本 | 影响 | 严重度 |
|------|------|--------|
| calculate_reproduction_value.py | 股价/ARV比低估 ~7.3x | 🔴 高 |
| stress_test_margin_of_safety.py | 安全边际虚高 | 🔴 高 |
| analyze_roic_wacc_spread.py | 使用 mkt_cap(USD) 与 CNY 报表混合 | 🟡 中 |
| audit_capital_allocation.py | 净现金/市值比率在 USD 下显示 | 🟡 中 (已在 fetch 中修复) |
## 修正方法
### 方法 A:在分析/报告生成时统一换算(推荐)
```python
rate = 7.3 # USD/CNY 汇率
price_cny = price_usd * rate # $13.65 → ¥99.6
mcap_cny = mcap_usd * rate # $6.2B → ¥452.6亿
```
全部计算使用 **CNY** 统一口径:
- 净现金/市值(CNY)= ¥197.9亿 / ¥452.6亿 = 43.7% ✅
- PE = ¥452.6亿 / ¥27.4亿 = 16.5x ✅
- EV = ¥452.6亿 - ¥197.9亿 = ¥254.7亿 ✅
### 方法 B:将 CNY 财务数据转为 USD(不推荐)
仅在需要与美股同行直接对比时使用。
### 汇率参考
当前 USD/CNY ≈ **7.3**(2026年5月)。建议每次分析时从 FRED 或 yfinance 获取最新汇率。
```python
# 从 yfinance 获取 USDCNY=X 汇率
import yfinance as yf
fx = yf.Ticker("USDCNY=X")
rate = fx.info.get("regularMarketPrice", 7.3)
```
## 受影响的中国 ADR 清单
| 代码 | 公司 | 交易所 | functional currency |
|------|------|--------|-------------------|
| BZ | BOSS直聘 | NASDAQ | CNY |
| LI | 理想汽车 | NASDAQ | CNY |
| ATHM | 汽车之家 | NYSE | CNY |
| BABA | 阿里巴巴 | NYSE | CNY |
| JD | 京东 | NASDAQ | CNY |
| NIO | 蔚来 | NYSE | CNY |
| XPEV | 小鹏 | NYSE | CNY |
| TAL | 好未来 | NYSE | CNY |
| EDU | 新东方 | NYSE | CNY |
**通用规律:** 所有注册在开曼/香港、运营在中国大陆、以 CNY 记账的 ADR 都适用此规则。
## 快速检查方法
观察交叉校验的 **C2** 输出:
- 如果净现金/市值 > 80% → 极大概率是币种不匹配
- 修复:将市值乘以 7.3(或当前汇率)得到 CNY 版本后再判断
references/financial-data-acquisition/baostock-guide.md# Baostock 使用指南
## 安装
```bash
pip install baostock
```
无需注册,无需 API Key。登录调用 `bs.login()` 返回 "login success!" 即成功。
## 已验证可用的函数
### 分红数据(已集成到 fetch_financials.py)
```python
import baostock as bs
lg = bs.login()
rs = bs.query_dividend_data(code="sh.601398", year="2024")
df = rs.get_data()
# 关键列:
# dividCashPsBeforeTax — 税前每股现金分红
# dividOperateDate — 除权日
# dividPlanAnnounceDate — 宣告日
# dividCashStock — 描述,如 "10派3.064元"
```
### 指数成分股
```python
rs = bs.query_hs300_stocks() # 沪深300
rs = bs.query_zz500_stocks() # 中证500
rs = bs.query_sz50_stocks() # 上证50
df = rs.get_data()
# 列: code (如 "sh.601398"), code_name (如 "工商银行")
# 转 yfinance 兼容:
# code.startswith("sh.") → code[3:] + ".SS"
# code.startswith("sz.") → code[3:] + ".SZ"
```
### 股票基本信息
```python
rs = bs.query_stock_basic(code="sh.601398")
df = rs.get_data()
# 列: code, code_name, ipoDate, status
```
### 行业分类
```python
rs = bs.query_stock_industry()
df = rs.get_data()
# 列: code, industryName, industryClassification
# 注意:实测 industryName 可能为空,需要排查列名映射
```
## 注意事项
- `bs.login()` 和 `bs.logout()` 必须成对调用
- q 每次 login 后获取的数据是独立的(断开重连会丢失会话)
- 所有财务指标函数(query_profit_data 等)返回的是**财务比率**,不是详细的财务报表科目
- 港股/美股不支持
references/financial-data-acquisition/baostock-quarterly-timing.md# Baostock 最新季度数据获取指南
## 背景
yfinance 对 A 股季度数据的更新通常有 1-3 个月的延迟。Baostock 的 `query_profit_data()` 可以在 yfinance 尚未更新时提供最新的季度的净利润、净利率、EPS_TTM 和 ROE。
## 已验证的用例(2026年5月1日)
yfinance 季度数据截止到 2025-12-31,而 Baostock 已提供 2026 Q1 数据。
```python
import baostock as bs
lg = bs.login()
rs = bs.query_profit_data(code='sh.605499', year='2026', quarter=1)
df = rs.get_data()
if not df.empty:
row = df.iloc[0]
print(f"营收(MBRevenue): {row.get('MBRevenue', 'N/A')}")
print(f"净利(netProfit): {row.get('netProfit', 'N/A')}")
print(f"净利率(npMargin): {row.get('npMargin', 'N/A')}")
print(f"EPS_TTM: {row.get('epsTTM', 'N/A')}")
print(f"ROE平均: {row.get('roeAvg', 'N/A')}")
bs.logout()
```
## 已知限制
| 字段 | Q1 | Q2 | Q3 | Q4 |
|------|-----|-----|-----|-----|
| netProfit(净利润) | ✅ | ✅ | ✅ | ✅ |
| MBRevenue(营收) | ⚠️ 常为空 | ✅ H1累计 | ⚠️ 常为空 | ✅ 全年 |
| npMargin(净利率) | ✅ | ✅ | ✅ | ✅ |
| epsTTM | ✅ | ✅ | ✅ | ✅ |
| roeAvg | ✅ | ✅ | ✅ | ✅ |
**注意:** MBRevenue 在 Q1 和 Q3 经常为空。营收需通过其他方式推算(如 netProfit / npMargin)。
## 在 fetch_financials.py 中的调用位置
`enrich_with_baostock(data)` 函数会在 yfinance 数据采集后自动运行(仅对 .SS/.SZ 后缀的 A 股 ticker)。
当前仅获取分红数据和基本信息。最新季度数据需要手动调用——`fetch_financials.py` 尚未将该逻辑集成到自动流水线中(future improvement)。
references/financial-data-acquisition/finnhub-guide.md# Finnhub API 使用指南
## 注册 & API Key
- 注册:https://finnhub.io/register(邮箱 + 密码)
- 免费 tier:60 API calls/分钟
- Key 存入 `config/api_keys.json`
- 安装:`pip install finnhub-python`
## 集成方式
在 `fetch_financials.py` 中自动执行 `enrich_with_finnhub(data)`,结果写入 `data["finnhub"]`。
## 数据内容
### latest_earnings — 最新财报摘要
```python
{
"date": "2026-04-30",
"eps_actual": 2.99,
"eps_estimate": 2.83,
"revenue_actual": 921000000,
"revenue_estimate": 919000000
}
```
### earnings_transcript — Earnings Call 完整记录
```python
"earnings_transcript": "Good afternoon, everyone...(前5000字)"
```
- 部分公司可能没有免费 tier 的 Transcript 数据
### recent_news — 近90天内新闻(前5条)
```python
[{"source": "Yahoo", "headline": "Crocs beats Q1 expectations...", "url": "...", "date": 1745964000}]
```
### insider_transactions — 内部人交易(前5条)
```python
[{"name": "Andrew Rees", "change": -6687, "share": 6687, "transaction_price": 79.63}]
```
## 已知限制
- 免费 tier 可能无法获取所有公司的 Transcript
- 每只股票约消耗 2-4 次 API 调用
- 免费 tier 60次/分钟 ≈ 单次分析 15-30 只股票
references/financial-data-acquisition/industry-data-pitfalls.md# 行业数据陷阱 — 财报数据获取中的已知问题 本文件记录从 yfinance 及其他数据源获取特定行业财务数据时已知的陷阱和调整规则。 ## 银行(A 股 + 美股) ### FCF 完全不可使用 **问题:** 银行的"经营现金流"(Operating Cash Flow)主要是存款流入与贷款流出的轧差额,与工业企业的"经营创造现金"概念完全不同。 **表现:** 兴业银行 2025 年经营现金流 ¥6537亿,2024 年却是 -¥2373亿。这不是经营恶化,而是存款波动的结果。 **规则:** 银行分析中**永远不使用 FCF**。银行的真实可分配利润按以下公式计算: ``` 真实可分配利润 = 净利润 × (1 - 最低资本留存率) 最低资本留存率 ≈ 30%(受监管要求约束) 股东收益率 ≈ (净利润 × 分红率) / 市值 ``` ### ROIC = ROE 银行不适用 `NOPAT / 投入资本` 的标准 ROIC 公式。银行的"投入资本"就是股东权益。银行的 ROIC 直接退化为 ROE: ``` ROE = 净利润 / 平均股东权益 ``` ### 资产质量关注点 银行的 PB<1 是常态(不是低估信号)。需要关注: - **不良贷款率 (NPL):** 官方数据可能低于真实值。关注类贷款 + 逾期贷款往往是 NPL 的 1.5-2x - **拨备覆盖率:** 越低说明资产质量缓冲越薄 - **核心一级资本充足率 (CET1):** 反映银行的资本缓冲 ## 水电站 / 公用事业(A 股) ### 低 ROIC 是常态而非问题 **问题:** 长江电力 ROIC=6.8%,看起来很低。 **解释:** 水电站是极端重资产。三峡大坝造价超过 ¥2000亿,这些资产已经建成并产生稳定现金流。分母(投入资本)巨大但不意味着生意差。**边际资本回报率是 100%**(多发一度电几乎全是利润)。 **规则:** 对水电站等已建成基础设施,ROIC 参考意义有限。关注: - **经营现金流稳定性(多年均值)** - **FCF / 营收比**(长江电力: 53.5%) - **债务偿还进度** ### 经营现金流包含重大一次性项目 水电站的"资本开支"包含常规维护(小)和重大更新(大)。需区分: - 维持性 Capex(正常年份 ¥5-10B) - 扩张性 Capex(新电站建设,¥50B+) ## A 股公司普遍缺失的数据 通过 yfinance 获取 A 股数据时以下字段几乎全为 N/A: | 字段 | 原因 | 替代方案 | |------|------|---------| | SBC(股权激励) | A 股股权激励以限制性股票为主,yfinance 不映射 | 从年报附注查找 | | 回购 | A 股回购极少,且不通过美股托管通道 | 从港交所/上交所公告查找 | | 分红 | A 股分红不通过 DTCC,yfinance 无数据 | 从 `info['dividendYield']` 反推 | | 应收/应付细节 | yfinance 资产负债表对中概/A股的科目映射不全 | 从年报中查找 | ## yfinance 股息率字段的格式差异 | 市场 | 格式 | 示例 | 说明 | |------|------|------|------| | 美股 | 小数 | `0.0038` 表示 0.38% | 直接可用 | | A 股 | 百分比值 | `4.06` 表示 4.06% | 需要除以 100 才与美股统一 | | 港股 | 不定 | 可能为小数或百分比 | 需检查后处理 | 脚本中的兼容代码: ```python div_pct = div * 100 if div < 1 else div ``` ## Web 搜索兜底的局限性 自动化浏览器搜索(Bing/Google/Baidu)经常会遇到: - Cloudflare CAPTCHA 验证 - 滑块验证码("请验证您是真人") - 访问频率限制 **策略:** 优先通过 yfinance 获取结构化数据。仅对必须但缺失的字段(如管理层指引、竞品数据)启动浏览器搜索。如果被 CAPTCHA 拦截,在报告中明确标记为"待补充"而非强行搜索。
references/financial-data-acquisition/mx-api-guide.md# MX API(东方财富妙想)配置与使用指南
> 2026-05-13 接入验证。适用于 A 股 + 港股数据采集。
## 申请方式
1. 打开微信扫码 `https://dl.dfcfs.com/m/itc4`
2. 或东方财富 APP → 搜索"妙想Skills" → 免费领取
3. 领取 API Key(格式:`mkt_xxxxx`)
## 配置
```bash
# 写入 profile 的 .env
echo 'MX_APIKEY=mkt_xxxxx' >> /home/ivan/github/my-skills-repo/data-science/.env.local
```
## 5 个 MX 技能清单
| 技能名称 | 功能 | 安装路径 | 脚本 |
|---------|------|---------|------|
| mx-data | 行情/财务/股东/关联数据查询 | `/home/ivan/github/my-skills-repo/mx-data/` | `mx_data.py` |
| mx-search | 新闻/公告/研报/政策搜索 | `/home/ivan/github/my-skills-repo/mx-search/` | `mx_search.py` |
| mx-xuangu | 按条件选股(PE/ROE/行业等) | `/home/ivan/github/my-skills-repo/mx-xuangu/` | `mx_xuangu.py` |
| mx-zixuan | 东方财富自选股管理 | `/home/ivan/github/my-skills-repo/mx-zixuan/` | `mx_zixuan.py` |
| mx-moni | 模拟组合(买卖/持仓) | `/home/ivan/github/my-skills-repo/mx-moni/` | `mx_moni.py` |
## 使用示例
### 查询个股行情+财务
```bash
cd /home/ivan/github/my-skills-repo/mx-data
MX_APIKEY=mkt_xxxxx python3 mx_data.py "东鹏饮料 最新价 总市值 PE PB ROE 营收增速"
```
### 搜索新闻
```bash
cd /home/ivan/github/my-skills-repo/mx-search
MX_APIKEY=mkt_xxxxx python3 mx_search.py "贵州茅台 2025年最新公告 产能扩建"
```
### 智能选股
```bash
cd /home/ivan/github/my-skills-repo/mx-xuangu
MX_APIKEY=mkt_xxxxx python3 mx_xuangu.py "筛选PE<20且ROE>15%的白酒行业股票"
```
## Python 接口(推荐在分析脚本中使用)
```python
from lib.mx_api import MXClient
client = MXClient() # 读取 MX_APIKEY 环境变量
# 快速查询
snapshot = client.fetch_snapshot("605499")
# → {"price": 192.83, "market_cap": "1115亿", "pe": 23.76, "pb": 5.32}
# 自然语言查询
data = client.query("605499 东鹏饮料 2025年营业收入 净利润 毛利率")
# → 解析 dataTableDTOList 获取表格数据
# 解析实体(股票名→代码)
hits = client.resolve_entity("东鹏饮料")
# → [{"fullName": "东鹏饮料", "secuCode": "605499", "entityType": "A股"}]
# 新闻搜索
news = client.news_search("东鹏饮料 最新公告")
```
## 输出文件说明
默认输出目录:`~/.mx/mx-data/output/`(已从旧 OpenClaw 硬编码路径修补)
每次查询生成:
- `mx_data_{query}.xlsx` — Excel 文件(多 sheet)
- `mx_data_{query}_description.txt` — 查询结果描述
- `mx_data_{query}_raw.json` — API 原始 JSON 响应
## 注意事项
1. **速率限制:** 每日免费调用次数有限(具体限额参见妙想页面)
2. **查询范围:** 不要一次查询太大数据(如"5年每日行情"),会导致返回内容过多
3. **参数名:** query 接口用 `{"toolQuery": "..."}` 而非 `{"query": "..."}`
4. **Hermes 集成:** `hermes skills install` 不支持本地路径,需要用 `cp -R` 手动安装
5. **Caching:** `lib.mx_api.py` 内置 30 分钟 TTL 缓存,通过 `lib.cache.cached` 实现
references/financial-data-acquisition/sec-edgar-guide.md# SEC EDGAR API 使用指南
## 概述
SEC EDGAR 提供所有美股上市公司的原始申报文件(10-K、10-Q、8-K 等)。
**完全免费**,无需注册,无需 API Key。
## 集成方式
在 `fetch_financials.py` 中自动执行 `enrich_with_sec(data)`,结果写入 `data["sec_edgar"]`。
## 数据获取链路
### 1. CIK 查询
```
GET https://www.sec.gov/files/company_tickers.json
```
返回所有上市公司 ticker → CIK 的完整映射。查找方式:
```python
for entry in mapping.values():
if entry["ticker"] == "CROX":
cik = entry["cik_str"] # 例: 1334036
```
### 2. 获取最近 10-K 提交信息
```
GET https://data.sec.gov/submissions/CIK{cik_padded_to_10}.json
```
解析 `filings.recent` 列表,找到 form="10-K" 的最新一条。提取:
- `accessionNumber`(档案号)
- `primaryDocument`(主文件名,通常是 HTML)
- `reportDate`(报告期)
### 3. 下载 10-K HTML
```
GET https://www.sec.gov/Archives/edgar/data/
{cik}/{accession_without_dashes}/{primary_document}
```
### 4. 提取章节
通过正则匹配提取:
- **Item 1 — Business**:公司业务描述
- **Item 1A — Risk Factors**:风险因素
- **Item 7 — MD&A**:管理层讨论与分析
- **Item 11 — Executive Compensation**:高管薪酬(通常引用至 Proxy Statement)
## 已知限制
- Executive Compensation 通常引用至 DEF 14A(Proxy Statement),需单独获取
- 10-K HTML 格式在各公司之间不统一,章节提取可能不完美
- 需要设置 User-Agent 才能访问 SEC 服务器
- SEC 有速率限制(约 10 次/秒),大部分场景不会触发
references/original-skills/buffett-review.md---
name: buffett_review
description: "Activates Warren Buffett's complete investment thinking system. Triggers on stock analysis, investment evaluation, financial report interpretation, moat/management assessment, buy/hold/sell decisions. NOT auto-generated alongside cfo-deep-dive — only on explicit user request like '用巴菲特视角分析', '巴菲特怎么看', '生成巴菲特报告'. 8-question quick filter, 5-moat framework, owner earnings valuation, margin-of-safety tiers. Reads reference files for deep analysis."
---
# Buffett Investment Thinking System
What you embody is the complete investment wisdom Warren Buffett accumulated over 70 years: Graham's margin of safety, Munger's quality premium, Berkshire's capital allocation philosophy, and the common sense and honesty that runs throughout.
Not applying formulas — thinking the way he actually thinks.
> **Read reference files with skill_view(name='buffett_review', file_path='references/<filename>')**\n> Construction: `skill_view(name='buffett_review', file_path='references/03-business-moat.md')`
> **Files must actually be read before analysis — do not rely on built-in knowledge as a substitute.**
---
## Quick Filter (2 minutes, 8 questions)
Run these 8 questions first. Two "No" answers require strong justification; four "No" answers means pass and move on to the next opportunity.
| # | Dimension | Question | No = Red Flag |
|---|-----|-----|------------|
| 1 | **Circle of Competence** | Can I explain in one paragraph how this business makes money? | Can't explain = outside circle of competence |
| 2 | **Durability** | Will this company still exist and be more competitive in 10 years? | No = technology/model disruption risk |
| 3 | **Moat** | Could a competitor replicate its core advantage with serious effort? | Yes = no moat |
| 4 | **Pricing Power** | Can it raise prices 5–10% without losing a significant share of customers? | No = commodity-type business |
| 5 | **Earnings Quality** | Does profit genuinely convert to cash (rather than accounting tricks)? | No = earnings quality problem |
| 6 | **Debt Safety** | In the industry's worst-case scenario (revenue −30%), can it survive? | No = leverage risk |
| 7 | **Management Integrity** | Does management honestly confront problems rather than hide them? | No = automatic veto |
| 8 | **Reasonable Price** | Is the gap between current price and intrinsic value large enough? | No = wait or skip |
> Integrity (Q7) is an automatic veto — no matter how good everything else looks.
---
## Reference File Reading Protocol
**Core principle: read on demand, do not read everything at once.** Decide which files to read based on task type.
### Task Type → Reading Path
**A · Quick Judgment** ("Is this worth deeper analysis?")
→ Use the 8-question filter directly — no need to read reference files. Pass the filter before proceeding to B.
**B · Full Company Deep Analysis** (standard path, execute in order)
```
Required (in order):
references/03-business-moat.md ← Moat / business model / goodwill
references/04-management-governance.md ← Management / culture / governance
references/05-financial-metrics.md ← Financial metrics / owner earnings
references/06-valuation-capital.md ← Valuation / margin of safety / capital allocation
Supplemental as needed:
references/08-industry-playbooks.md ← After identifying the industry, read the relevant chapter
references/07-risk-behavior.md ← When there are concerns about leverage / derivatives / value traps
```
**C · Specific Topics** (jump directly to the corresponding file)
| User is asking about… | Read |
|---------|---|
| Moat / brand / goodwill / business model type | `references/03-business-moat.md` |
| Management / integrity / institutional imperative / acquisition rationale | `references/04-management-governance.md` |
| Financial statements / ROIC / owner earnings / look-through earnings | `references/05-financial-metrics.md` |
| Valuation / margin of safety / buybacks / dividends / arbitrage / bonds | `references/06-valuation-capital.md` |
| **Hold / sell / whether to continue / trim position** | `references/07-risk-behavior.md` (required — four sell criteria) |
| When to sell / value traps / behavioral bias / leverage / inflation | `references/07-risk-behavior.md` |
| A specific industry (insurance / banking / consumer, etc.) | Relevant chapter in `references/08-industry-playbooks.md` |
| Investment philosophy / compounding / intrinsic value / concentration vs. diversification | `references/02-investment-philosophy.md` |
| Mental frameworks / circle of competence / inversion / Mr. Market | `references/01-thinking-frameworks.md` |
---
## Output File
File naming (same version as the CFO trio):
```
buffett_view_[股票代码]_[公司名]_[日期]_V[版本].md
```
Stored alongside the other reports at:
```
~/.hermes/skills/data-science/financial-screener/reports/
```
---
## Deep Analysis Framework (Path B expanded)
### 1 · Mental Positioning (do first — cannot skip)
> "Risk comes from not knowing what you are doing."
- **Circle of Competence**: Can I explain in one paragraph how this business makes money? Cannot → stop and explain why.
- **Inversion check**: In what ways could this investment lose me money? List the top three paths.
- **Time horizon**: Use a "10-year hold" perspective, not "will it go up this quarter?"
---
### 2 · Business Quality (read 03 + 04)
Five moat types: intangible assets, cost advantage, switching costs, network effects, efficient scale.
Key judgment: **trend** (widening / stable / narrowing) — not just current state.
Management: **Integrity** (automatic veto) → capital allocation ability → owner mentality
Watch for the institutional imperative — it causes excellent managers to make irrational decisions.
> "You can never make a good deal with a bad person, no matter how attractive the prospects."
---
### 3 · Financials & Valuation (read 05 + 06)
```
Owner Earnings = Net Income + D&A − Maintenance Capex − Working Capital Increase
```
ROIC 10-year average target >15%; cash conversion rate target >90%.
**Margin of Safety Tiers:**
| Certainty | Discount Required |
|------|--------|
| Very high (wide moat + predictable growth) | 20–30% |
| Generally excellent | 30–40% |
| Uncertainty factors present | 40–50% |
| Cannot reliably estimate | Do not invest |
**⚠️ Calculation Discipline (from 2026-05-03 BZ postmortem):**
All financial calculations — currency conversions, ratios, valuations — must be executed via Python `execute_code` or terminal commands. Mental arithmetic is prohibited. Every figure must carry its currency prefix (¥/$) and unit (亿/万). When a computed ratio exceeds reasonable bounds (e.g., net cash/market cap > 80%), pause and verify the currency consistency before proceeding.
---
### 4 · Risk (read 07 when concerns exist)
All three risk categories must be checked:
- **Structural**: moat narrowing, technological disruption, regulatory attack
- **Financial**: excessive leverage, cash flow fabrication, off-balance-sheet liabilities
- **Behavioral**: confirmation bias, sunk cost, institutional imperative
**Sell Criteria (four):** Price severely overvalued / Fundamental moat destruction / Management integrity issue (sell immediately) / A significantly better opportunity exists
---
### 5 · Industry (read the relevant chapter in 08)
After identifying the industry, jump directly to the corresponding chapter in `references/08-industry-playbooks.md`, which contains key metrics, historical case studies, and macro sensitivity analysis for that industry.
---
## Standard Output Format
**All sections are required outputs and cannot be omitted.** Quick judgment (Path A) may use one sentence per section; deep analysis (Path B) requires full expansion.
```
## Conclusion
[Buy / Don't Buy / Keep Watching / Hold / Sell] — one-sentence core rationale
## Circle of Competence Assessment ← required output, cannot skip
[State clearly: inside circle / outside circle / boundary area]
If outside circle: stop analysis and honestly explain why.
## Key Assumptions (3–5) ← required output, cannot skip
[Core assumptions the decision depends on — listed explicitly for later verification]
## Business Quality
- Moat: [type] + [strong/medium/weak] + [widening/stable/narrowing]
- Management: [integrity rating] / [capital allocation rating] / [owner mentality rating]
- Business model: [franchise / commodity / hybrid]
- Institutional imperative warning: [present / absent — state basis]
## Financial Snapshot
- ROIC (10-year average):
- Cash conversion rate:
- Debt safety (worst-case scenario test):
- Owner earnings estimate:
## Valuation
- Intrinsic value range:
- Current margin of safety: [%] (corresponding certainty level: high/medium/low)
- Recommended buy price:
## Sell Criteria — Item-by-Item Check ← required for hold/sell scenarios, each item explicitly judged
1. Price severely overvalued? [Yes/No + basis]
2. Fundamental moat destruction? [Yes/No + basis]
3. Management integrity issue? [Yes/No + basis; if "Yes," sell immediately]
4. Significantly better opportunity available? [Yes/No + basis]
## Key Risks (max 3)
[Focus on the most critical — do not list everything]
## Monitoring Indicators ← required output, cannot skip
- Check each quarter:
- Signals that trigger a sell:
## Overall Assessment
[From Buffett's perspective and in his tone — give the decision recommendation and rationale directly]
```
---
## Reference File Index
| File | Contents |
|-----|-----|
| `references/01-thinking-frameworks.md` | Circle of competence, inversion, Mr. Market, long-termism, Munger's multi-model framework, independent thinking, opportunity cost, patience |
| `references/02-investment-philosophy.md` | Intrinsic value, compounding, undervaluation, concentrated investing, rebuttal to efficient market theory, stance on market forecasting |
| `references/03-business-moat.md` | Five moat types, business model (franchise vs. commodity), economic goodwill, durability of competitive advantage |
| `references/04-management-governance.md` | Three-dimensional management assessment, institutional imperative, corporate culture, governance and shareholder orientation, acquisition criteria |
| `references/05-financial-metrics.md` | Owner earnings, ROIC/ROE, cash conversion rate, look-through earnings, balance sheet health |
| `references/06-valuation-capital.md` | Three methods for intrinsic value estimation, margin of safety, five capital allocation paths, buybacks, dividends/retained earnings/taxes, arbitrage/bonds/convertibles |
| `references/07-risk-behavior.md` | When to sell, value traps, leverage, inflation, derivatives, common behavioral biases |
| `references/08-industry-playbooks.md` | Insurance (including underwriting discipline/float), banking, consumer retail, media, energy, railroads, technology, cautionary tales (airlines/textiles) |
references/original-skills/critical-review.md--- name: critical_review description: "批判性审视报告 — 以CFO+巴菲特+价值投资的视角,对investment_memo和reasoning_trace进行对抗式审查,指出不合理假设、数据缺陷、逻辑漏洞、认知偏差,输出独立的风险揭示文档。审查范围包括:报告完整性(8个板块+三模型交叉验证)、数据质量、假设压力测试、估值逻辑、认知偏差。" version: 2.1.0 author: Hermes Agent --- # 批判性审视 — Critical Review ## 角色设定(系统前缀提示) 每次触发此 skill 时,必须在系统层面注入以下角色人格: > 你现在是一位拥有30年经验的CFO兼首席风险官,同时你深谙巴菲特和查理·芒格的思维模型。你的工作是**专门挑刺**。 > > 你的信条: > - "任何投资建议中,最危险的部分就是作者最有信心的那个结论。" > - "每一份分析报告都是由一系列假设堆砌而成的——找到那个最脆弱的假设,整个逻辑就会坍塌。" > - "管理层的笑脸不值钱,资产负债表上的数字才是真的。" > - "如果一份分析没有列出三个以上能推翻结论的理由,那它就不是分析,是宣传。" > > 你的任务是阅读 investment_memo 和 reasoning_trace,然后输出一份独立的 critical_review 文档。你要对原报告的每个核心结论提出质疑,指出: > 1. 哪些数据可能不可靠 > 2. 哪些假设过于乐观 > 3. 哪些逻辑推理有跳跃 > 4. 哪些风险被低估了 > 5. 作者的认知偏差可能是什么 > 6. 如果作者错了,最可能错在哪里 ## 触发条件 - 每次 financial-analysis 生成投资备忘录 + reasoning_review 生成思考链后,**自动执行** - 用户明确要求 "批判性审视" 或 "挑刺" 或 "唱反调" ## 输入文件 读取同一标的的以下两个文件: | 文件 | 路径 | |------|------| | 投资备忘录 | `reports/investment_memo_[股票代码]_[公司名]_[日期]_V[版本].md` | | 思考链 | `reports/reasoning_trace_[股票代码]_[公司名]_[日期]_V[版本].md` | ## 输出文件 写入 `reports/critical_review_[股票代码]_[公司名]_[日期]_V[版本].md` 版本号与同标的的 investment_memo 和 reasoning_trace 保持一致。 ## 审查框架(逐节对抗式审查) ### 第1节:整体评估与完整性检查 (Overall Assessment & Completeness Audit) 对原报告给出一个整体的可信度评分和基调判断。 **审查要点:** - 原报告的整体倾向是乐观、中性还是悲观? - 作者是否在选择性呈现数据?(只说好的,不提坏的?) - 报告中的"叙事"(story)是否压倒了"数字"(facts)? - **报告完整性检查:** 确认原报告是否包含全部 8 个板块(1.核心结论 2.商业模式 3.财务体检 4.管理层 5.估值三模型 6.风险 7.催化剂 8.三维扫描)。如有缺漏,在此标记。 - **三模型完整性检查:** 确认第5节是否完整覆盖 5a(ARV)、5b(EPV)、5c(护城河验证)、5d(格林沃尔德决策树)、5e(麦肯锡价值驱动模型)、5f(反向DCF)、5g(交叉验证对比)。如有缺失,在此标记。 ### 第2节:数据质量审查 (Data Quality Audit) 逐条检查每个数据源的可靠性和适用性。 **审查要点:** - yfinance 的数据是否经过验证?(公司财报数据 vs yfinance 的映射可能有误) - A股数据中哪些字段是 yfinance 没能正确映射的? - Baostock 的分红数据是否包含所有分红类型? - FRED 的美债数据用于 A 股 WACC 计算是否合理? - **对于缺失数据的处理:** 报告是否诚实地标记了缺失字段? - **⚠️ 现金类科目完整性检查(高频错误):** 确认 `cash_and_equivalents` 是否遗漏了 `other_short_term_investments` 字段。对于中国互联网公司/ADR,短期理财产品可能占现金的80%+。检查 financials JSON 中是否存在 `total_cash_and_st_investments` 字段,并与 `cash_and_equivalents` 对比差异。 - **⚠️ 币种一致性检查:** 所有金额计算中,确认分子和分母是否在同一币种(如净现金/市值时,现金在CNY但市值在USD → 必须统一)。净现金/市值 > 80% 时自动标记为可疑并要求做币种换算验证。 ### 第3节:关键假设压力测试 (Assumption Stress Test) 找出原报告中最脆弱的3-5个假设,逐个进行极限测试。 **审查要点(每项假设需要回答):** 1. 这个假设的原始依据是什么? 2. 如果这个假设错了50%,结论会变成什么样? 3. 这个假设是否有历史数据支持?还是拍脑袋? **典型攻击目标(举例):** - WACC 为什么取8%而不是10%或6%? - 护城河持续期10年的依据是什么?如果只有3年呢? - 营收增速32%可持续吗?如果跌到10%估值会怎样? ### 第4节:三模型估值逻辑对抗 (Valuation Adversarial Review) 对第5节中三种估值模型的每一段进行对抗式推演。 **模型A(格林沃尔德)审查:** - ARV审查:品牌重置成本是怎么算出来的?固定资产重置倍数的合理性? - EPV审查:NOPAT的正常化调整是否合理?加权平均权重(5:3:2)是否适当?WACC取值的合理性?SG&A隐蔽利润加回的25%是否过高? - 决策树审查:三要素判据是否正确计算?是否存在 EPV < ARV 但被忽略的情况? **模型B(麦肯锡价值驱动模型)审查:** - g(长期增长率)的设定依据是什么?是否 ≤ 名义GDP增速? - IR = g/ROIC 是否 < 1?如果 IR ≥ 1,说明增长在销毁价值,模型是否跳过? - ROIC、WACC 的微小变化对估值影响多大?(g/(WACC-g) 放大效应) - 此模型明确标注为麦肯锡(McKinsey)方法,不与格林沃尔德三要素混淆? **模型C(反向DCF)审查:** - 隐含增速与历史增速的差距是否合理? - 市场是过度悲观还是均值回归预期? ### 第5节:认知偏差诊断 (Cognitive Bias Diagnosis) **常见偏差识别:** | 偏差 | 表现 | 在本报告中是否有? | |------|------|------------------| | 确认偏差 | 只找支持结论的证据 | | | 可用性偏差 | 过于依赖容易获取的数据 | | | 锚定效应 | 股价被当前价格锚定 | | | 叙事谬误 | 用一个好的故事替代了严密的分析 | | | 过度自信 | 对护城河持续期的确定性过高 | | **特别注意:** 作者最自信的判断,往往是最需要怀疑的。 ### 第6节:遗漏风险清单 (Missing Risk Register) 列出原报告可能没有充分讨论的风险。 **模板:** ```markdown | 风险 | 原报告提及? | 我认为的重要性 | 理由 | |------|------------|--------------|------| | [风险1] | ✅/❌ | 高/中/低 | | ``` ### 第7节:情景推演 (What If Analysis) 挑出3个原报告中"不太可能但一旦发生就致命"的情景,进行推演。 **模板:** ```markdown ### 情景A:[名称](概率[X%]) 如果[某个关键假设不成立]: → FY+1: [量化影响] → FY+3: [量化影响] → 对股价的影响:[上升/下降 X%] ``` ### 第8节:最终裁决 (Final Verdict) 给出你对这份报告的整体判断。 **模板:** ```markdown ## 8. 最终裁决 **原报告可信度评分:** [6/10] **检查结果:** - 8个板块完整性:[全部覆盖/缺失___] - 三模型交叉验证:[完整/缺少___] **我认同的部分:** 1. ... 2. ... **我不认同的部分:** 1. ... 2. ... **如果要我给建议,我的版本是:** [简短的一句话结论] ```
references/original-skills/financial-analysis.md---
name: financial-analysis
description: "投资分析核心 — 数据采集 → 脚本计算 → 三模型估值 → 4份报告 — 对筛选出的标的进行机构级双重健康检查(资深对冲基金分析师 + CFO视角),输出结构化投资备忘录"
version: 1.0.0
author: Hermes Agent
---
# Financial Analysis — 投资备忘录生成器
> 💡 **想查看完整的使用指南?说 "如何使用" 即可获取(加载 `how-to-use` skill)。**
> 涵盖:数据采集 → 8 个脚本 → 交叉校验 → 4 份报告。用户明确要求"上传Notion"时才上传。
## 角色设定(系统前缀提示)
每次触发此 skill 时,必须在系统层面注入以下角色人格:
> 你现在是全球顶级对冲基金拥有20年实战经验的资深投资分析师,同时你具备顶级企业CFO的严苛财务审查视角。你深度理解巴菲特/芒格、格雷厄姆、格林沃尔德的价值投资理论。
>
> 你的投资哲学极其厌恶风险,你极其警惕管理层的"叙事(Narratives)",只相信冷冰冰的数字、真实的自由现金流和确定的股东回报。你擅长像CFO一样对企业进行"深度体检",甄别资产质量与投资效率。
>
> 数据铁律:
> - 强制联网获取最新财报(TTM)、最新Earnings Call指引、实时股价/市值
> - 拉取过去3-5年核心财务数据进行纵向对比
> - 计算真实自由现金流时,必须扣除股权激励费用(SBC)
> - 账上未明确用于高回报核心业务的多余现金,绝对不能算作业务再投资,应视为单纯的"现金收益"(Cash Yield)
> - 复杂估值计算(ARV, EPV, 单位产出资本)必须清晰列出公式、输入值及计算过程
> - **ARV 必须展示 3 年对比表格**,逐项资产列示;**EPV 必须使用 3 年加权平均 NOPAT**(5:3:2 权重或行业适当权重),禁止单年数据
>
> 报告完整性铁律:
> - 每份投资备忘录必须完整包含 **全部 9 个板块**(1.核心结论 2.商业模式 3.管理层 4.财务体检 5.估值 6.风险 7.催化剂 8.三维扫描 9.决策建议),**不得缺漏任何一节**
> - 第5节(估值)必须完整展示 **三种模型的交叉验证**(格林沃尔德三要素 + 麦肯锡价值驱动模型 + 反向DCF),不得省略其中任何一种
> - 第5节中各子节(5a-5g)的呈现格式必须与底部「备忘录中的呈现格式」一致
>
> 计算纪律(重要—历史教训总结):
> - 🚫 **禁止脑算**:所有金额换算、比率计算、估值定价必须用 `execute_code`(Python)或终端命令完成。不允许在回复正文中直接写计算结果。
> - 🚫 **禁止混用币种**:当数据混用 CNY 和 USD 时(如中国ADR yfinance返回CNY财务报表但市值在USD),必须先统一币种再计算。净现金/市值 > 100% 时触发强制检查。
> - 🇨🇳 **中国 ADR 统一 CNY 原则**:用户明确要求所有金额以人民币计价。对于中国 ADR(美股),报CNY财务数据的不动,市值/股价从 USD 按汇率 7.3 转换为 CNY。所有比率在单一币种下计算。
> - ✅ **每条数字必须显式标注币种和单位**:格式 `¥200亿` 而非 `200亿`。美元前缀 `$`。
> - ✅ **数量级双重检查**:`$6.2B` 换算 RMB 后应约为 `¥450亿` 而非 `¥45亿`($1B ≈ ¥7亿 ≈ ¥70亿,注意亿级单位!)
> - ✅ **结果合理性自检**:任何计算得出的比率超过行业常识范围(如净现金/市值 > 80%、PE < 5x、毛利率 > 95%)时,自动暂停并反查数据源和计算步骤。
>
> 日期纪律(2026-05-05 修正):
> - 报告文件名中的日期必须是 **运行分析时的系统当前日期**(`date +%Y%m%d`),而不是会话开始日期或任何其他日期
> - 报告正文中的日期同理
> - 跨天运行的多个分析,各自使用各自的生成日期
## 触发条件
当用户说以下内容时触发此 skill:
- "用CFO视角审查 [公司名称/代码]"
- "深度分析 [公司名称/代码]"
- "写一份投资备忘录关于 [公司名称/代码]"
- "帮我分析候选标的"(配合 strategic-target-search 管道输出)
- "体检 [公司名称/代码]"
- "分析一下 [代码]"
- "如何使用"(加载使用指南 → `skill_view(name='how-to-use')`)
- 或使用 `--ai-only` 生成的 `ai_analysis_report.md` 中的候选标的后,要求深入分析
## 数据获取(两步法)
> ⚠️ **脚本不可用注意事项:** 截至 2026-05-13,`financial-analysis/scripts/` 目录下的 8 个分析脚本(`calculate_reproduction_value.py` 等)**尚未部署到文件系统**。SKILL.md 中引用的脚本路径是目标结构,实际运行时需用 Python 直接计算。
>
> **替代工作流:**
> 1. 用 mx-data 拉取原始数据 → 从 `_raw.json` 文件提取表格(JSON 路径:`d['data']['data']['searchDataResultDTO']['dataTableDTOList']`)
> 2. 用 `execute_code`(Python)直接计算 ARV、EPV、ROIC-WACC、利润率分解等
> 3. 数据来源和计算步骤必须在报告推理轨迹(reasoning_trace)中完整记录
>
> 脚本部署后此条可移除。
### 第1步:运行财务数据采集脚本
```bash
python3 ~/.hermes/skills/data-science/financial-data-acquisition/scripts/fetch_financials.py \
--ticker [代码] \
--output ~/.hermes/skills/data-science/out/temp_data/financials_[代码].json
```
### 第2步:运行四项专项分析脚本(自动生成 ARV / 维护性CapEx / Spread / 利润率分解)
```bash
cd ~/.hermes/skills/data-science/financial-analysis/scripts
python3 calculate_reproduction_value.py --ticker [代码] --output ../../out/temp_data/arv_[代码].json --format json
python3 extract_maintenance_capex.py --ticker [代码] --output ../../out/temp_data/maintenance_capex_[代码].json --format json
python3 analyze_roic_wacc_spread.py --ticker [代码] --output ../../out/temp_data/spread_[代码].json --format json
python3 margin_decomposition.py --ticker [代码] --output ../../out/temp_data/margin_[代码].json --format json
python3 audit_capital_allocation.py --ticker [代码] --output ../../out/temp_data/capital_allocation_[代码].json --format json
python3 insider_alignment_check.py --ticker [代码] --output ../../out/temp_data/insider_check_[代码].json --format json
python3 detect_accounting_anomalies.py --ticker [代码] --output ../../out/temp_data/anomalies_[代码].json --format json
python3 stress_test_margin_of_safety.py --ticker [代码] --output ../../out/temp_data/stress_test_[代码].json --format json
```
### 第2.5步:数据交叉校验(强制 — 报告生成前的安全阀)
```bash
cd ~/.hermes/skills/data-science/financial-analysis/scripts
python3 cross_validate.py --ticker [代码] --data ../../out/temp_data/financials_[代码].json
```
**校验结果处置规则:**
| exit code | 含义 | 处理方式 |
|-----------|------|---------|
| 0 | 全部通过 | 直接进入报告生成 |
| 1 | 有警告 | 进入报告生成,但在报告中标注「⚠️ 交叉校验有 X 项警告,详见 validation_[代码].json」 |
| 2 | **有错误** | **暂停报告生成!** 将校验错误信息展示给用户,请求确认/修复后再继续 |
**常见错误处理指南:**
- `C2 币种不一致` → 市值和现金在不同币种,统一换算后再计算净现金/市值等指标
- `C5 利润质量异常` → 检查 OCF/净利润比率,可能需手动从财报补充数据
- `C1 非活期现金占比高` → 阅读公司财报注释确认短期投资性质(定存/理财/结构性存款)
**对应输出文件:** `../../out/temp_data/validation_[代码].json`
### 第3步:读取全部输出文件
| 文件 | 内容 | 在备忘录中的用途 |
|------|------|----------------|
| `financials_[代码].json` | yfinance 5年财报 + Baostock + SEC + Finnhub + FRED | 所有基本面数据来源 |
| `arv_[代码].json` | 逐项资产重置价值 | ARV/股 → 第5节 |
| `maintenance_capex_[代码].json` | 维护性vs增长性CapEx拆分 | EPV调整、FV再投资率 |
| `spread_[代码].json` | ROIC-WACC差值 + 同业对比 | 护城河判定 → 第2节 |
| `margin_[代码].json` | 利润率分解 + 利润质量评分 | 利润质量 → 第3节 |
| `capital_allocation_[代码].json` | 资本配置审计 | 管理层 → 第4节 |
| `insider_check_[代码].json` | 内部人一致性检查 | 管理层 → 第4节 |
| `anomalies_[代码].json` | 会计异常检测 | 财务体检 → 第3节 |
| `stress_test_[代码].json` | 安全边际压力测试 | 下行风险 → 第6节 |
## 输出文件
写入 `~/.hermes/skills/data-science/out/out_reports/` 目录下。
**命名规则:** `[类型]_[股票代码]_[公司名]_[系统当前日期YYYYMMDD]_V[版本].md`
**日期规则:** 使用 `date +%Y%m%d` 获取系统当前日期。不得使用会话开始时间或任何其他日期。
**版本规则:**
- **版本号自动感知(强制):** 写文件之前,必须扫描 `reports/` 目录下同一标的当日已有的文件,提取当天已存在的最大版本号,然后 +1。**不能假定为V1。**
- 版本号从 V1 开始(当天第一份)
- 同一标的同一天每多生成一次报告,版本号 +1
- 四个文件(备忘录+思考链+批判性审视+巴菲特)使用**同一版本号**
- 跨天重置:新的一天从 V1 重新开始
**自动生成规则:**
- 每次调用 financial-analysis 时:自动生成**4 份报告**(备忘录 + 思考链 + 批判性审视 + 巴菲特视角)
- 所有 4 份报告版本号一致
- 用户习惯:当说"分析一下 [代码] 的投资价值"时默认生成全套 4 份,无需单独要求
### 三件套生成顺序
```
1. investment_memo_[代码].md ← 基于 financials_[代码].json 写核心分析
2. reasoning_trace_[代码].md ← 基于 investment_memo 的推理过程
3. critical_review_[代码].md ← 基于 reasoning_trace,对抗式审查
```
不允许跳过步骤:没有 reasoning_trace 就无法生成 credible 的 critical_review。
### 第4步(仅当用户要求时执行):上传至 Notion
**默认不执行。** 只有用户明确说出"上传Notion"、"同步到Notion"、"发到Notion"等指令时,才运行此步骤。
如需上传,通过 `notion-upload` skill 完成:
**前置条件:**
1. `NOTION_API_KEY` 已配置在 `~/.hermes/.env` 中
2. Notion 目标页面已对 Integration 开放访问
```bash
cd ~/.hermes/skills/data-science/notion-upload/scripts
export $(grep NOTION_API_KEY ~/.hermes/.env)
python3 upload_to_notion.py --ticker [代码]
```
详见 `skill_view(name='notion-upload')`。
## 分析框架(9点结构)
### 1. 核心结论与预期差 (The Variant Perception)
- **市场共识:** 华尔街对这只股票的主流看法是什么?当前 Price Target 区间?市场定价包含了怎样的增长、利润率、资本回报假设?为什么市场这么想?(2-3句,展示你对市场叙事的深刻理解)
- **预期差(Variant Perception):** 你的观点与市场共识有什么不同?市场忽略了什么关键数据、趋势或结构性问题?这个预期差如果兑现,会产生多大的价值重估空间?(2-3句,必须清晰说明你的核心论据)
- **一句话结论:** [买入 / 增持 / 持有 / 观望 / 减持 / 卖出 / 做空] — 一句话总结核心判断(操作建议 + 核心理由 + 预期回报),应自然承接上文预期差分析
### 2. 商业模式与微观投资效率 (Business & Investment Efficiency)
- **业务本质:** 公司到底在做什么生意?用一段话说清楚它是怎么赚钱的。必须通俗易懂,让对这家公司一无所知的人也能看明白
- **多业务分拆:** 如果公司有多个业务板块(如:广告+订阅+电商),必须**分大板块逐一说明**——每个板块的营收占比、盈利模式、增长趋势、竞争格局。业务分析要覆盖其总营收的90%以上
- **业务存在的问题:** 这个生意有什么结构性缺陷?增长天花板在哪?竞争格局的演变方向是什么?
- **单体经济学**(CFO视角):公司"砸多少钱能产出一个有效单位"?ROIC 分解——靠高净利还是高周转?
- **护城河压力测试:** 明确护城河来源,并推演什么极端情况会彻底摧毁它?
### 3. 管理层诚信与真实股东回报 (Management Integrity & Actual Return)
#### 管理层三维评估
| 维度 | 评分 | 评估内容 |
|------|------|---------|
| **诚信** | A/B/C/D | 财务披露质量、报表是否及时完整、是否有会计调整/重述历史、关联交易是否透明、管理层在公开场合的言论与实际行动是否一致 |
| **资本配置能力** | A/B/C/D | 过去5年的回购/分红/SBC/并购决策:回购是否在低估时执行、SBC是否被回购覆盖、并购是否产生正回报、是否保留了过多冗余现金 |
| **所有者思维** | A/B/C/D | 内部人持股比例(>20%为优秀,5-20%为一般,<5%为警示)、薪酬结构(是否与业绩挂钩而非固定高薪)、管理层是否愿意在股价低迷时用自有资金增持 |
**真实股东收益率:**
```
股东收益率 = (现金分红 + 净回购金额) / 当前市值
SBC稀释净效应 = 净回购 - SBC(正值 = 净回报股东,负值 = 净稀释)
```
**资本配置审计评分:**(参考 `audit_capital_allocation.py` 输出,0-10分)
**SBC 稀释分析:** 至少展示3年的SBC/营收、SBC/OCF、SBC/净利润趋势。
### 4. CFO级财务体检与资产质量 (Financial Health & Asset Quality)
- 历年财务数据必须以表格呈现(至少3年),表格下方必须输出 **「核心洞察」**,不得只列数据不做分析
- 核心洞察应包含:趋势方向(改善/恶化)、关键拐点、数据之间相互印证或矛盾的发现
- 资产质量辨析(好PB vs 坏PB)
- 自由现金流 (FCF) 的真实性:经营现金流 - 资本开支 - SBC = 真实所有者盈余
### 5. 估值与安全边际 (Valuation & Margin of Safety)
**⚠️ 强制规则:** 在撰写本节之前,必须依次加载以下三个方法论文档,逐一对照公式、步骤和格式:
```python
# 顺序加载——不跳过、不倒序
skill_view(name='financial-analysis', file_path='references/greenwald-valuation.md')
skill_view(name='financial-analysis', file_path='references/mckinsey-value-driver-model.md')
skill_view(name='financial-analysis', file_path='references/reverse-dcf.md')
```
加载后逐项对照:ARV 回收率假设、EPV 权重分配、麦肯锡公式参数、反向 DCF 判读逻辑。**不允许凭记忆写估值公式。**
本节使用三种独立估值模型进行交叉验证。完整方法论详见本 skill 的 references/ 目录,通过 `skill_view(name='financial-analysis', file_path='references/...')` 加载。
**模型索引:**
- **模型 A — 格林沃尔德三要素法** → `references/greenwald-valuation.md`
- **模型 B — 麦肯锡价值驱动模型** → `references/mckinsey-value-driver-model.md`
- **模型 C — 反向 DCF** → `references/reverse-dcf.md`
**⚠️ 硬性要求:**
1. ARV 和 EPV 必须展示 **3 年以上的多期计算过程**,不得只写单年结果
2. **三种模型必须各自展示完整的计算步骤和中间结果**,不得跳过中间变量直接给最终数字
3. 所有计算必须通过 Python execute_code 或终端命令执行
4. 不得混合三种模型的方法论
5. 每个公式都要写出代入数值后的中间结果
### 估值与安全边际呈现格式
#### 5a. ARV — 3年资产重置价值对比
```markdown
| 资产类别 | FY-2(¥亿) | FY-1(¥亿) | 最新(¥亿) | 回收率 | 重置价值(¥亿) |
|---------|----------|----------|----------|--------|-------------|
| 现金+短期投资 | XX | XX | XX | 100% | XX |
| 应收账款 | XX | XX | XX | XX% | XX |
| 存货 | XX | XX | XX | XX% | XX |
| 固定资产 | XX | XX | XX | 150% | XX |
| 商誉 | XX | XX | XX | 0% | 0 |
| **ARV总值** | | | | | **¥XX亿** |
| 流通股数 | XX | XX | XX | | |
| **ARV/股** | **¥XX** | **¥XX** | **¥XX** | | |
```
#### 5b. EPV — 3年加权正常化收益能力(步骤+中间结果)
```python
# 第1步:3年营业利润加权
OP_2025 = ¥XX亿(×0.5) OP_2024 = ¥XX亿(×0.3) OP_2023 = ¥XX亿(×0.2)
加权OP = OP_2025×0.5 + OP_2024×0.3 + OP_2023×0.2 = ¥XX亿 # ← 中间结果
# 第2步:隐蔽利润加回
平均SG&A = (¥XX+¥XX+¥XX)/3 = ¥XX亿
hidden = 平均SG&A×25% = ¥XX亿
# 第3步:NOPAT
NOPAT = (加权OP¥XX亿 + hidden¥XX亿) × (1-XX%) = ¥XX亿
# 第4步:CapEx手术
增长CapEx = 营收增长¥XX亿 × (PP&E/营收) = ¥XX亿
维护CapEx = 总CapEx¥XX亿 - 增长CapEx¥XX亿 = ¥XX亿
adj_earnings = NOPAT¥XX亿 + 税后超额折旧 - 维护CapEx¥XX亿 = ¥XX亿
# 第5步:EPV
超额现金 = max(0, 账面现金 - 营收×1%) = ¥XX亿
EPV(股权) = adj_earnings¥XX亿/WACC + 超额现金¥XX亿 - 有息债务¥XX亿 = ¥XX亿
EPV/股 = ¥XX亿 / XX亿股 = ¥XX
| WACC | 8% | 9% | 10%(基准) | 12% |
|------|----|----|----------|-----|
| EPV/股 | ¥XX | ¥XX | **¥XX** | ¥XX |
```
#### 5c. 护城河交叉验证 + 预期收益率
| EPV=¥XX vs ARV=¥XX | [EPV>ARV/≈ARV/<ARV] |
预期收益率(仅EPV>ARV时):现金回报+内生增长+再投资-衰减
#### 5d. 格林沃尔德三要素独立判据
```
① ARV/股=¥XX vs 市价¥XX → [一级/无]
② EPV/股=¥XX vs 市价¥XX → [二级/无]
③ EPV>ARV? [是/否]
决策树:一级 / 二级 / 三级 / 无 安全边际
```
#### 5e. 麦肯锡价值驱动模型(模型B — McKinsey,非格林沃尔德方法)
```python
NOPLAT = EBIT×(1-税率) = ¥XX亿 × (1-XX%) = ¥¥XX亿
ROIC = NOPLAT / 投入资本 = ¥¥XX亿 / ¥¥XX亿 = XX.X%
WACC = X.X%(与格林沃尔德一致)
# 设定参数
g = X%(长期可持续增长率,≤名义GDP增速3-5%)
再投资率 IR = g / ROIC = X% / XX% = XX.X%
# IR必须<1,否则增长在销毁价值
# 核心价值公式
企业价值(经营) = NOPLAT × (1 - IR) / (WACC - g)
= ¥¥XX亿 × (1-XX.X%) / (X.X%-X%)
= ¥¥XX亿
# 加回非核心资产
EV = ¥¥XX亿 + 超额现金¥¥XX亿 + 非经营性投资¥¥XX亿
股权价值 = EV - 有息债务¥¥XX亿 = ¥¥XX亿
每股价值 = ¥¥XX亿 / XX亿股 = ¥¥XX
# 模型自我验证
# IR = XX.X% < 1? [是/否] → ROIC必须>g
# WACC - g = X.X% > 0? [是/否] → g必须<WACC
# ROIC - WACC = X.X% > 0? [是/否] → 增长创造价值
```
#### 5f. 反向 DCF(模型C — 市场隐含假设)
**⚠️ 强制格式:必须严格按照 `references/reverse-dcf.md` 第7节「投资备忘录中的标准输出格式」执行。以下仅为简要参考,完整模板见 reverse-dcf.md。**
标准输出包含6步:
```
Step 1: 参数预估依据
├── ① WACC (Rf/b/ERP/Ke/Kd 每项??来源)
├── ② g_terminal (选定值+理由+约束校验 ✅)
├── ③ FCF基期 (方案A标准 vs 方案B正常化)
└── ④ g搜索范围 (按行业分类表选择)
Step 2: 输入参数汇总表 (两套方案)
Step 3: 方案A — 标准FCF基期
├── 核心公式 (两段式DCF公式展开)
├── 7档试算表 (含FCF₁/FCF₅/预测期PV/终值PV/EV/每股价值/vs市价)
└── Python二分法代码 + 精确隐含增速
Step 4: 方案B — 正常化FCF (可选)
Step 5: 方案C — 隐含PE反推法 (FCF负时备用)
Step 6: 判读结论 (历史参考 + 三方案矛盾分析 + 三模型交叉验证)
```
**参考案例:** CROX 投资备忘录 5f 节(2026-05-15 生成)
#### 5g. 三模型交叉验证对比
| 维度 | 格林沃尔德(A) | 麦肯锡(B) | 反向DCF(C) |
|------|-------------|------------|-----------|
| 安全边际 | [一级/二级/三级/无] | [NOPLAT×(1-IR)/(WACC-g)] vs 市值 | 隐含vs历史 |
| 增长价值 | ❌定性(零增长EPV) | ✅定量(显式g假设) | ✅隐含增速 |
**综合判断:** 三种模型[一致/分歧],分歧原因分析。
---
### 6. 下行风险预演 (Pre-mortem Analysis)
列出最可能伤害投资价值的 **3-5 项风险**。每项风险必须包含以下完整分析:
| 风险 | 概率 | 影响 | 分析 |
|------|------|------|------|
| [风险名称] | [高/中/低] | [高/中/低] | **详细阐述:** 什么条件下这个风险会兑现?对营收/利润/现金流的量化影响?历史上是否有类似案例?管理层是否有应对措施? |
| ... | ... | ... | ... |
**每项风险的分析栏不得少于 2-3 句话,必须包含:**
1. **触发条件:** 什么情况下这个风险会成为现实
2. **量化影响:** 对营收/利润/估值的具体影响(可用%或绝对数值)
3. **历史参考:** 行业或公司历史上是否有先例
4. **管理层的应对能力:** 公司能否应对?有什么工具(现金、定价权、成本削减)?
**最可能伤害股价的情景(单独写一段):**
详细推演一个最可能发生的负面情景:什么触发 → 传导路径 → 量化结果 → 对股价的影响。
### 7. 催化剂与交易结构 (Catalysts & Execution)
### 8. 终极三维扫描 (打分 0-10分)
从三个独立维度对投资标的进行综合评分,每项必须有打分理由,不得只有数字。
| 维度 | 评分 | 打分理由 |
|------|------|---------|
| **质量** | X/10 | 商业模式可持续性、护城河强度、管理层能力、财务健康度。好生意应该有护城河+高ROIC+低SBC+低负债。 |
| **估值** | X/10 | 当前价格与内在价值的关系。参考三模型交叉验证结果:格林沃尔德安全边际、超额回报安全边际、反向DCF隐含增速合理性。 |
| **催化剂** | X/10 | 未来6-18个月内是否有触发价值重估的事件?回购、分红、业绩拐点、行业政策、新产品? |
| | | |
| **综合评分** | **X/10** | **三分的加权平均(质量40%、估值40%、催化剂20%),并给出最终的投资行动建议。** |
**综合评分对应建议:**
- 8-10分 → 强力买入(显著安全边际 + 强护城河 + 催化剂明确)
- 6-8分 → 增持/持有(合理估值 + 好公司)
- 4-6分 → 观望(估值合理但没有明确催化剂)
- 2-4分 → 减持/回避(估值过高或基本面恶化)
- 0-2分 → 卖出/做空(结构性衰退或估值泡沫)
---
### 9. 决策建议 (Investment Decision)
综合全部9个板块的分析,给出清晰的决策建议。
| 项目 | 内容 |
|------|------|
| **操作建议** | 买入 / 增持 / 持有 / 观望 / 减持 / 卖出 / 做空 |
| **目标价** | ¥XX-XX(或$XX-XX),对应XX倍正常化PE / XX倍EV/EBITDA |
| **持有期限** | 3-6个月 / 6-12个月 / 1-3年 / 3-5年 |
**买入逻辑(3-5条):**
1. ...(每条一句话,对应前文分析中的核心论据)
2. ...
3. ...
**卖出条件(3-5条):**
1. 基本面恶化:...(具体的量化标准,如营收增速跌破X%)
2. 估值达标:股价达到目标价区间上沿
3. 管理层行为:...(如大规模减持、停止回购、SBC失控)
4. 更好机会:出现风险收益比更优的替代标的
5. 强制止损:股价跌破¥XX(对应XX%下行空间)
## 已知陷阱与行业特化规则
### 亏损/负FCF公司分析特化(2026-05-14 INTC 教训)
当公司出现以下信号时,标准三模型估值框架**部分或全部失效**:
- FCF 连续两年负值
- Net Income 连续两年亏损
- EBIT 为负导致 NOPAT 为负
- ROIC < 0%(摧毁价值)
**各模型失效原因与替代方案:**
| 模型 | 失效原因 | 替代处理 |
|------|---------|---------|
| **EPV** | NOPAT 为负 → EPV 负值(无意义) | 跳过 EPV 计算,在报告中注明「当前盈利能力为负,EPV 框架不适用」。改为用反向推算:假设公司恢复到 X% 正常化利润率需要的时间。 |
| **麦肯锡** | NOPLAT 负 → (WACC-g) 分母为正但分子负 → 企业价值负 | 跳过。注明「ROIC<WACC,增长正在摧毁价值」。 |
| **反向 DCF** | FCF₀ 为负 → 无法作为基准 | 标准 DCF 不适用。改为用 **隐含 PE 反推法**:市值 / 远期PE = 市场隐含的净利润水平。与公司历史净利润峰值对比,判断市场定价是否合理。 |
| **ARV** | ✅ 仍然适用 | 资产重置价值是这类公司最重要的估值锚。PP&E 回收率需按行业调整(半导体 fab 1.3-1.5x,软件 0.3-0.5x)。 |
**报告格式调整(当确认标的为亏损/负FCF时):**
- 第5节标题改为「估值 — 框架失效说明与替代分析」
- 逐条说明哪些框架不适用及原因
- 重点使用 ARV + 隐含PE反推 + 分析师目标价交叉验证
- 决策建议给出「买入条件清单」而非标准目标价(如:当股价跌至 XX 以下 / FCF 转正 / 客户公告等才能买入)
**SBC 在亏损公司中的特殊处理:**
亏损期仍发放高额 SBC(如 Intel 2025 年 SBC $2.4B,占营收 4.6%)是管理层与股东利益不一致的明确信号。必须在第3节「管理层评估」中重点标注,资本配置评分应 ≤ 3/10。
### 困境/资不抵债公司分析
当股东权益为负、OCF连续两年为负、或净负债/市值 > 200% 时,标准估值框架不适用。评估重点转向「现金跑道 × 债务偿还能力 × 债权人优先级」。
### 银行股分析特化(FCF 陷阱)
银行股的"经营现金流"主要是存款流入与贷款流出的差额,完全不等同于工业企业的 FCF。银行 ROIC 退化为 ROE。PB<1 是在为资产质量定价。
### 中国 A 股数据注意事项
- 股息率格式:A股返回百分比(4.06=4.06%),美股返回小数
- SBC/回购/分红:A股在 yfinance 上大多缺失
- 银行/保险资产负债表映射不准
- **ROE/行业分类**:yfinance 对 A 股的数据常有偏差(ROE偏低、行业分类笼统),需用 mx-data 或 akshare 同花顺通道交叉验证
### A 股数据源决策规则
分析 A 股时,按以下优先级选择数据通道(详见 `references/china-a-share-data-sources.md`):
```
mx-data (东方财富官方API) → 首选手工查询
↓ 不可用时
yfinance (基础数据) + akshare 同花顺通道 (财务数据)
↓ 行情缺失
qt.gtimg.cn (腾讯行情兜底)
```
**切勿依赖 push2.eastmoney.com** — 该端点在海外/非大陆网络环境中经常被屏蔽。
### SaaS/高增长科技公司分析特化(2026-05-05 DUOL教训)
格林沃尔德框架对SaaS/轻资产科技公司存在系统性偏差。以下问题必须在分析时明确标注:
**ARV低估问题:** SaaS公司的核心资产(用户基数、品牌、培训数据、算法、API生态)不在资产负债表上。ARV反映的是现金+服务器+办公设备的清算价值,严重低估了公司真正的经济价值。因此,ARV对于SaaS公司更像一个「不完整的地板参考」,而非锚定估值。
**EPV权重争议:** 标准5:3:2权重在高速增长公司中可能过度惩罚历史。如果公司在3年内从亏损到高速盈利(如Duolingo: -$13M→$63M→$136M),20%权重给亏损年的处理会系统性拉低EPV。对于追赶上期增速 > 30% 的公司,建议使用以下替代方案:
- 4:3:3 权重(减少历史年份影响)
- 或仅用最近2个盈利年的4:4权重+20%给保守折扣
- 在报告第5b节中注明「⚠️ 高增长公司,标准权重可能低估EPV」,并展示替代权重的对比
**EPV<ARV信号解读:** 当EPV<ARV时,在传统工业公司中意味着企业无法创造超额回报。但在SaaS公司中,这个信号可能意味着EPV的盈利基数过低(利润尚未成熟),而非公司在销毁价值。应在5c节中标注「⚠️ 可能因增长阶段EPV尚未跑出ARV」。
### 周期性行业/重资产公司分析特化
- EPV 必须使用正常化 NOPAT(3-5年加权平均,非单年)
- 投入资本计算需要处理周期膨胀
- FV 应设为零(除非有极强竞争壁垒)
- 评估生存能力优先于估值(计算现金跑道)
分析中国 ADR 时,yfinance 返回 CNY 财务报表但 USD 股价。**必须在分析前统一币种。** 市值/股价从 USD 按汇率 7.3 转换为 CNY。详见 `references/adr-currency-protocol.md`。
**快速记忆:** $6.2B × 7.3 = ¥452亿(非 ¥45亿——注意亿级!)
## 妙想 (mx-*) 技能集成
**⚠️ 陷阱警示(2026-05-13 修正):** 当用户问「妙想的 skill」时,指的是全部的 `mx-*` 前缀技能(mx-data / mx-search / mx-xuangu / mx-zixuan / mx-moni),而非某个特定 skill。切勿回答「没有妙想 skill」。
| mx-* Skill | 文件路径 | 作用 |
|------------|---------|------|
| mx-data | `skills/mx-data/SKILL.md` | A股 行情/财务/关联数据查询 |
| mx-search | `skills/mx-search/SKILL.md` | 新闻/研报/公告/政策搜索 |
| mx-xuangu | `skills/mx-xuangu/SKILL.md` | 智能选股条件筛洗 |
| mx-zixuan | `skills/mx-zixuan/SKILL.md` | 自选股管理 |
| mx-moni | `skills/mx-moni/SKILL.md` | 模拟组合管理 |
**在 A 股分析中的调用流:**
1. `mx-xuangu` → 初筛候选标的
2. `mx-search` → 获取近期新闻/研报/公告
3. `mx-data` → 精确财务数据(替代 yfinance A 股数据)
4. `mx-zixuan` → 加入自选跟踪
**前置条件:** 需要 `MX_APIKEY` 环境变量(从 https://dl.dfcfs.com/m/itc4 获取)。详见 `references/china-a-share-data-sources.md`。
references/original-skills/financial-data-acquisition.md---
name: financial-data-acquisition
description: "金融数据采集器 — 从 yfinance + SEC EDGAR + Finnhub + FRED + Baostock + Tushare + MX API (东方财富妙想) 获取结构化财报、10-K文本、Earnings Call、宏观经济、A股全面数据"
version: 2.2.0
author: Hermes Agent
---
# Financial Data Acquisition — 金融数据采集器
## 角色定位
此 skill 是 **financial-analysis skill 的数据前置层**。在分析一个标的之前,先调用此 skill 获取结构化的财务数据,然后传给 financial-analysis 进行9点深度审查。
## 触发条件
- "拉取 [代码] 的财务数据"
- "获取 [公司] 的财报信息"
- "查询 [股票] 的实时行情/财务指标"(会触发 mx-data 自然语言查询)
- 被 cfo 分析流程自动调用
## 数据源矩阵
```\n数据源 美股 A股(.SS/.SZ) 港股(.HK)\n────────────────────────────────────────────────────────────────────\npolygon.io 行情 ✅ 财报 ✅ ❌ ❌\n SBC ✅ 回购 ✅\n 日K线 ✅ 分红 ✅ 拆分 ✅\n (Free: 5calls/min, 需API Key)\nyfinance 行情 ✅ 财报 ✅ 行情 ✅ 财报 ⚠️(缺SBC) 待测试\n SBC ✅ 回购 ✅ 分红 ❌ 回购 ❌ SBC ❌
baostock ❌ 分红 ✅ 指数成分股 ✅ ❌
历史K线 ✅ 行业 ✅
tushare ❌ 前十大股东 ✅ 分红 ✅ ❌
(120积分,可用free接口)
sec-edgar 10-K章节 ✅ ❌ ❌
Business/Risk/MD&A/ExecComp
finnhub Earnings Call ✅ ❌ ❌
新闻 ✅ 内部交易 ✅
fred 宏观数据 ✅ ✅ ✅
国债利率/CPI/GDP/失业率/联邦基金利率
mx-api(妙想) ❌ 行情 ✅ 财务 ✅ 股东 ✅ 行情 ✅ 财务 ✅
新闻 ✅ 公告 ✅ 选股 ✅ 新闻 ✅ 公告 ✅
自选股管理 ✅ 模拟组合 ✅
```
### yfinance — 主采集引擎(所有市场)
- 利润表/资产负债表/现金流量表 5年数据(美股完整,A股缺SBC/回购/分红)
- `info` 字典:实时股价、市值、PE、PB、股息率
- A 股后缀:上海 `.SS`,深圳 `.SZ`
### Tushare Pro — A 股补充数据(前十大股东 + 分红,需 API Token)
**状态:** 2026-05-06 接入,积分120。仅对 A 股(.SS/.SZ)生效。
**安装:** `pip install tushare`
**Token 配置:** `config/api_keys.json` 中 `tushare_token` 字段
**可获取的数据(当前积分可用):**
| 接口 | 积分 | 状态 | 内容 |
|------|------|------|------|
| `dividend` | 200 | ✅ 实测可用 | 历年分红明细(除权除息日、每股税前/税后、送转股) |
| `top10_holders` | 200 | ✅ 实测可用 | 前十大股东(持股数量、持股比例、公告日期) |
| `daily` | 0 | ✅ 免费 | 日线行情 |
| `adj_factor` | 0 | ✅ 免费 | 复权因子 |
| `income` | 200 | ❌ 积分不足 | 利润表 |
**积分不够的接口:** `income`(利润表)、`balancesheet`(资产负债表)、`cashflow`(现金流)、`fina_indicator`(财务指标)、`fina_mainbz`(主营构成)—— 需要200+积分,当前120分不足。
**在 fetch_financials.py 中的调用位置:** `enrich_with_tushare(data)` — A 股(.SS/.SZ) 自动触发,输出至 `data["tushare"]`
### Baostock — A 股数据补充(pip install baostock,无需注册)
**核心用途:** 补 yfinance 缺失的 A 股分红数据,以及**在 yfinance 尚未更新时提前获取最新季度的净利润、净利率、EPS_TTM 等关键财务指标**(如2026 Q1数据,yfinance未收录时baostock已可用)。
**可获取的数据:**
- ✅ **每股现金分红**(税前/税后、除权日、宣告日)— `query_dividend_data()`
- ✅ **最新季度净利润、净利率、EPS_TTM、ROE** — `query_profit_data()`(比 yfinance 更及时)
- ✅ **指数成分股**:沪深300/中证500/上证50 — `query_hs300_stocks()` 等
- ✅ **历史日K线**:日/周/月 — `query_history_k_data_plus()`
- ✅ **股票基本信息**:上市日期、状态 — `query_stock_basic()`
- ✅ **杜邦ROE分解**:`query_dupont_data()`
**不能补:** 详细财报科目、SBC、回购
**在 fetch_financials.py 中的调用位置:** `enrich_with_baostock(data)` — A 股(.SS/.SZ) 自动触发,输出至 `data["baostock"]`
### SEC EDGAR — 美股10-K定性信息(免费、无需注册)
- CIK 查询:`sec.gov/files/company_tickers.json`(官方 ticker→CIK 映射)
- 10-K 提交信息:`data.sec.gov/submissions/CIK{cik}.json`
- 提取章节:Business(Item 1)、Risk Factors(Item 1A)、MD&A(Item 7)、Executive Compensation(Item 11)
- **章节长度:** 每章节最多提取 100,000 字符(约50页文本)。大公司的 MD&A 和 Risk Factors 可能触顶100K,但已足够定性分析使用。
- **注意:** 10-K 的 HTML 格式因公司而异(ITEM 1. vs ITEM I. vs 1. Business),正则提取可能不完美。部分 ExecComp 章节引用至 Proxy Statement(DEF 14A),需单独获取。
### Finnhub — 美股 Earnings Call + 新闻(需 API Key)
- 需要有效 API Key 在 `config/api_keys.json`
- 安装:`pip install finnhub-python`
- 注册:https://finnhub.io/register(免费 tier:60次/分钟)
- 数据内容:`latest_earnings`、`earnings_transcript`、`recent_news`、`insider_transactions`
- 输出位置:`data["finnhub"]`
### FRED(圣路易斯联储)— 宏观经济数据(需 API Key)
- 需要有效 API Key 在 `config/api_keys.json`
- 安装:`pip install fredapi`
- 注册:https://fred.stlouisfed.org/docs/api/api_key.html
- 数据内容:10年国债收益率、联邦基金利率、CPI同比、失业率、名义GDP
- 输出位置:`data["fred"]`
- 适用于所有市场(WACC 计算中的无风险利率 Rf 来源于此)
### MX API(东方财富妙想)— A 股官方数据通道(2026-05-13 新增)
**状态:** 2026-05-13 接入,无需积分、无需注册(通过东方财富APP或微信扫码免费领取 API Key)。
**适用市场:** A股 ✅ 港股 ✅ 美股 ❌
**安装:** 无需安装库,直接 HTTP POST 请求。
#### 可用接口
| 接口 | API 端点 | 功能 |
|------|---------|------|
| `mx-data` | `POST /finskillshub/api/claw/query` | 行情、财务、股东、关联关系数据查询 |
| `mx-search` | `POST /finskillshub/api/claw/news-search` | 新闻、公告、研报、政策搜索 |
| `mx-xuangu` | 智能选股 | 按行情/财务/技术指标筛选股票 |
| `mx-zixuan` | 自选管理 | 东方财富通行证自选股增删查 |
| `mx-moni` | 模拟组合 | 模拟买卖、持仓、委托、历史成交 |
#### 配置
```bash
# 环境变量
export MX_APIKEY="mkt_xxxxx"
# 申请地址
# https://dl.dfcfs.com/m/itc4 → 东方财富APP/微信扫码 → 免费领取
```
#### 使用方法(自然语言查询)
```bash
# mx-data 查询
python3 ~/.hermes/skills/mx-data/mx_data.py "东鹏饮料 最新价 总市值 PE ROE"
# 输出:Excel + JSON + 终端预览
# mx-search 搜索
python3 ~/.hermes/skills/mx-search/mx_search.py "贵州茅台 2025年最新公告"
# mx-xuangu 选股
python3 ~/.hermes/skills/mx-xuangu/mx_xuangu.py "筛选PE<20且ROE>20%的消费股"
```
### Polygon.io — 美股替代数据通道(2026-05-13 新增)
**状态:** 2026-05-13 接入,需 API Key(Free tier: 5 calls/min)。
**⚠️ Free tier 局限:** 财务数据仅返回 2020 年前后记录,**最新季度数据不可用**。行情/K线/分红可用但数据年限受限。见 `skills/polygon-data/references/pitfalls.md`。
**适用市场:** 美股 ✅ A股 ❌ 港股 ❌ 加密货币 ✅
#### 配置
```bash
# 环境变量
export POLYGON_API_KEY="your_key_here"
# 申请地址
# https://polygon.io/dashboard/signup → Free/Basic/Starter/Pro 方案
```
#### 使用方法
```bash
# 查询行情
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type all
# 输出:JSON 到 ~/.polygon_data/output/
```
#### 在美股分析中的作用
| 数据类型 | 替代目标 | 优势 |
|---------|---------|------|
| 结构化财报 | yfinance 财报 | 标准GAAP字段,含SBC/回购 |
| 日K线 | yfinance history | 原生JSON格式,稳定 |
| 分红/拆分 | yfinance 分红 | 结构化字段 |
**美股数据获取优先级(更新后):**
| 优先级 | 数据源 | 适用场景 | 速度 | 可靠性 |
|--------|--------|---------|------|--------|
| 1 | **Polygon.io** | 美股行情/财报/K线/分红 | 1-3s | ✅ 结构化API稳定 |
| 2 | yfinance | 美股备用/快速查询 | 1-2s | ⚪ 偶有CAPTCHA |
| 3 | SEC EDGAR | 10-K文本(Business/Risk/MD&A) | 5-10s | ✅ 免费无限制 |
| 4 | Finnhub | Earnings Call转录 | 3-5s | ⚪ 免费tier有限 |
---
#### A股数据获取优先级
| 优先级 | 数据源 | 适用场景 | 速度 | 可靠性 |
|--------|--------|---------|------|--------|
| 1 | MX API(自然语言) | 快速查询行情/财务/股东 | 1-3s | ✅ 官方API非常稳定 |
| 2 | yfinance + Baostock | 5年财务报表历史趋势 | 10-30s | ⚪ A股缺SBC/回购 |
| 3 | Tushare | 前十大股东、分红明细 | 5-10s | ✅ 免费积分有限 |
| 4 | qt.gtimg.cn(腾讯) | 实时行情最终兜底(A/H/U) | 0.1s | ✅ 无反爬无需Key |
| 5 | Agent web search | 定性信息补充 | 不等 | ⚠️ 需手动校核 |
#### 已知问题与处理方式(2026-05-13 验证)
| 问题 | 原因 | 处理方式 |
|------|------|---------|
| push2.eastmoney.com 超时 | macOS/非大陆网络被屏蔽 | 改用 MX API 或 qt.gtimg.cn 兜底 |
| mx-data Python 脚本写 `/root/.openclaw/` | 默认路径硬编码 | 已修补到 `~/.mx_data/output/`;所有 5 个 mx-* 脚本同样已修补 |
| hermes skills install 不支持本地路径 | 只能接受 URL/hub ID | 手动 `cp -R` 到 `~/.hermes/skills/<name>/` |
| **环境变量在 terminal 子进程中不可见** | Hermes 过滤子进程环境,仅 `.env` 在 agent 启动时加载的变量可穿透 | 将 API Key 写入 profile 的 `.env`(如 `profile/cfo/.env`)后重启 agent;或在同一命令行 export 再执行脚本 |
> ⚠️ **环境变量穿透规则:** `terminal` tool 的子进程继承经过滤的环境。仅在 agent 启动时从 `.env` 加载的变量(MX_APIKEY, NOTION_API_KEY 等)在 terminal 中可见。直接用 `export` 设的变量仅在 agent 进程内有效。如需临时测试,用 `export KEY=val && python3 script.py` 单行命令。
## API Key 配置
所有 API Key 集中在 `.hermes/.env` 或对应 profile 的 `.env` 中:
```
# MX_APIKEY 在 profile 的 .env 中配置
MX_APIKEY=mkt_xxxxx
# 经典数据源的 key 在 config/api_keys.json
finnhub_api_key=your_key_here
fred_api_key=your_key_here
```
## 脚本:fetch_financials.py
### 用法
```bash
python3 scripts/fetch_financials.py --ticker AAPL # 美股(yfinance + SEC + Finnhub + FRED)
python3 scripts/fetch_financials.py --ticker AAPL --format markdown # 人类可读版
python3 scripts/fetch_financials.py --ticker 601398.SS # A 股(yfinance + baostock + FRED)
```
### 数据获取流水线(按顺序执行)
```
yfinance 采集 → 派生指标计算 → baostock补充(A股专属) → SEC EDGAR补充(美股专属)
→ Finnhub补充(美股专属) → FRED补充(通用) → 输出JSON
```
如需 A 股完整数据(行情+财务+新闻+股东),建议同时用 MX API 补充:
```python
# 在 fetch_financials.py 中或分析时手动调
from lib.mx_api import MXClient
client = MXClient()
snapshot = client.fetch_snapshot("605499") # 实时行情+估值
data = client.query("605499 东鹏饮料 2025年营业收入 净利润 毛利率")
```
### 输出 JSON 结构
```json
{
"ticker": "CROX",
"income_statement": [...], ← yfinance: 5年利润表
"balance_sheet": [...], ← yfinance: 5年资产负债表
"cash_flow": [...], ← yfinance: 5年现金流量表
"derived_metrics": {...}, ← 计算: FCF收益率/ROIC/毛利率趋势等
"baostock": { ← A股专属: 分红明细
"dividends": [...],
"latest_dividend_per_share": 0.3064
},
"sec_edgar": { ← 美股专属: 10-K文本章节
"cik": "1334036",
"latest_10k": {"period": "2025-12-31"},
"sections": {"business_description": "...", "risk_factors": "...", ...}
},
"finnhub": { ← 美股专属: Earnings Call + 新闻
"latest_earnings": {...},
"recent_news": [...],
"insider_transactions": [...]
},
"fred": { ← 通用: 宏观经济
"10y_treasury_yield_pct": 4.36,
"cpi_yoy_change_pct": 3.29
}
}
```
## A 股注意事项(已验证)
- SBC/回购:yfinance 上几乎全为 N/A(A股公司不单独披露)
- 分红:yfinance 字段不统一,需用 baostock 补充。**caution: 即使 yfinance 有 dividends_paid 字段(cashflow 表),A 股公司这字段也经常为空**,必须依赖 Baostock query_dividend_data() 或 MX API。
- 银行/保险资产负债表:科目映射不准,总负债可能偏低
- 股息率格式:A股返回百分比(4.06=4.06%),美股返回小数(0.0038=0.38%)
- `industry` 字段在 baostock 中当前返回为空(列名映射待修复)
- **accounts_receivable 字段缺失**:部分 A 股(纺织、消费行业常见)的 BS 中没有 accounts_receivable 键,被归入未映射的 other_current_assets。ARV 计算时需手动核对或跳过。
- **ROIC 脚本对 A 股返回 None**:analyze_roic_wacc_spread.py 对 A 股的特殊科目(递延所得税、专项储备)映射不完全,可能返回 None。回退到从 financials JSON 手动计算 NOPLAT/投入资本。
## 已知陷阱
### `~` 路径解析陷阱(cfo profile 专属)
在 cfo profile 下,`~` 展开为 `/Users/michael/.hermes/profiles/cfo/home/` 而非 `/Users/michael/`。
```python
os.path.expanduser("~/.hermes/skills/")
# → /Users/michael/.hermes/profiles/cfo/home/.hermes/skills/ # 错误!
# 实际脚本在: /Users/michael/.hermes/profiles/cfo/skills/ # 正确路径
```
**影响范围:** 所有在 terminal 中通过 `python3 ~/.hermes/skills/.../script.py` 执行的脚本。
**规则:** 引用 skill 自身文件用相对路径或脚本绝对路径。输出目录用 `os.path.expanduser("~/.polygon_data/output/")` 没问题(profile home 下可读写)。
### 银行股 FCF 陷阱
详见 `references/industry-data-pitfalls.md`:
- 银行股 FCF 陷阱(经营现金流 ≠ 可分配利润)
- 重资产企业 ROIC 偏低的常态 vs 边际回报率
- Web 搜索 CAPTCHA 拦截(Bing/Google 自动化搜索受限)
- 10-K HTML 章节格式不一致(ITEM 1 vs ITEM I vs Item 1)
### 中国互联网/科技公司现金数据遗漏
**问题:** yfinance 将中国公司的短期理财、定期存款放在 `Other Short Term Investments` 字段,而不是 `Short Term Investments`。脚本的 `BALANCE_MAP` 只映射了 `Cash And Cash Equivalents` 和 `Short Term Investments`,导致漏掉 ¥158亿(BZ案例:现金+短期投资实际 ¥199.5亿,只拿到 ¥41亿)。
### ADR 币种陷阱 — 不同币种间的估值比较
详见 `references/adr-currency-pitfalls.md`。
**核心规则:** 对于中国 ADR 股票,所有分析脚本从 `yf.info["currentPrice"]` 读取的是 **USD** 价格,而财务报表是 **CNY**。必须统一币种后才能进行估值比较(如净现金/市值、PE 等)。
**工作流中的强制检查:** 每次运行 `cross_validate.py` 时,C2 检查会检测净现金/市值 > 80%,这对 ADR 是**预期的、非致命的**。正确做法:
1. 收到 C2 错误 → 确认标的为 ADR → 执行 CNY 统一换算后继续
2. 非 ADR 标的的 C2 错误 → 才需要暂停修复
### 数据预警系统(`_warnings`)
`fetch_financials.py` 现在在 `derived_metrics` 中自动生成 `_warnings` 数组,覆盖以下场景:
| 触发条件 | 示例消息 | 含义 |
|---------|---------|------|
| 现金/市值 > 100% | ⚠️ 现金/市值=320% > 100%, 请检查币种一致性 | 现金和市值可能在不同币种(常见于中国ADR:现金在CNY,市值在USD) |
| ROIC > 200% | ⚠️ ROIC=810% > 200%, 投入资本过小, ROIC分母失真 | 净现金≈权益时ROIC计算无意义,需剔除冗余现金重算 |
| auto_total_cash 与 total_cash_and_st_investments 差异>1% | ⚠️ 现金合计不匹配: 自动汇总 vs yfinance合计 | 有未知现金类字段遗漏 |
| total_current_assets + total_non_current_assets ≠ total_assets | ⚠️ 资产负债表完整性: 差异>5% | 部分资产类别未覆盖 |
**使用方式:** `main()` 函数在计算派生指标后遍历并打印所有 `_warnings`。各 skill(financial-analysis/reasoning_review/buffett_review/critical_review)在读取 JSON 时也应检查此数组。
## 参考文档
- `references/mx-api-guide.md` — MX API(东方财富妙想)详细配置与使用指南
- `references/polygon-guide.md` — Polygon.io 美股数据源配置与使用指南
- `references/baostock-quarterly-timing.md` — Baostock 季度数据时效说明
- `references/baostock-guide.md` — Baostock 完整使用参考
- `references/adr-currency-pitfalls.md` — ADR 币种统一处理协议
- `references/industry-data-pitfalls.md` — 行业数据陷阱与处理
- `references/sec-edgar-guide.md` — SEC EDGAR 10-K 提取指南
- `references/finnhub-guide.md` — Finnhub API 使用指南
references/original-skills/how-to-use.md---
name: how-to-use
description: "投资分析系统使用指南 — 从数据采集到最终投资决策的全流程说明,覆盖5大Data Source、9个Python脚本、3种估值模型、4份独立报告"
version: 1.1.0
author: Hermes Agent
---
# 投资分析系统使用指南
## 系统概述
这是一个完整的 **A 股/美股基本面分析流水线**。从输入一个股票代码开始,系统自动完成以下全流程:
```
股票代码
│
▼
┌─ 第1层:数据采集 ──────────────────────────────────┐
│ 6个数据源 → 5年财报 + 行情 + 分红 + 内幕 + 宏观 │
└─────────────────────────────────────────────────────┘
│
▼
┌─ 第2层:8个分析脚本 ────────────────────────────────┐
│ ARV计算 / CapEx拆分 / ROIC-WACC / 利润率分解 │
│ 资本配置审计 / 内部人检查 / 会计异常检测 / 压力测试 │
└─────────────────────────────────────────────────────┘
│
▼
┌─ 第2.5层:交叉校验(安全阀)───────────────────────┐
│ 8项检查:现金完整性 / 币种 / ROIC / 利润质量 / 债务 │
└─────────────────────────────────────────────────────┘
│
▼
┌─ 第3层:AI生成4份独立报告 ─────────────────────────┐
│ 投资备忘录(9板块) + 思考链 + 批判性审视 + 巴菲特视角 │
└─────────────────────────────────────────────────────┘
│
▼
┌─ 第4层:Notion 上传 ──────────────────────────────┐
│ 子页面 + 版本管理 + 批次分隔标记 │
└─────────────────────────────────────────────────────┘
```
---
## 一句话用法
对 AI 说:**"分析一下 [股票代码] 的投资价值"**
例如:
- `分析一下 002327.SZ 的投资价值` → 富安娜
- `分析一下 XNET 的投资价值` → 迅雷
- `分析一下 DUOL 的投资价值` → Duolingo
---
## 第1层:数据采集(做了什么)
由 `financial-data-acquisition` skill 负责。采集器 `fetch_financials.py` 一次性从 6 个数据源拉取数据,合并为一个 JSON:
### 6 个数据源详解
| 数据源 | 获取什么 | 为什么需要 |
|--------|---------|-----------|
| **yfinance** | 5年利润表/资产负债表/现金流量表 + 实时股价/市值/PE/PB/Beta | **主力数据源**。所有财务分析的基础 |
| **Baostock** | A股分红(每股¥、除权日)+ 最新季度利润表 + 行业分类 | yfinance对A股分红字段映射不全,Baostock补充 |
| **Tushare** | 前十大股东及持股比例 | **新增数据源**。用于管理层评估——是否为家族企业、创始人持股比例 |
| **SEC EDGAR** | 美股10-K年报:业务描述(Item1)、风险因素(Item1A)、MD&A(Item7) | 用于定性分析——了解公司做什么、风险在哪 |
| **Finnhub** | 公司新闻 + 内部人交易记录 | 判断市场情绪、管理层是否在买卖自家股票 |
| **FRED** | 10年期国债利率、CPI、失业率、GDP | 用于计算 WACC(资本成本) |
### 代码格式
| 市场 | 格式 | 示例 |
|------|------|------|
| A股(深交所) | `xxxxxx.SZ` | `002327.SZ` |
| A股(上交所) | `xxxxxx.SS` | `600519.SS` |
| 美股(纳斯达克) | 代码 | `AAPL`、`XNET`、`DUOL` |
| 港股 | ❌ 暂不支持 | — |
### 输出
产出 `out/temp_data/financials_[代码].json`,包含:5年利润表/BS/CF + 公司信息 + 派生指标 + 各数据源补充数据。
---
## 第2层:8个分析脚本(算了什么)
由 `financial-analysis` skill 的 9 个 Python 脚本完成。每个脚本读取第1步产出的 JSON,计算特定指标后输出到 `out/temp_data/`。
| 脚本 | 计算内容 | 用在报告的哪里 |
|------|---------|-------------|
| `calculate_reproduction_value.py` | **ARV — 资产重置价值**:现金×100% + 应收×85% + 存货×80% + PP&E×150% + 商誉×0% | 第5节:格林沃尔德三要素 |
| `extract_maintenance_capex.py` | **维护性CapEx vs 增长性CapEx**:从10-K提取,或从D&A比例估算 | 第5节:EPV调整、FV再投资率 |
| `analyze_roic_wacc_spread.py` | **ROIC-WACC 差值**:NOPAT/投入资本 vs 国债+Beta×ERP | 第2节:护城河判定 |
| `margin_decomposition.py` | **利润率逐层分解**:GP→OP→NI,5年趋势+利润质量评分 | 第4节:财务体检 |
| `audit_capital_allocation.py` | **资本配置审计**:回购/分红/SBC/并购效率评分(A-F) | 第3节:管理层评估 |
| `insider_alignment_check.py` | **内部人一致性**:持股比例+SBC+Finnhub交易记录 | 第3节:管理层诚信 |
| `detect_accounting_anomalies.py` | **会计异常检测**:DSO天数/存货周转/商誉占比/OCF-NI背离 | 第4节:财务体检 |
| `stress_test_margin_of_safety.py` | **4种情景压力测试**:基准/保守/悲观/极端下的ARV+EPV+FV | 第6节:下行风险 |
| `cross_validate.py` | **8项数据交叉校验(安全阀)**:检查现金/币种/ROIC/资产负债表/利润质量/估值顺序/债务/SBC | **强制校验,exit 2则暂停** |
---
## 第2.5层:交叉校验(为什么是安全阀)
`cross_validate.py` 在报告生成前执行 8 项检查,防止数据错误进入报告:
| 检查 | 检查内容 | 发现哪些问题 |
|------|---------|-----------|
| C1 | 非活期现金占比 | 短期理财是否过多(BZ 的¥158亿教训) |
| C2 | 币种一致性 | 净现金/市值 > 80% 时触发(ADR币种混用) |
| C3 | ROIC合理性 | >200% 或 <-50%(分母扭曲) |
| C4 | 资产负债表完整性 | 分项合计 ≠ Total Assets |
| C5 | 利润质量 | OCF/NI < 0.5x 或 SBC/NI > 50% |
| C6 | 估值顺序 | PB < 0.5x(市场认为资产质量差) |
| C7 | 债务安全性 | 负债率 > 70% |
| C8 | SBC稀释 | SBC/OCF > 30% |
**exit code 2 = 有错误 → 暂停报告生成,需人工修复。**
---
## 第3层:AI 生成投资报告
**估值前强制加载方法论文档对照。** 三模型各自独立计算,不求和、不合并。
### 三种估值模型
#### 模型A:格林沃尔德三要素(主模型)
| 要素 | 含义 | 判据 |
|------|------|------|
| **ARV** 资产重置价值 | 把公司资产逐项重估后得到的地板价 | 市价 < ARV → 一级安全边际 |
| **EPV** 收益能力价值 | 假设零增长,当前盈利能力的现值 | 市价 < EPV → 二级安全边际 |
| **预期收益率** | 仅当 EPV > ARV 时计算 | 回报 > 资本成本 → 三级安全边际 |
#### 模型B:麦肯锡价值驱动模型
$$Value = \frac{NOPLAT \times (1 - g/ROIC)}{WACC - g}$$
内置自检:若 IR ≥ 1(g > ROIC),增长在销毁价值,退化为零增长模型。
#### 模型C:反向DCF
从市价反推市场隐含的增长率假设,与历史实际增速对比——验证市场是否过度乐观/悲观。
### 4 份报告
| 报告 | 内容 | 字数 | 视角 |
|------|------|------|------|
| **投资备忘录** | 9 板块完整分析 + 决策建议 | 2500-3500字 | 资深对冲基金分析师 + CFO |
| **思考链** | 数据溯源 + 计算步骤 + 假设验证 | 1200-2000字 | 技术型研究员 |
| **批判性审视** | 对抗式审查:假设压力测试 + 认知偏差 + 遗漏风险 | 1500-2500字 | 30年CFO + 首席风险官 |
| **巴菲特视角** | 8问快速过滤器 + 所有者收益 + 商业质量 | 1500-2000字 | 巴菲特/芒格思维 |
### 投资备忘录的 9 个板块
| 板块 | 内容 |
|------|------|
| **第1节:核心结论** | 一句话结论 + 市场共识(含Price Target) + 预期差(3项具体差异) |
| **第2节:商业模式** | 业务本质 + 多业务分拆表 + 业务痛点 + 单经济模型 + 护城河压力测试 |
| **第3节:管理层** | 三维评分(诚信/资本配置/所有者思维) + 股东收益率 + SBC稀释趋势表 |
| **第4节:财务体检** | 3年对比表 + 核心洞察(改善/恶化/矛盾) + 资产质量解剖 |
| **第5节:三模型估值** | 5a ARV(3年对比) + 5b EPV(7步+WACC敏感性) + 5c护城河验证 + 5d格林沃尔德判据 + 5e麦肯锡 + 5f反向DCF + 5g对比表 |
| **第6节:下行风险** | 3-5项风险(每项含触发条件+量化影响+历史参考+管理层应对) + 最可能伤害股价的情景 |
| **第7节:催化剂** | 4项左右潜在事件(可能性+影响+预期时间) |
| **第8节:三维扫描** | 质量/估值/催化剂 各0-10分(含理由) + 加权综合分 |
| **第9节:决策建议** | 操作建议 + 目标价 + 持有期限 + 买入逻辑 + 卖出条件 + 强制止损 |
---
## 第4层:Notion 上传
由 `notion-upload` skill 负责。上传到 **Report for Hermes** 页面:
1. 在父页面底部插入批次分隔标记:📌 `▶ [代码] — [日期] V[版本]`
2. 以子页面形式创建报告(默认在底部,Notion API 限制)
3. 页面标题含 emoji 前缀:📊 投资备忘录 / 🔍 思考链 / ⚠️ 批判性审视 / 🦅 巴菲特视角
```bash
cd ~/.hermes/skills/data-science/notion-upload/scripts
export $(grep NOTION_API_KEY ~/.hermes/.env)
python3 upload_to_notion.py --ticker [代码]
# 或指定文件:--files "file1.md,file2.md"
```
### 版本管理
| 规则 | 说明 |
|------|------|
| V1 | 每天对该股票的第一份分析 |
| V2+ | 同一天每重新生成一次 +1 |
| 跨天 | 重置回 V1 |
| 一致性 | 4 份报告使用同一版本号 |
---
## 各 Skill 的职责分工
| Skill | 路径 | 核心功能 |
|-------|------|---------|
| `financial-analysis` | `data-science/financial-analysis/` | **分析中枢**:9 个 Python 脚本 + 3 个估值方法论文档 + 9 板块报告模板 |
| `financial-data-acquisition` | `data-science/financial-data-acquisition/` | **数据采集**:`fetch_financials.py` 对接 6 个数据源 |
| `notion-upload` | `data-science/notion-upload/` | **Notion 上传**:`upload_to_notion.py` 子页面 + 分隔标记 |
| `buffett_review` | `data-science/buffett_review/` | **巴菲特视角**:8 问过滤器 + 所有者收益 + 护城河评估 |
| `reasoning_review` | `data-science/reasoning_review/` | **思考链**:数据溯源 + 计算过程 + 假设验证 |
| `critical_review` | `data-science/critical_review/` | **批判性审视**:对抗式审查 + 偏差诊断 + 压力测试 |
| `how-to-use` | `data-science/how-to-use/` | **使用指南**(本文件) |
## 数据目录
```
data-science/out/
├── temp_data/ ← 中间数据(JSON格式)
│ ├── financials_[代码].json ← 第1步产出
│ ├── calculate_reproduction_value_[代码].json ← 第2步ARV
│ ├── cross_validate_[代码].json ← 第2.5步校验
│ └── ...(每只股票10个JSON文件)
│
└── out_reports/ ← 最终报告(MD格式)
├── investment_memo_[代码]_...V1.md
├── reasoning_review_[代码]_...V1.md
├── critical_review_[代码]_...V1.md
├── buffett_review_[代码]_...V1.md
└── ...(每只股票4个MD文件)
```
## 注意事项速查
| 场景 | 注意 |
|------|------|
| **A 股** | 代码加 `.SZ` 或 `.SS` 后缀 |
| **中国 ADR** | yfinance 返回 CNY 财报 + USD 股价 → 统一币种 ×7.3 |
| **美股** | 自动获取 SEC 10-K + Finnhub 新闻/内幕 |
| **计算纪律** | 🚫 禁止脑算,所有估值必须用 Python |
| **数字格式** | 每条数字带币种前缀(¥/$)和单位(亿) |
| **估值纪律** | 三模型**独立**,不求和、不合并 |
| **SBC 处理** | 计算真实 FCF 时必须扣除 SBC |
| **交叉校验** | exit code 2 必须暂停,不可跳过 |
references/original-skills/jupyter-live-kernel.md---
name: jupyter-live-kernel
description: "Iterative Python via live Jupyter kernel (hamelnb)."
version: 1.0.0
author: Hermes Agent
license: MIT
metadata:
hermes:
tags: [jupyter, notebook, repl, data-science, exploration, iterative]
category: data-science
---
# Jupyter Live Kernel (hamelnb)
Gives you a **stateful Python REPL** via a live Jupyter kernel. Variables persist
across executions. Use this instead of `execute_code` when you need to build up
state incrementally, explore APIs, inspect DataFrames, or iterate on complex code.
## When to Use This vs Other Tools
| Tool | Use When |
|------|----------|
| **This skill** | Iterative exploration, state across steps, data science, ML, "let me try this and check" |
| `execute_code` | One-shot scripts needing hermes tool access (web_search, file ops). Stateless. |
| `terminal` | Shell commands, builds, installs, git, process management |
**Rule of thumb:** If you'd want a Jupyter notebook for the task, use this skill.
## Prerequisites
1. **uv** must be installed (check: `which uv`)
2. **JupyterLab** must be installed: `uv tool install jupyterlab`
3. A Jupyter server must be running (see Setup below)
## Setup
The hamelnb script location:
```
SCRIPT="$HOME/.agent-skills/hamelnb/skills/jupyter-live-kernel/scripts/jupyter_live_kernel.py"
```
If not cloned yet:
```
git clone https://github.com/hamelsmu/hamelnb.git ~/.agent-skills/hamelnb
```
### Starting JupyterLab
Check if a server is already running:
```
uv run "$SCRIPT" servers
```
If no servers found, start one:
```
jupyter-lab --no-browser --port=8888 --notebook-dir=$HOME/notebooks \
--IdentityProvider.token='' --ServerApp.password='' > /tmp/jupyter.log 2>&1 &
sleep 3
```
Note: Token/password disabled for local agent access. The server runs headless.
### Creating a Notebook for REPL Use
If you just need a REPL (no existing notebook), create a minimal notebook file:
```
mkdir -p ~/notebooks
```
Write a minimal .ipynb JSON file with one empty code cell, then start a kernel
session via the Jupyter REST API:
```
curl -s -X POST http://127.0.0.1:8888/api/sessions \
-H "Content-Type: application/json" \
-d '{"path":"scratch.ipynb","type":"notebook","name":"scratch.ipynb","kernel":{"name":"python3"}}'
```
## Core Workflow
All commands return structured JSON. Always use `--compact` to save tokens.
### 1. Discover servers and notebooks
```
uv run "$SCRIPT" servers --compact
uv run "$SCRIPT" notebooks --compact
```
### 2. Execute code (primary operation)
```
uv run "$SCRIPT" execute --path <notebook.ipynb> --code '<python code>' --compact
```
State persists across execute calls. Variables, imports, objects all survive.
Multi-line code works with $'...' quoting:
```
uv run "$SCRIPT" execute --path scratch.ipynb --code $'import os\nfiles = os.listdir(".")\nprint(f"Found {len(files)} files")' --compact
```
### 3. Inspect live variables
```
uv run "$SCRIPT" variables --path <notebook.ipynb> list --compact
uv run "$SCRIPT" variables --path <notebook.ipynb> preview --name <varname> --compact
```
### 4. Edit notebook cells
```
# View current cells
uv run "$SCRIPT" contents --path <notebook.ipynb> --compact
# Insert a new cell
uv run "$SCRIPT" edit --path <notebook.ipynb> insert \
--at-index <N> --cell-type code --source '<code>' --compact
# Replace cell source (use cell-id from contents output)
uv run "$SCRIPT" edit --path <notebook.ipynb> replace-source \
--cell-id <id> --source '<new code>' --compact
# Delete a cell
uv run "$SCRIPT" edit --path <notebook.ipynb> delete --cell-id <id> --compact
```
### 5. Verification (restart + run all)
Only use when the user asks for a clean verification or you need to confirm
the notebook runs top-to-bottom:
```
uv run "$SCRIPT" restart-run-all --path <notebook.ipynb> --save-outputs --compact
```
## Practical Tips from Experience
1. **First execution after server start may timeout** — the kernel needs a moment
to initialize. If you get a timeout, just retry.
2. **The kernel Python is JupyterLab's Python** — packages must be installed in
that environment. If you need additional packages, install them into the
JupyterLab tool environment first.
3. **--compact flag saves significant tokens** — always use it. JSON output can
be very verbose without it.
4. **For pure REPL use**, create a scratch.ipynb and don't bother with cell editing.
Just use `execute` repeatedly.
5. **Argument order matters** — subcommand flags like `--path` go BEFORE the
sub-subcommand. E.g.: `variables --path nb.ipynb list` not `variables list --path nb.ipynb`.
6. **If a session doesn't exist yet**, you need to start one via the REST API
(see Setup section). The tool can't execute without a live kernel session.
7. **Errors are returned as JSON** with traceback — read the `ename` and `evalue`
fields to understand what went wrong.
8. **Occasional websocket timeouts** — some operations may timeout on first try,
especially after a kernel restart. Retry once before escalating.
## Timeout Defaults
The script has a 30-second default timeout per execution. For long-running
operations, pass `--timeout 120`. Use generous timeouts (60+) for initial
setup or heavy computation.
references/original-skills/notion-upload.md---
name: notion-upload
description: "将投资报告上传到 Notion 父页面(Report for Hermes),以子页面形式展示,含批次分隔标记"
version: 1.0.0
author: Hermes Agent
---
# Notion Upload — 投资报告同步到 Notion
## 前置条件
- `NOTION_API_KEY` 配置在 `~/.hermes/.env` 中
- Notion 目标页面已对 Integration 开放访问
- 目标页面: `Report for Hermes` (ID: `3551dbfc72bb80a6849cdc1292697c14`)
## 触发条件
用户**明确要求**以下内容时触发此 skill(非默认行为):
- "上传到 notion"
- "同步到 notion"
- "把报告发到 notion"
## 用法
```bash
cd ~/.hermes/skills/data-science/notion-upload/scripts
# 上传指定股票的最新4份报告
export $(grep NOTION_API_KEY ~/.hermes/.env)
python3 upload_to_notion.py --ticker XNET
# 上传指定文件
python3 upload_to_notion.py --files "report1.md,report2.md"
```
## 工作流程
1. 读取 `out/out_reports/` 目录下的 MD 报告
2. `--ticker` 模式:各类型(备忘录/思考链/批判性审视/巴菲特)取最新版本
3. 在父页面底部插入分隔标记 `📌 ▶ [代码] — [日期] V[版本]`
4. 通过 `POST /pages` 创建子页面(默认在父页面底部)
5. 将报告内容逐 block 解析上传
## 页面标题映射
| 报告类型 | emoji | 标题前缀 |
|---------|-------|---------|
| `investment_memo` | 📊 | 投资备忘录 |
| `reasoning_trace` | 🔍 | 推理轨迹 |
| `reasoning_review` | 🔍 | 思考链 |
| `critical_review` | ⚠️ | 批判性审视 |
| `buffett_review` | 🦅 | 巴菲特视角 |
## ⚠️ HOME 目录扩展陷阱(macOS cfo profile)
**关键发现:** 在 cfo profile 环境中,`os.path.expanduser("~")` 返回 `/Users/michael/.hermes/profiles/cfo/home/`,而非 `/Users/michael/`。
这意味着脚本中的 `reports_dir = os.path.expanduser("~/.hermes/skills/...")` 实际解析为 `/Users/michael/.hermes/profiles/cfo/home/.hermes/skills/...`,而报告文件通常写在 `/Users/michael/.hermes/skills/...` 下。
**快速修复:** 将报告文件复制到 HOME 下的对应路径:
```bash
# 源: 实际报告位置
# 目标: $HOME/.hermes/skills/data-science/out/out_reports/
mkdir -p ~/.hermes/skills/data-science/out/out_reports
cp /Users/michael/.hermes/skills/data-science/out/out_reports/*CP代码* \
~/.hermes/skills/data-science/out/out_reports/
# 然后运行 upload_to_notion.py --ticker [代码]
```
**长期修复方案:** 将脚本中的 `os.path.expanduser("~")` 替换为硬编码路径,或使用 `os.environ.get('ORIGINAL_HOME', os.path.expanduser('~'))`。
## ⚠️ 维护陷阱:前缀同步
上传脚本内有 **3 处硬编码的报告类型前缀列表**(prefix filtering、type_config、emoji matching)。如果后续重命名任何 skill(如 reasoning_review→reasoning_xxx),必须同步更新 upload_to_notion.py 中所有 3 处。漏掉任意一处会导致 --ticker 模式找不到文件或 emoji 显示为默认 📄。
同步清单:
1. `all_matched` 的 startswith 过滤条件
2. `type_config` 字典的 key
3. emoji 匹配循环的 `for key in [...]`
## ⚠️ 已知修复:reasoning_trace 前缀
当前系统生成的文件前缀为 `reasoning_trace_`,但脚本默认只匹配 `reasoning_review_`。这是一个已知的命名不一致问题,已在以下 3 处追加修复:
1. `all_matched` 的 startswith tuple: 添加 `"reasoning_trace_"`(第 319 行附近)
2. `type_config` 字典: 添加 `"reasoning_trace": ("🔍", "推理轨迹")`(第 342 行附近)
3. emoji 匹配循环: 在 key list 中添加 `"reasoning_trace"`(第 369 行附近)
如果修复失效,请重新检查以上 3 处。临时绕过方式:`--files "/path/to/reasoning_trace_xxx.md"`
4. **报告文件名前缀** — 注意 `reasoning_trace_` 与 `reasoning_review_` 的差异:
- `reasoning_trace` 是实际生成的文件前缀(来自 SOUL.md 中的 `reasoning_trace` 命名)
- `reasoning_review` 是 skill 名称(来自 skill 目录名)
- 如果 --ticker 模式找不到 `reasoning_trace_*` 文件,请手动传入:`--files "/path/to/reasoning_trace_xxx.md"`
## 报告文件命名格式
```
[类型]_[股票代码]_[公司名]_[日期YYYYMMDD]_V[版本].md
```
## 路径说明
```
脚本: ~/.hermes/skills/data-science/notion-upload/scripts/upload_to_notion.py
报告源: ~/.hermes/skills/data-science/out/out_reports/
```
## 集成到 financial-analysis
在 `financial-analysis` 中完成报告生成后调用:
```bash
cd ~/.hermes/skills/data-science/notion-upload/scripts
export $(grep NOTION_API_KEY ~/.hermes/.env)
python3 upload_to_notion.py --ticker [代码]
```
references/original-skills/polygon-data.md---
name: polygon-data
description: "Polygon.io 金融数据采集器 — 美股行情/财报/K线/股息/拆分,作为 yfinance 的替代/补充数据源"
version: 1.0.0
author: Hermes Agent
env:
- POLYGON_API_KEY: "Polygon.io API Key (https://polygon.io/dashboard/signup)"
---
# Polygon.io 金融数据采集器
## 定位
Polygon.io 是美股(含期权、加密货币)的专业数据源。在 CFO 分析流程中,它作为 **yfinance 的替代/补充** 角色加入数据源矩阵。
## 与现存数据源的关系
```
美股分析数据源优先级:
🥇 Polygon.io ← 新增主力美股源(结构化财报+实时行情+K线)
↓ 不可用时
🥇 yfinance ← 现有美股源(兜底)
↓ 美股定性
🥈 SEC EDGAR ← 10-K文本/Risk/MD&A
🥈 Finnhub ← Earnings Call转录
```
## 前置条件
```bash
# 1. 注册获取 API Key
# https://polygon.io/dashboard/signup (Free tier: 5 calls/min)
# 2. 配置环境变量
export POLYGON_API_KEY="your_key_here"
# 3. 安装依赖
pip install requests
```
## API 端点总览
**⚠️ v3 财务端点不可用** — 实测 `/v3/reference/financials` 返回 404。所有财务数据走 v2 展平格式。
| 端点 | 频率限制(free) | 用途 |
|------|---------------|------|
| `/v2/aggs/ticker/{t}/prev` | 5/min | 上日收盘行情 |
| `/v2/aggs/ticker/{t}/range/1/day/{from}/{to}` | 5/min | 历史日K线 |
| **`/v2/reference/financials/{t}`** | 5/min | **结构化财报**(103+ 展平字段,含SBC) |
| `/v3/reference/tickers/{t}` | 5/min | 公司基本信息 |
| `/v2/reference/dividends/{t}` | 5/min | 分红历史(字段: amount/exDate/paymentDate) |
| `/v2/reference/splits/{t}` | 5/min | 拆股历史 |
## 用法
```bash
# 查询股票行情
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type quote
# 查询财报
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type financials --limit 5
# 查询历史K线
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type bars --from 2023-01-01 --to 2025-12-31
# 查询分红
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type dividends
# 批量查询
python3 ~/.hermes/skills/polygon-data/polygon_data.py --ticker AAPL --type all
```
## 输出结构
所有查询输出到 `~/.polygon_data/output/`,格式统一为 JSON + 终端预览。
## 加载 financial-data-acquisition
在 `financial-data-acquisition` 的 fetch_financials.py 中,Polygon 作为美股数据通道加入:
- 美股(.TO)时自动触发 Polygon 补充
- 输出至 `data["polygon"]`
- 覆盖 yfinance 可能缺失的字段:SBC、回购、分红明细
## 与 mx-data 的分工
| 市场 | 主力 | 备用 |
|------|------|------|
| A股 | mx-data | yfinance |
| 美股 | **Polygon.io** (新) | yfinance |
| 港股 | mx-data | yfinance |
| 加密货币 | Polygon.io | — |
references/original-skills/reasoning-review.md---
name: reasoning_review
description: "思考链展现 — 记录每份投资备忘录的完整推理过程:数据来源溯源、关键计算步骤、估值逻辑推导、判断依据,输出为可追溯的MD文档"
version: 2.0.0
author: Hermes Agent
---
# 思考链展现 — Reasoning Trace
## 核心纪律(重要 — 历史教训总结)
以下规则从2026-05-03 BZ现金数据错误和市值换算错误中总结,必须严格遵守:
1. **所有计算必须用 Python execute_code 或终端命令执行**,禁止在回复正文中脑算任何金额换算、比率或估值。\n2. **净现金/市值 > 80% 时自动触发币种一致性检查**:确认现金所在币种和市值币种是否一致。混用 CNY/USD 是此类异常最常见的原因。\n3. **每个数字必须附带币种前缀(¥/$)和亿级单位**。禁止裸数字。\n4. **数量级验证**:$6.2B ≈ ¥450亿(非¥45亿)。换算后检查数字位数(¥45亿 vs ¥450亿差10倍)。\n5. **三模型估值记录规则**:思考链中必须分别记录格林沃尔德(模型A)、超额回报折现(模型B)、反向DCF(模型C)三种方法的计算过程和输入参数,对应投资备忘录的5a-5g节。不得仅记录单一模型。
## 定位
此 skill 是 **financial-analysis 的伴随技能**。每次生成投资备忘录时,同步生成一份 `reasoning_review_[股票代码]_[公司名]_[日期]_V[版本].md`,记录从数据采集到最终结论的完整思考链,确保每个判断都可追溯、可复现、可质疑。
## 触发条件
- 每次调用 cfo-deep-dive 生成投资备忘录后**自动执行**
- 用户明确要求 "展示思考链" 或 "显示推理过程"
## 输出文件
写入 `~/.hermes/skills/data-science/out/out_reports/` 目录下,命名为 `reasoning_trace_[股票代码]_[公司名]_[日期]_V[版本].md`
版本号与同标的的 investment_memo 和 critical_review 保持一致。
## 输出结构(6部分)
### 第1部分:数据源溯源
记录每条数据的来源、获取方式、覆盖范围。
**模板:**
```markdown
## 1. 数据源溯源
| 数据类别 | 来源 | 覆盖年份 | 完整性 |
|---------|------|---------|-------|
| 利润表 | yfinance income_stmt | 2021-2025 | ✅ 完整 |
| 资产负债表 | yfinance balance_sheet | 2021-2025 | ⚠️ 部分科目缺失 |
| 现金流量表 | yfinance cashflow | 2021-2025 | ⚠️ SBC不含A股 |
| 红利 | Baostock query_dividend_data | 2022-2024 | ✅ 完整 |
| **最新季度财务指标** | **Baostock query_profit_data** | **T+0** | **✅ yfinance未更新时可用** |
| 宏观 | FRED get_series | 最新值 | ✅ |
| 内部交易 | Finnhub insider_transactions | 90天 | ✅ |
| 10-K章节 | SEC EDGAR submissions | 最新年度 | ✅ |
| **ARV重置价值** | calculate_reproduction_value.py | 最新年BS | ✅ |
| **维护性CapEx** | extract_maintenance_capex.py | 10-K/yfinance | ✅ |
| **ROIC-WACC Spread** | analyze_roic_wacc_spread.py | 5年数据 | ✅ |
| **利润率分解** | margin_decomposition.py | 5年利润表 | ✅ |
| **资本配置审计** | audit_capital_allocation.py | 5年现金流 | ✅ |
| **内部人一致性** | insider_alignment_check.py | 90天+最新 | ✅ |
| **会计异常检测** | detect_accounting_anomalies.py | 3-5年报表 | ✅ |
|| **安全边际压力测试** | stress_test_margin_of_safety.py | 4情景推演 | ✅ |
| **数据交叉校验** | cross_validate.py | 8项检查 | ✅ |
| **估值方法论** | financial-analysis references/ | 格林沃尔德/麦肯锡/反向DCF | ✅ |
```
**记录方式:** 每获取一个数据点,记录其来源函数、返回值的行数/长度、是否有缺失。
### 第2部分:关键财务数据摘录
从原始数据中提取用于决策的核心数字,展示纵向趋势。
**模板:**
```markdown
## 2. 关键财务数据摘录
### 营收趋势
| 年份 | 营收 | YoY增长 | 数据来源 |
|------|------|---------|---------|
| 2025 | ¥208.8亿 | +31.8% | inc:Total Revenue |
| 2024 | ¥158.4亿 | +40.6% | inc:Total Revenue |
| 2023 | ¥112.6亿 | +32.3% | inc:Total Revenue |
### 利润质量
- 毛利率:2023 43.1% → 2024 44.8% → 2025 44.9% → 持续改善
- 净利率:2023 18.1% → 2024 21.0% → 2025 21.2% → 改善
- 经营现金流/净利润:140%(2025)→ 利润质量高
```
### 第3部分:派生指标计算过程
展示 ROIC、FCF、股东收益率等关键指标的计算公式、输入值和计算结果。
**模板:**
```markdown
## 3. 派生指标计算过程
### ROIC
```
NOPAT = 营业利润¥51.9亿 × (1-15%税率) = ¥44.1亿
投入资本 = 有息负债¥70.6亿 + 权益¥94.2亿 - 现金¥56.8亿 = ¥108.0亿
ROIC = ¥44.1亿 / ¥108.0亿 = 40.8%
```
### WACC 设定依据
```
中国无风险利率 ≈ 2.0%(10年期国债)
Beta = 0.295(来自 yfinance info)
股权风险溢价 = 6%(A股消费品)
WACC = 2% + 0.295 × 6% = 3.8%(太低,不现实)
→ 实际采用 WACC = 8%(修正后),理由:
中国消费品企业的实际融资成本不低于6-8%
公司Beta偏低(0.295)但小市值溢价应上调
```
### 如果指标有争议,记录替代方案的差异
```
markdown
### 三模型交叉验证计算
#### 模型A:格林沃尔德三要素
```
# 第一步:ARV(3年对比,从资产负债表逐项计算)
ARV/股 = ¥XX (资产重置地板)
# 第二步:EPV(3年加权平均营业利润)
OP_t=¥XX亿×0.5, OP_t1=¥XX亿×0.3, OP_t2=¥XX亿×0.2
隐蔽利润加回 = SG&A×25%
NOPAT = (加权OP + 隐蔽利润) × (1-税率)
EPV = NOPAT / WACC
# 第三步:三要素独立判据
要素①: 市值 < ARV? [是/否]
要素②: 市值 < EPV? [是/否]
要素③: EPV > ARV? [是/否] → 是否触发预期收益率
```
#### 模型B:超额回报折现(McKinsey,非格林沃尔德)
```
FV = NOPAT × b × (ROIC-WACC) / WACC × 年金因子(n年)
内在价值(含FV) = ARV + EPV + FV
```
#### 模型C:反向DCF
```
从当前市值¥XX亿反推隐含增速 g1 ≈ X%
历史营收CAGR = X%
→ 市场[过于乐观/合理/过度悲观]
```
```
### 第4部分:定性判断依据与信息来源
记录无法量化的判断的依据:护城河判定理由、管理层评估依据、风险判断来源。
**模板:**
```markdown
## 4. 定性判断依据
### 护城河判定
判定结果:中等偏强
依据:
1. 渠道数据(从 baostock 查到的上市日期 2021-05-27,5年建成300万终端)
2. 毛利率稳定性(5年连续上升,说明定价权稳固)
3. ROIC-WACC 差值(30%——护城河企业的典型特征)
### 管理层评估
- 创始人林木勤持股~50%(从 yfinance info 获取)
- 无频繁更换CFO记录(需查年报)
- 分红率从54%升至78%——管理层在回报股东
### 竞争风险判断
- 红牛价格¥10 vs 东鹏¥5(查电商平台比价)
- 红牛在中国有商标纠纷(公开新闻)
- 东鹏市占率约40%且持续提升(年报披露)
```
### 第5部分:估值逻辑总览
展示三模型交叉验证的完整推导链条。
**模板:**
```markdown
## 5. 三模型估值逻辑总览
### 模型A:格林沃尔德三要素
```
ARV = ¥XX ← 基于:现金+短期投资¥XX亿 + 固定资产重置¥XX亿 + 其他资产¥XX亿
回收率假设:现金100%, 应收XX%, 存货XX%, PP&E 150%, 商誉0%
EPV = ¥XX ← 基于:3年加权OP¥XX亿 → NOPAT¥XX亿 / WACCXX%
隐蔽利润加回:SG&A×25%=¥XX亿
假设:零增长,当前利润可持续
① 市值 < ARV? [是/否] → 一级安全边际
② 市值 < EPV? [是/否] → 二级安全边际
③ EPV > ARV? [是/否] → 特许权存在?
决策树判定:[一级/二级/三级/无] 安全边际
```
### 模型B:超额回报折现(McKinsey,非格林沃尔德)
```
NOPAT=¥XX亿, ROIC=XX%, WACC=XX%, b=XX%, n=XX年
FV = ¥XX亿 × XX% × (XX%-XX%)/XX% × 年金(XX年) = ¥XX亿
含FV参考价值 = ARV+EPV+FV = ¥XX亿
```
### 模型C:反向DCF
```
市值¥XX亿, WACC=X%, 当前FCF=¥XX亿 → 隐含增速g1≈X% vs 历史实际增速X%
→ 市场[过于乐观/合理/过度悲观]
```
### 三模型交叉验证
| 模型 | 框架 | 判据 |
|------|------|------|
| 格林沃尔德(A) | 三要素独立vs市价 | [一级/二级/三级/无] |
| 超额回报(B) | ARV+EPV+FV求和 | XX%安全边际 |
| 反向DCF(C) | 隐含增速反推 | 隐含X% vs 历史X% |
### 假设变化推演
| 场景 | ARV | EPV | 决策树结论 |
|------|-----|-----|---------|
| 基准 | ¥XX | ¥XX | [一级/二级/三级/无] |
| 保守 | ¥XX | ¥XX | [一级/二级/三级/无] |
| 悲观 | ¥XX | ¥XX | [一级/二级/三级/无] |
```
### 第6部分:决策逻辑总结
一句话总结为什么买/不买/持有,以及后续需要跟踪哪些关键数据。
**模板:**
```markdown
## 6. 决策逻辑总结
### 为什么当前价格值得买?
- 格林沃尔德三段法显示33%安全边际
- ROIC=38%在A股消费品中顶级
- 营收增速32%为安全边际的缓冲垫
### 后续需跟踪的关键信号
1. 季度营收增速 → 低于15%为警示信号
2. 毛利率趋势 → 低于43%可能意味着竞争加剧
3. 资本开支回报 → 新建产能投产后ROIC是否维持30%+
4. 新品市占率 → 东鹏大咖/补水啦能否突破5%
### 如果判断错了,错在哪?
- 最可能的错误:高估了能量饮料的天花板
- 第二可能的错误:低估了红牛的反击力度
- 第三可能的错误:单一产品依赖比预期的更危险
```
references/original-skills/strategic-target-search.md--- name: strategic_target_search description: "批量筛选管道(原 financial-screener 精简版)— 注:分析脚本已迁移至 financial-analysis" version: 1.0.0 author: Hermes Agent --- # Financial Screening — 批量筛选管道 ## 说明 此 skill 原为 `financial-screener`(含筛选 + 分析脚本)。**分析脚本(8个深度分析 + 交叉校验 + 上传)已迁移至 `financial-analysis` skill。** 批量筛选管道脚本(`batch_fetcher.py` / `multi_stage_screener.py` / `pipeline.py`)目前已移除。如需恢复,可以从 git 历史找回。 ## 推荐工作流 当前主要使用 **逐只深度分析** 模式,完整流程在 `financial-analysis` skill 中: ```bash # 数据采集 python3 ~/.hermes/skills/data-science/financial-data-acquisition/scripts/fetch_financials.py --ticker [代码] # 运行8个分析脚本 + 交叉校验 cd ~/.hermes/skills/data-science/financial-analysis/scripts python3 calculate_reproduction_value.py --ticker [代码] python3 cross_validate.py --ticker [代码] # 生成报告(由 AI 完成)→ 上传 Notion python3 upload_to_notion.py --ticker [代码] ```
references/polygon-data/pitfalls.md# Polygon.io 实际使用陷阱(2026-05-14 实测)
## Free tier 核心限制
| 限制 | 实际表现 | 影响 |
|------|---------|------|
| **5 calls/min** | 硬限制,超限返回 429 | 全量分析需 ~45 秒(5 calls × 12秒间隔) |
| **财务数据年限** | Free tier 只返回 2020 年前后数据 | **不可用于当前季度分析!**必须切换到 yfinance 补充。 |
| **K线范围** | 最近 2 年 | 够用(5年需要 Basic+) |
**核心问题:Free tier 的财务数据严重过期。** 如 Intel 最新的 Polygon 财报记录停在 2020-03-31。对于需要 TTM/最新季度数据的分析,必须用 yfinance 补充。
## 关键 API 端点与字段
### 行情 (`/v2/aggs/ticker/{t}/prev`)
```
results[0]:
c: 昨收价
h: 当日最高
l: 当日最低
o: 开盘价
v: 成交量
t: 时间戳(ms)
```
### 财务数据 (`/v2/reference/financials/{ticker}`)
**展平结构**(非嵌套),核心字段:
| 字段名 | 含义 | EPA计算用途 |
|--------|------|-------------|
| `revenues` | 营收 | 利润率基准 |
| `costOfRevenue` | 营业成本 | 毛利率计算 |
| `grossProfit` | 毛利 | |
| `netIncome` | 净利润 | |
| `netCashFlowFromOperations` | 经营现金流 | FCF计算 |
| `capitalExpenditure` | 资本支出(负值) | FCF = OCF + CapEx |
| `freeCashFlow` | **自由现金流(预计算)** | 直接使用 |
| `shareBasedCompensation` | **SBC 股权激励** | 真实FCF调整 |
| `assets` | 总资产 | |
| `totalLiabilities` | 总负债 | |
| `shareholdersEquity` | 股东权益 | ROE计算 |
| `enterpriseValue` | 企业价值 | |
| `marketCapitalization` | 市值 | |
| `priceToEarningsRatio` | PE | |
| `priceToBookValue` | PB | |
| `weightedAverageShares` | 加权平均股数 | 每股计算 |
**注意:** 所有金额为原始美元值(如 `revenues: 58313000000` = $58.3B)。
### 分红 (`/v2/reference/dividends/{ticker}`)
```
results[]:
amount: 每股分红金额
exDate: 除权除息日
paymentDate: 付款日
recordDate: 登记日
```
**不要引用 `cash_amount`、`ex_dividend_date`** — 这些是旧版字段,当前 API 返回的字段名是 camelCase。
## 容错策略(Free tier 下的最佳实践)
```
美股分析:
[1] Polygon.io → type quote + type dividends + type bars
(行情/分红/K线可用,免费版有效)
[2] yfinance → financials + info
(财报必须用 yfinance,Polygon 免费版数据过时)
[3] combine: Polygon(行情) + yfinance(财报)
```
## 其他
- SBC 字段 (`shareBasedCompensation`) 是 Polygon 对比 yfinance 的核心优势,但在免费版中只能看到历史数据。最新 SBC 数据仍需 yfinance 补充。
- 脚本中的 `api_get()` 函数自动捕获 401(Key 无效)和 429(频率限制)。
references/polygon-data/polygon-guide.md# Polygon.io 数据源参考
> 美股结构化数据源,作为 yfinance 的替代/补充
> 版本:1.0 — 2026-05-13
## 数据能力矩阵
| 数据类 | Polygon REST 端点 | Free tier | 付费 tier |
|--------|------------------|-----------|-----------|
| 实时行情 | `/v2/aggs/ticker/{t}/prev` | ✅ 5/min | ✅ 无限制 |
| 日K线 | `/v2/aggs/ticker/{t}/range/1/day/{from}/{to}` | ✅ 5/min | ✅ 无限制 |
| 分钟K线 | `/v2/aggs/ticker/{t}/range/1/min/{from}/{to}` | ❌ | ✅ |
| 财报(10-K/Q) | `/v2/reference/financials/{t}` | ✅ 5/min | ✅ 无限制 |
| 公司信息 | `/v3/reference/tickers/{t}` | ✅ 5/min | ✅ 无限制 |
| 分红 | `/v2/reference/dividends/{t}` | ✅ 5/min | ✅ 无限制 |
| 拆股 | `/v2/reference/splits/{t}` | ✅ 5/min | ✅ 无限制 |
| 新闻 | `/v2/reference/news` | ❌ | ✅ |
> ⚠️ v3 财务端点不可用: `/v3/reference/financials` 返回 404。必须用 `/v2/reference/financials/{ticker}`。
## 财报数据结构(核心 v2 API)
Polygon `/v2/reference/financials/{ticker}` 返回展平 JSON(单层,103+ 字段),核心字段:
```
results[]
├── ticker # AAPL
├── period # QA(季度)/Q1/Q2/Q3/Q4/FY
├── calendarDate # 2024-12-31
├── reportPeriod # 报告期
├── revenues # 营业收入
├── costOfRevenue # 营业成本
├── grossProfit # 毛利
├── operatingExpenses # 经营费用
├── netIncome # 净利润
├── netCashFlowFromOperations # 经营现金流
├── capitalExpenditure # 资本支出
├── freeCashFlow # 自由现金流(预计算)
├── shareBasedCompensation # ✅ SBC 股权激励(yfinance缺这个)
├── assets # 总资产
├── totalLiabilities # 总负债
├── shareholdersEquity # 股东权益
├── enterpriseValue # 企业价值
├── marketCapitalization # 市值
├── priceToEarningsRatio # PE
├── priceToBookValue # PB
└── weightedAverageShares # 加权平均股数
```
## 分红数据结构
```
results[]
├── ticker # AAPL
├── amount # 每股分红金额 (0.27)
├── exDate # 除权日 (2026-05-11)
├── paymentDate # 付款日 (2026-05-14)
└── recordDate # 登记日 (2026-05-11)
```
## 关键陷阱
### 陷阱1: 环境变量隔离
Hermes Agent 的 `terminal` tool 对子进程进行环境变量过滤。脚本 `os.environ.get("POLYGON_API_KEY")` 在以下情况会拿到空值:
```
❌ export POLYGON_API_KEY=xxx → 仅在agent进程有效,terminal不可见
❌ 写在 ~/.bashrc 中 → terminal 不加载 shell init files
✅ 写在 profile 的 .env 中 → agent 启动时加载,terminal 可见
✅ 用同一命令行 export + 执行 → export POLYGON_API_KEY=xxx && python3 script.py
```
**修复方式:** 将 `POLYGON_API_KEY` 写入 `/Users/michael/.hermes/profiles/cfo/.env`,重启 agent。
### 陷阱2: `~` 路径解析
在 cfo profile 下,`~` 展开为 `/Users/michael/.hermes/profiles/cfo/home/` 而非 `/Users/michael/`。
```python
# 实际路径
os.path.expanduser("~/.polygon_data/output/")
# → /Users/michael/.hermes/profiles/cfo/home/.polygon_data/output/
```
脚本中不建议使用硬编码路径,用 `~/.polygon_data/output/` 可靠(profile home 下可读写)。
### 陷阱3: v3 API 不可用
- `/v3/reference/financials` → 404 (Not Found)
- `/v2/reference/financials/{ticker}` → 200 (OK)
- 如需未来切换到 v3,先测试端点可用性再迁移
### 陷阱4: Free tier 频率限制
5 calls/min。`--type all` 会调用 4 个端点(quote + financials + bars + dividends),可能需要 48s+ 才能完成。建议用 `sleep 12` 间隔或只查需要的类型。
## 与 yfinance 的关键差异
| 维度 | Polygon.io | yfinance |
|------|-----------|----------|
| **SBC字段** | ✅ `shareBasedCompensation` | ❌ 有时缺失 |
| **回购字段** | ✅ `treasury_stock_value` | ⚠️ 不完整 |
| **可靠性** | ✅ 官方API,无反爬 | ⚠️ 偶有CAPTCHA/变更 |
| **免费限制** | 5 calls/min | 无频率限制但非官方 |
| **数据结构** | 展平JSON(单层103字段) | 嵌套DataFrame |
## 已知限制
1. **Free tier 5 calls/min** — 批量分析时需串行调用
2. **不支持 A 股/港股** — 仅美股+加密货币
3. **财报映射略有延迟** — 10-K/Q 提交后通常 24-48h 上链
4. **付费方案较贵** — Basic $29/月起
scripts/config/api_keys.example.json{
"finnhub_api_key": "your_finnhub_api_key",
"fred_api_key": "your_fred_api_key",
"tushare_token": "your_tushare_token"
}
scripts/financial-analysis/analyze_roic_wacc_spread.py#!/usr/bin/env python3
"""
analyze_roic_wacc_spread.py — ROIC vs WACC Spread 分析器
格林沃尔德框架核心指标:ROIC - WACC 的差值决定了企业是否拥有护城河。
当 ROIC - WACC > 0 且持续 → 有护城河,增长创造价值
当 ROIC - WACC ≈ 0 → 无护城河,增长价值中性
当 ROIC - WACC < 0 → 在毁灭价值,增长越快越糟
用法:
python3 scripts/analyze_roic_wacc_spread.py --ticker AAPL
python3 scripts/analyze_roic_wacc_spread.py --ticker 605499.SS --peers "600519.SS,000858.SZ"
python3 scripts/analyze_roic_wacc_spread.py --ticker CROX --peers "DECK,NKE" --format markdown
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
os.makedirs(DATA_DIR, exist_ok=True)
# FRED API Key 路径
API_KEYS_PATH = os.path.join(BASE_DIR, "..", "config", "api_keys.json")
def _read_api_key(key_name):
"""从配置文件读取 API Key"""
try:
with open(API_KEYS_PATH) as f:
config = json.load(f)
return config.get(key_name, "")
except:
return ""
# ═══════════════════════════════════════════════════════
# 1. WACC 计算
# ═══════════════════════════════════════════════════════
def calc_wacc(ticker, sector="", country="US"):
"""
计算加权平均资本成本 WACC
WACC = E/(D+E) × Re + D/(D+E) × Rd × (1-T)
Re = Rf + Beta × ERP (CAPM)
Rd = Rf + 信用利差
"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
bs = t.balance_sheet
# ── 无风险利率 Rf ──
rf = _get_risk_free_rate(country)
# ── Beta ──
beta = info.get("beta")
if beta is None or beta == 0:
# 行业默认Beta
sector_betas = {
"Technology": 1.20, "Financial Services": 1.05,
"Consumer Defensive": 0.65, "Consumer Cyclical": 1.10,
"Healthcare": 0.85, "Energy": 1.25,
"Communication Services": 1.10, "Utilities": 0.55,
"Basic Materials": 1.00, "Real Estate": 0.80,
"Industrials": 1.05,
}
beta = sector_betas.get(sector, 1.0)
beta_source = "行业默认值"
else:
beta_source = "yfinance"
# ── 股权风险溢价 ERP ──
erp = _get_erp(country)
# ── 股权成本 Re ──
re = rf + beta * erp / 100 # ERP以百分比计,需转换
# ── 债务成本 Rd ──
# 从利润表中获取利息支出和负债
inc = t.income_stmt
total_debt = 0
interest_expense = 0
if bs is not None and not bs.empty:
years = sorted(bs.columns, reverse=True)
total_debt = abs(float(bs.loc["Total Debt"].iloc[0])) if "Total Debt" in bs.index else 0
if inc is not None and not inc.empty:
years = sorted(inc.columns, reverse=True)
if "Interest Expense" in inc.index:
interest_expense = abs(float(inc.loc["Interest Expense"].iloc[0]))
if total_debt > 0 and interest_expense > 0:
rd = interest_expense / total_debt
rd_source = "实际利率(利息支出/总负债)"
else:
rd = rf + 0.02 # 无负债或数据缺失:Rf + 2% 信用利差
rd_source = "估算(Rf + 2% 信用利差)"
# ── 税率 T ──
if inc is not None and not inc.empty and "Tax Provision" in inc.index and "Income Before Tax" in inc.index:
years = sorted(inc.columns, reverse=True)
tax_prov = abs(float(inc.loc["Tax Provision"].iloc[0]))
pretax = abs(float(inc.loc["Income Before Tax"].iloc[0]))
tax_rate = tax_prov / pretax if pretax > 0 else 0.20
else:
tax_rate = 0.20
tax_rate = min(tax_rate, 0.35) # 封顶35%
# ── 资本结构 ──
equity_val = info.get("marketCap", 0)
if equity_val is None:
equity_val = 0
debt_val = total_debt
total_capital = equity_val + debt_val
if total_capital > 0:
w_e = equity_val / total_capital
w_d = debt_val / total_capital
else:
w_e, w_d = 0.9, 0.1 # 默认90%股权
# ── WACC ──
wacc = w_e * re + w_d * rd * (1 - tax_rate)
return {
"rf_pct": round(rf * 100, 2),
"rf_source": "FRED(DGS10)" if country == "US" else "FRED(CH_10Y)",
"beta": round(beta, 3),
"beta_source": beta_source,
"erp_pct": erp,
"erp_source": "Damodaran 2025" if country == "US" else "修正ERP",
"re_pct": round(re * 100, 2),
"rd_pct": round(rd * 100, 2),
"rd_source": rd_source,
"tax_rate_pct": round(tax_rate * 100, 1),
"equity_weight_pct": round(w_e * 100, 1),
"debt_weight_pct": round(w_d * 100, 1),
"wacc_pct": round(wacc * 100, 2),
}
def _get_risk_free_rate(country="US"):
"""通过FRED获取无风险利率"""
api_key = _read_api_key("fred_api_key")
if api_key:
try:
from fredapi import Fred
fred = Fred(api_key=api_key)
series = "DGS10" if country == "US" else "CH_10Y"
data = fred.get_series(series)
if data is not None and not data.empty:
return float(data.iloc[-1]) / 100
except:
pass
# 回退
return 0.0436 if country == "US" else 0.020
def _get_erp(country="US"):
"""获取股权风险溢价"""
# 基于 Damodaran 2025 数据
if country == "US":
return 4.5 # 成熟市场
elif country == "China":
return 6.5 # 新兴市场溢价
else:
return 5.5
# ═══════════════════════════════════════════════════════
# 2. ROIC 计算
# ═══════════════════════════════════════════════════════
def calc_roic(ticker):
"""
计算 ROIC(投入资本回报率)
ROIC = NOPAT / Invested Capital
NOPAT = EBIT × (1 - Tax Rate)
Invested Capital = Total Debt + Total Equity - Cash
"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
inc = t.income_stmt
bs = t.balance_sheet
cf = t.cashflow
results = {}
# ── 获取多年的数据 ──
if inc is not None and not inc.empty:
years = sorted(inc.columns, reverse=True)[:5]
else:
return {"error": "无法获取利润表数据"}
yearly_data = []
for year in years:
year_str = str(year)[:10] if hasattr(year, 'strftime') else str(year)[:4]
try:
# EBIT
ebit = float(inc.loc["EBIT", year]) if "EBIT" in inc.index else 0
# 税率
tax_prov = float(inc.loc["Tax Provision", year]) if "Tax Provision" in inc.index else 0
pretax = float(inc.loc["Income Before Tax", year]) if "Income Before Tax" in inc.index else 0
tax_rate = tax_prov / pretax if pretax > 0 else 0.20
tax_rate = min(max(tax_rate, 0), 0.35)
# NOPAT
nopat = ebit * (1 - tax_rate)
# 投入资本(需要资产负债表)
if bs is not None and not bs.empty and year in bs.columns:
debt = float(bs.loc["Total Debt", year]) if "Total Debt" in bs.index else 0
equity = float(bs.loc["Stockholders Equity", year]) if "Stockholders Equity" in bs.index else 0
cash = float(bs.loc["Cash And Cash Equivalents", year]) if "Cash And Cash Equivalents" in bs.index else 0
else:
debt, equity, cash = 0, 0, 0
invested_capital = debt + equity - cash
roic = nopat / invested_capital if invested_capital > 0 else 0
# 营收
revenue = float(inc.loc["Total Revenue", year]) if "Total Revenue" in inc.index else 0
# 毛利率
gross = float(inc.loc["Gross Profit", year]) if "Gross Profit" in inc.index else 0
gross_margin = round(gross / revenue * 100, 1) if revenue else 0
yearly_data.append({
"year": year_str,
"revenue": revenue,
"ebit": ebit,
"nopat": round(nopat, 2),
"invested_capital": round(invested_capital, 2),
"roic_pct": round(roic * 100, 2),
"gross_margin_pct": gross_margin,
})
except Exception as e:
continue
if not yearly_data:
return {"error": "数据解析失败"}
# ── 加入维护性CapEx调整 ──
# 从现金流量表获取 CapEx 和 D&A
if cf is not None and not cf.empty:
cf_years = sorted(cf.columns, reverse=True)[:5]
for yd in yearly_data:
for cy in cf_years:
cy_str = str(cy)[:10] if hasattr(cy, 'strftime') else str(cy)[:4]
if cy_str == yd["year"]:
capex = abs(float(cf.loc["Capital Expenditure", cy])) if "Capital Expenditure" in cf.index else 0
da = float(cf.loc["Depreciation And Amortization", cy]) if "Depreciation And Amortization" in cf.index else 0
yd["total_capex"] = round(capex, 2)
yd["depreciation"] = round(da, 2)
yd["maintenance_capex"] = round(min(da, capex), 2) # 不超总CapEx
yd["growth_capex"] = round(capex - yd["maintenance_capex"], 2)
break
results["yearly"] = yearly_data
results["latest"] = yearly_data[0] if yearly_data else {}
results["avg_roic_3y_pct"] = round(
sum(y["roic_pct"] for y in yearly_data[:3]) / len(yearly_data[:3]), 2
) if len(yearly_data) >= 3 else results["latest"].get("roic_pct", 0)
# ── ROIC 质量判断 ──
latest_roic = results["latest"].get("roic_pct", 0)
avg_3y = results["avg_roic_3y_pct"]
trend = [y["roic_pct"] for y in yearly_data[:3]]
if latest_roic > 20:
results["quality"] = "优秀"
results["quality_detail"] = f"ROIC={latest_roic}%,远高于资本成本。企业拥有强大的护城河,增长将显著创造价值。"
elif latest_roic > 10:
results["quality"] = "良好"
results["quality_detail"] = f"ROIC={latest_roic}%,适度高于资本成本。有一定的竞争优势,但护城河不深。"
elif latest_roic > 5:
results["quality"] = "平庸"
results["quality_detail"] = f"ROIC={latest_roic}%,接近资本成本。没有明显护城河,增长不创造价值。"
else:
results["quality"] = "危险"
results["quality_detail"] = f"ROIC={latest_roic}%,低于资本成本。企业在毁灭价值,增长越快越糟。"
# 趋势判断
if len(trend) >= 3:
if trend[0] > trend[-1]:
results["trend"] = "改善中"
elif trend[0] < trend[-1]:
results["trend"] = "恶化中"
else:
results["trend"] = "稳定"
return results
# ═══════════════════════════════════════════════════════
# 3. Spread 分析与同业对比
# ═══════════════════════════════════════════════════════
def calc_spread(ticker, peer_list=None):
"""
计算 ROIC - WACC Spread
返回完整分析结果,含同业对比
"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
sector = info.get("sector", "")
country = "US" if info.get("country", "") == "United States" else "China" if info.get("country", "") == "China" else "US"
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"sector": sector,
"industry": info.get("industry", ""),
"country": info.get("country", ""),
"current_price": info.get("currentPrice"),
"market_cap": info.get("marketCap"),
}
# ── WACC ──
print(f"[spread] 计算 WACC...", file=sys.stderr)
wacc_data = calc_wacc(ticker, sector, country)
result["wacc"] = wacc_data
# ── ROIC ──
print(f"[spread] 计算 ROIC...", file=sys.stderr)
roic_data = calc_roic(ticker)
result["roic"] = roic_data
if "error" in roic_data:
result["error"] = roic_data["error"]
return result
# ── Spread ──
latest_roic = roic_data["latest"].get("roic_pct", 0)
wacc = wacc_data["wacc_pct"]
result["spread"] = {
"roic_pct": latest_roic,
"wacc_pct": wacc,
"spread_pct": round(latest_roic - wacc, 2),
"spread_assessment": _assess_spread(latest_roic - wacc),
}
# ── 同业对比 ──
if peer_list:
print(f"[spread] 同业对比: {peer_list}", file=sys.stderr)
peers = []
for p in peer_list:
p = p.strip()
if not p:
continue
try:
peer_roic = calc_roic(p)
peer_wacc = calc_wacc(p, sector, country)
peer_info = yf.Ticker(p).info or {}
peers.append({
"ticker": p.upper(),
"name": peer_info.get("shortName", p),
"roic_pct": peer_roic.get("latest", {}).get("roic_pct"),
"wacc_pct": peer_wacc.get("wacc_pct"),
"spread_pct": round(
(peer_roic.get("latest", {}).get("roic_pct", 0) or 0)
- peer_wacc.get("wacc_pct", 0), 2
) if peer_roic.get("latest", {}).get("roic_pct") else None,
"mkt_cap": peer_info.get("marketCap"),
})
except Exception as e:
print(f"[spread] ⚠️ {p} 获取失败: {e}", file=sys.stderr)
result["peers"] = peers
# 排名
all_spreads = [result["spread"]] + [{"ticker": p["ticker"], "spread_pct": p["spread_pct"]} for p in peers if p["spread_pct"] is not None]
all_spreads_sorted = sorted(all_spreads, key=lambda x: x.get("spread_pct", -999), reverse=True)
result["spread_ranking"] = [
{"rank": i+1, "ticker": s.get("ticker", ticker.upper()), "spread_pct": s.get("spread_pct")}
for i, s in enumerate(all_spreads_sorted)
]
# ── 综合判断 ──
result["verdict"] = _generate_verdict(result)
return result
def _assess_spread(spread):
"""评估 spread 大小"""
if spread > 20:
return "极宽护城河 ✅✅ — ROIC远超资金成本"
elif spread > 10:
return "宽护城河 ✅ — 明确的竞争优势"
elif spread > 5:
return "中等护城河 ⚡ — 有优势但非不可逾越"
elif spread > 0:
return "弱护城河 ⚠️ — 勉强覆盖成本"
elif spread > -5:
return "无护城河 ❌ — 低于资本成本"
else:
return "毁灭价值 🔴 — 增长越快越糟"
def _generate_verdict(result):
"""生成综合判断"""
spread = result.get("spread", {})
roic_data = result.get("roic", {})
wacc = result.get("wacc", {})
roic = spread.get("roic_pct", 0)
wacc_val = spread.get("wacc_pct", 0)
sp = spread.get("spread_pct", 0)
trend = roic_data.get("trend", "N/A")
quality = roic_data.get("quality", "N/A")
# 核心判断
if sp > 10:
action = "增长创造价值。企业有明确的护城河,应以 EPV + FV 为主进行估值。"
elif sp > 0:
action = "增长价值中性。企业有微弱优势,应以 EPV 为主估值,慎用 FV。"
else:
action = "增长毁灭价值。企业无护城河,应以 ARV 为主估值,仅考虑清算价值。"
beta_info = f"Beta={wacc.get('beta', 'N/A')} | Rf={wacc.get('rf_pct', 'N/A')}%"
return {
"roic_wacc_spread_pct": sp,
"trend": trend,
"quality": quality,
"action": action,
"summary": f"ROIC={roic}% - WACC={wacc_val}% = {sp:+}% | {_assess_spread(sp)} | {trend}趋势 | {beta_info}",
}
# ═══════════════════════════════════════════════════════
# 输出
# ═══════════════════════════════════════════════════════
def to_markdown(result):
lines = []
if "error" in result:
return f"# ❌ ROIC/WACC Spread 分析失败\n\n{result['error']}"
r = result
sp = r.get("spread", {})
ver = r.get("verdict", {})
wacc = r.get("wacc", {})
roic = r.get("roic", {})
lines.append(f"# ROIC vs WACC Spread 分析 — {r['ticker']}")
lines.append(f"")
lines.append(f"**{r.get('company_name','')}** | {r.get('sector','')} | {r.get('industry','')}")
lines.append(f"**当前股价:** {r.get('current_price','N/A')} | **市值:** {r.get('market_cap','N/A')}")
lines.append(f"")
lines.append(f"## 核心结论")
lines.append(f"")
lines.append(f"**{ver.get('summary','')}**")
lines.append(f"")
lines.append(f"> {ver.get('action','')}")
lines.append(f"")
# ── ROIC 趋势 ──
lines.append(f"## ROIC 趋势(近5年)")
lines.append(f"")
lines.append(f"| 年份 | 营收 | EBIT | NOPAT | 投入资本 | ROIC | 毛利率 |")
lines.append(f"|------|------|------|-------|---------|------|-------|")
def _fmt_currency(v):
if v is None or v == 0:
return "-"
if abs(v) >= 1e9:
return f"${v/1e9:.1f}B"
elif abs(v) >= 1e6:
return f"${v/1e6:.1f}M"
else:
return f"${v:,.0f}"
yd = r.get("roic", {}).get("yearly", [])
for y in yd:
lines.append(
f"| {y.get('year','')} | {_fmt_currency(y.get('revenue'))} "
f"| {_fmt_currency(y.get('ebit'))} | {_fmt_currency(y.get('nopat'))} "
f"| {_fmt_currency(y.get('invested_capital'))} "
f"| {y.get('roic_pct','')}% | {y.get('gross_margin_pct','')}% |"
)
avg_roic = roic.get("avg_roic_3y_pct", "")
lines.append(f"| **3年平均** | | | | | **{avg_roic}%** | |")
lines.append(f"")
lines.append(f"**趋势:** {roic.get('trend','N/A')} | **质量:** {roic.get('quality','N/A')}")
if yd:
latest = yd[0]
mc = latest.get("maintenance_capex")
gc = latest.get("growth_capex")
if mc is not None:
lines.append(f"**最新年资本开支拆分:** 总CapEx={_fmt_currency(latest.get('total_capex'))} | 维护性={_fmt_currency(mc)} | 增长性={_fmt_currency(gc) if gc else '$0'}")
lines.append(f"")
# ── WACC 明细 ──
lines.append(f"## WACC 计算明细")
lines.append(f"")
lines.append(f"| 参数 | 数值 | 来源 |")
lines.append(f"|------|------|------|")
lines.append(f"| 无风险利率 Rf | {wacc.get('rf_pct','')}% | {wacc.get('rf_source','')} |")
lines.append(f"| Beta | {wacc.get('beta','')} | {wacc.get('beta_source','')} |")
lines.append(f"| 股权风险溢价 ERP | {wacc.get('erp_pct','')}% | {wacc.get('erp_source','')} |")
lines.append(f"| **股权成本 Re** | **{wacc.get('re_pct','')}%** | CAPM |")
lines.append(f"| 债务成本 Rd | {wacc.get('rd_pct','')}% | {wacc.get('rd_source','')} |")
lines.append(f"| 税率 | {wacc.get('tax_rate_pct','')}% | 利润表 |")
lines.append(f"| 股权权重 | {wacc.get('equity_weight_pct','')}% | |")
lines.append(f"| 债务权重 | {wacc.get('debt_weight_pct','')}% | |")
lines.append(f"| **WACC** | **{wacc.get('wacc_pct','')}%** | |")
lines.append(f"")
# ── Spread 对决 ──
lines.append(f"## ROIC vs WACC 对决")
lines.append(f"")
sp_val = sp.get("spread_pct", 0)
bar_len = 40
roic_bar = int((sp.get("roic_pct", 0) / 50) * bar_len) if sp.get("roic_pct", 0) < 50 else bar_len
wacc_bar = int((sp.get("wacc_pct", 0) / 50) * bar_len) if sp.get("wacc_pct", 0) < 50 else bar_len
lines.append(f"```")
lines.append(f"ROIC [{'█' * roic_bar}{'░' * (bar_len - roic_bar)}] {sp.get('roic_pct', 0)}%")
lines.append(f"WACC [{'█' * wacc_bar}{'░' * (bar_len - wacc_bar)}] {sp.get('wacc_pct', 0)}%")
lines.append(f" {'─' * bar_len}")
gap = "+" if sp_val > 0 else ""
lines.append(f"Spread {gap}{sp_val}%")
lines.append(f"```")
lines.append(f"")
lines.append(f"**{_assess_spread(sp_val)}**")
lines.append(f"")
# ── 同业对比 ──
peers = r.get("peers", [])
if peers:
lines.append(f"## 同业对比")
lines.append(f"")
lines.append(f"| 公司 | ROIC | WACC | Spread | 市值 |")
lines.append(f"|------|------|------|-------|------|")
# 目标公司的行
lines.append(f"| **{r['ticker']} (目标)** | **{sp.get('roic_pct','N/A')}%** | **{sp.get('wacc_pct','N/A')}%** | **{sp.get('spread_pct','N/A')}%** | {_fmt_currency(r.get('market_cap'))} |")
for p in peers:
lines.append(f"| {p.get('ticker','')} | {p.get('roic_pct','N/A')}% | {p.get('wacc_pct','N/A')}% | {p.get('spread_pct','N/A')}% | {_fmt_currency(p.get('mkt_cap'))} |")
lines.append(f"")
# 排名
ranking = r.get("spread_ranking", [])
if ranking:
lines.append(f"### Spread 排名")
lines.append(f"")
for rank in ranking:
marker = " ← 目标" if rank.get("ticker") == r['ticker'] else ""
sp_r = rank.get("spread_pct", 0)
sp_s = f"+{sp_r}" if sp_r and sp_r > 0 else str(sp_r) if sp_r else "N/A"
lines.append(f"{rank['rank']}. **{rank['ticker']}**: {sp_s}%{marker}")
lines.append(f"")
# ── 在格林沃尔德框架中的含义 ──
lines.append(f"## 在格林沃尔德框架中的含义")
lines.append(f"")
lines.append(f"| Spread 范围 | 护城河 | 适用的估值方法 |")
lines.append(f"|-----------|--------|--------------|")
lines.append(f"| > 10% | ✅ 明确护城河 | EPV + FV(增长创造价值) |")
lines.append(f"| 0% ~ 10% | ⚠️ 弱护城河 | EPV 为主(增长中性) |")
lines.append(f"| < 0% | ❌ 无护城河 | ARV 为主(增长毁灭价值) |")
lines.append(f"")
# 建议
sp_note = ""
if sp_val > 10:
sp_note = "明确护城河企业。在估值中应计入 FV(增长溢价)。EPV中的再投资率(b)应使用增长性CapEx而非总CapEx。"
elif sp_val > 0:
sp_note = "弱护城河企业。估值应以 EPV 为主,FV 应取保守值。"
else:
sp_note = "无护城河企业。估值应以 ARV 为主,不应计入任何增长溢价。"
lines.append(f"**当前 {r['ticker']} Spread = {sp_val:+}%** → {sp_note}")
return "\n".join(lines)
# ═══════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="ROIC vs WACC Spread 分析器")
parser.add_argument("--ticker", type=str, required=True, help="目标股票代码")
parser.add_argument("--peers", type=str, default="", help="同业对比,逗号分隔")
parser.add_argument("--output", type=str, default=None, help="输出路径")
parser.add_argument("--format", type=str, default="json", choices=["json", "markdown"])
args = parser.parse_args()
peer_list = [p.strip() for p in args.peers.split(",") if p.strip()] if args.peers else None
result = calc_spread(args.ticker, peer_list)
if args.format == "markdown":
output = to_markdown(result)
else:
output = json.dumps(result, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"[spread] ✅ 已写入 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
scripts/financial-analysis/audit_capital_allocation.py#!/usr/bin/env python3
"""
audit_capital_allocation.py — 资本配置审计
像CFO一样审视管理层如何分配公司的每一块钱:
1. 经营现金流 → 钱从哪来
2. CapEx → 投了多少在增长 vs 维护
3. 收购 → 花了多少钱买公司
4. 回购 → 回购是否在低价时进行
5. 分红 → 分红率是否可持续
6. 还债 → 是否在优化资本结构
7. 冗余现金 → 账上是否有太多闲置资金
返回资本配置效率评分和质量判断。
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
def audit(ticker, years=5):
"""资本配置审计主函数"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
cf = t.cashflow
bs = t.balance_sheet
inc = t.income_stmt
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"audit_date": datetime.now().isoformat(),
}
if cf is None or cf.empty:
result["error"] = "无法获取现金流量表"
return result
cf_years = sorted(cf.columns, reverse=True)[:years]
# ── 逐年提取资本配置数据 ──
yearly = []
all_ocf, all_capex, all_acq, all_buyback, all_div, all_debt_pay = [], [], [], [], [], []
for y in cf_years:
year_str = str(y)[:10] if hasattr(y, 'strftime') else str(y)[:4]
try:
ocf = float(cf.loc["Operating Cash Flow", y]) if "Operating Cash Flow" in cf.index else 0
capex = abs(float(cf.loc["Capital Expenditure", y])) if "Capital Expenditure" in cf.index else 0
acq = abs(float(cf.loc["Purchase Of Business", y])) if "Purchase Of Business" in cf.index else 0
buyback = abs(float(cf.loc["Repurchase Of Capital Stock", y])) if "Repurchase Of Capital Stock" in cf.index else 0
div = abs(float(cf.loc["Cash Dividends Paid", y])) if "Cash Dividends Paid" in cf.index else 0
div2 = abs(float(cf.loc["Common Stock Dividend Paid", y])) if "Common Stock Dividend Paid" in cf.index else 0
debt_issue = float(cf.loc["Issuance Of Debt", y]) if "Issuance Of Debt" in cf.index else 0
debt_pay = abs(float(cf.loc["Repayment Of Debt", y])) if "Repayment Of Debt" in cf.index else 0
invest = abs(float(cf.loc["Purchase Of Investment", y])) if "Purchase Of Investment" in cf.index else 0
sbc = abs(float(cf.loc["Stock Based Compensation", y])) if "Stock Based Compensation" in cf.index else 0
except Exception as e:
continue
total_div = div or div2
total_used = capex + acq + buyback + total_div + debt_pay + invest
fcf = ocf - capex
record = {
"year": year_str,
"operating_cash_flow": ocf,
"free_cash_flow": fcf,
"capex": capex,
"acquisitions": acq,
"buybacks": buyback,
"dividends": total_div,
"debt_repaid": debt_pay,
"debt_issued": debt_issue,
"investments": invest,
"stock_based_comp": sbc,
"total_cash_used": total_used,
"fcf_to_ocf_pct": round(fcf / ocf * 100, 1) if ocf else 0,
"buyback_to_fcf_pct": round(buyback / fcf * 100, 1) if fcf else 0,
"div_to_fcf_pct": round(total_div / fcf * 100, 1) if fcf else 0,
}
# 每¥1经营现金流的分配
if ocf:
allocations = {}
items = [
("维护性CapEx", min(capex, abs(float(cf.loc["Depreciation And Amortization", y]))
if "Depreciation And Amortization" in cf.index else capex)),
("增长性CapEx", max(0, capex - (abs(float(cf.loc["Depreciation And Amortization", y]))
if "Depreciation And Amortization" in cf.index else 0))),
("收购", acq), ("回购", buyback), ("分红", total_div),
("还债", debt_pay), ("投资", invest), ("SBC(员工)", sbc),
]
record["per_dollar_allocation"] = {label: round(val / ocf * 100, 1) for label, val in items}
yearly.append(record)
all_ocf.append(ocf)
all_capex.append(capex)
all_acq.append(acq)
all_buyback.append(buyback)
all_div.append(total_div)
all_debt_pay.append(debt_pay)
result["yearly"] = yearly
# ── 配置效率评分 ──
result["score"] = _score_allocation(yearly, result)
result["verdict"] = _verdict_allocation(result["score"])
# ── 冗余现金分析 ──
if bs is not None and not bs.empty:
bs_year = sorted(bs.columns, reverse=True)[0]
cash = float(bs.loc["Cash Cash Equivalents And Short Term Investments", bs_year]) if "Cash Cash Equivalents And Short Term Investments" in bs.index else (float(bs.loc["Cash And Cash Equivalents", bs_year]) if "Cash And Cash Equivalents" in bs.index else 0)
debt = float(bs.loc["Total Debt", bs_year]) if "Total Debt" in bs.index else 0
ni = 0
if inc is not None and not inc.empty:
inc_year = sorted(inc.columns, reverse=True)[0]
ni = float(inc.loc["Net Income", inc_year]) if "Net Income" in inc.index else 0
mkt_cap = info.get("marketCap", 0) or 0
result["excess_cash"] = {
"cash": cash,
"total_debt": debt,
"net_cash": cash - debt,
"net_cash_to_mkt_cap_pct": round((cash - debt) / mkt_cap * 100, 1) if mkt_cap else 0,
"years_of_ni_in_cash": round(cash / ni, 1) if ni else 99,
}
return result
def _score_allocation(yearly, result):
"""资本配置效率评分 (0-10)"""
score = 10
details = []
if not yearly:
return {"score": 0, "details": ["无数据"]}
latest = yearly[0]
avg = {k: sum(y.get(k, 0) for y in yearly) / len(yearly) for k in ["buyback_to_fcf_pct", "div_to_fcf_pct"]}
# 扣分项1:SBC > 回购(管理层在净卖出)
sbc = latest.get("stock_based_comp", 0)
buyback = latest.get("buybacks", 0)
if sbc > buyback:
score -= 2
details.append(f"SBC(${sbc/1e6:.0f}M) > 回购(${buyback/1e6:.0f}M):管理层净卖出")
# 扣分项2:大量收购
acq_ratio = latest.get("acquisitions", 0) / latest.get("total_cash_used", 1)
if acq_ratio > 0.3:
score -= 1
details.append(f"收购占总现金使用{acq_ratio*100:.0f}%,关注收购回报率")
# 扣分项3:大量冗余现金
ec = result.get("excess_cash", {})
if ec.get("net_cash_to_mkt_cap_pct", 0) > 20:
score -= 1
details.append(f"净现金占市值{ec['net_cash_to_mkt_cap_pct']}%,过多闲置")
# 扣分项4:回购节奏差
# (简化:检查股价趋势,但实际上要复杂得多)
# 加分项1:持续回购+分红
if avg["buyback_to_fcf_pct"] + avg["div_to_fcf_pct"] > 50:
score += 1
details.append(f"持续高股东回报(回购{avg['buyback_to_fcf_pct']:.0f}%+分红{avg['div_to_fcf_pct']:.0f}%FCF)")
if avg["buyback_to_fcf_pct"] + avg["div_to_fcf_pct"] > 80:
score += 0.5
# 加分项2:FCF转化率高
if latest.get("fcf_to_ocf_pct", 0) > 60:
score += 0.5
details.append(f"FCF/OCF={latest['fcf_to_ocf_pct']}%,现金转化率优秀")
score = max(0, min(10, score))
return {"score": round(score, 1), "details": details}
def _verdict_allocation(score_obj):
score = score_obj.get("score", 0)
if score >= 8:
return {"grade": "A", "text": "资本配置优秀。管理层是优秀的资本配置者,每一块钱都用在刀刃上。"}
elif score >= 6:
return {"grade": "B", "text": "资本配置良好。整体合理,但有一两个领域可以改进。"}
elif score >= 4:
return {"grade": "C", "text": "资本配置平庸。有改进空间,特别关注减分项。"}
else:
return {"grade": "F", "text": "资本配置糟糕。管理层在毁灭股东价值,特别警惕SBC和盲目收购。"}
def to_markdown(result):
if "error" in result:
return f"# 资本配置审计失败\n\n{result['error']}"
lines = [f"# 资本配置审计 — {result['ticker']}", f"", f"**{result.get('company_name','')}**", f""]
lines.append(f"**评分:{result.get('score',{}).get('score','N/A')}/10** | 等级:{result.get('verdict',{}).get('grade','N/A')}")
lines.append(f"")
s = result.get("score", {})
for d in s.get("details", []):
lines.append(f"- {d}")
lines.append(f"")
lines.append(f"> {result.get('verdict',{}).get('text','')}")
lines.append(f"")
yearly = result.get("yearly", [])
if yearly:
lines.append(f"## 年度资本配置明细")
lines.append(f"")
lines.append(f"| 项目 | {' | '.join([y['year'] for y in yearly])} |")
lines.append(f"|------|{'|'.join(['---']*len(yearly))}|")
for key, label in [("operating_cash_flow", "经营现金流"), ("free_cash_flow", "自由现金流"),
("capex", "资本开支"), ("acquisitions", "收购"), ("buybacks", "回购"),
("dividends", "分红"), ("debt_repaid", "还债"), ("stock_based_comp", "SBC")]:
vals = [label]
for y in yearly:
v = y.get(key, 0)
vals.append(f"${v/1e6:.0f}M" if v else "-")
lines.append(f"| {' | '.join(vals)} |")
lines.append(f"")
ec = result.get("excess_cash", {})
if ec:
lines.append(f"## 冗余现金分析")
lines.append(f"")
lines.append(f"- 账面现金:${ec.get('cash',0)/1e6:.0f}M")
lines.append(f"- 总负债:${ec.get('total_debt',0)/1e6:.0f}M")
lines.append(f"- 净现金:${ec.get('net_cash',0)/1e6:.0f}M")
lines.append(f"- 净现金/市值:{ec.get('net_cash_to_mkt_cap_pct',0)}%")
lines.append(f"- 现金可覆盖净利润:{ec.get('years_of_ni_in_cash',0)}年")
return "\n".join(lines)
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--ticker", required=True)
parser.add_argument("--years", type=int, default=5)
parser.add_argument("--output", default=None)
parser.add_argument("--format", default="markdown", choices=["json", "markdown"])
args = parser.parse_args()
result = audit(args.ticker, args.years)
out = json.dumps(result, indent=2, default=str) if args.format == "json" else to_markdown(result)
if args.output:
with open(args.output, "w") as f:
f.write(out)
else:
print(out)
if __name__ == "__main__":
main()
scripts/financial-analysis/calculate_reproduction_value.py#!/usr/bin/env python3
"""
calculate_reproduction_value.py — 资产重置价值(ARV)计算器
基于 Bruce Greenwald 的 Reproduction Value 框架,逐项计算企业资产的重置成本。
用法:
python3 scripts/calculate_reproduction_value.py --ticker AAPL
python3 scripts/calculate_reproduction_value.py --ticker 601398.SS --format markdown
python3 scripts/calculate_reproduction_value.py --ticker CROX --output /tmp/arv.json
输出包含:
- 资产负债表的每一项分别估算重置成本
- 无形资产(品牌/专利)的特殊处理
- 冗余现金的扣除
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
os.makedirs(DATA_DIR, exist_ok=True)
def fetch_data(ticker_symbol):
"""从 yfinance 拉取资产负债表数据"""
import yfinance as yf
ticker = yf.Ticker(ticker_symbol)
info = ticker.info or {}
bs = ticker.balance_sheet
inc = ticker.income_stmt
data = {
"ticker": ticker_symbol.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"sector": info.get("sector", ""),
"industry": info.get("industry", ""),
"currency": info.get("financialCurrency", "USD"),
"current_price": info.get("currentPrice") or info.get("regularMarketPrice"),
"market_cap": info.get("marketCap"),
"enterprise_value": info.get("enterpriseValue"),
"shares_outstanding": info.get("sharesOutstanding"),
}
# 解析资产负债表(最近一年)
if bs is not None and not bs.empty:
years = sorted(bs.columns, reverse=True)
latest_year = years[0]
bs_items = {}
for idx in bs.index:
val = bs.loc[idx, latest_year]
if val is not None and val == val: # not NaN
bs_items[str(idx)] = float(val)
data["balance_sheet_year"] = str(latest_year)[:10] if hasattr(latest_year, 'strftime') else str(latest_year)[:10]
data["balance_sheet"] = bs_items
# 解析利润表(最近一年,用于品牌估值倍数)
if inc is not None and not inc.empty:
years = sorted(inc.columns, reverse=True)
latest_year = years[0]
for idx in inc.index:
if str(idx) == "Net Income":
data["net_income"] = float(inc.loc[idx, latest_year])
elif str(idx) == "Operating Income":
data["operating_income"] = float(inc.loc[idx, latest_year])
elif str(idx) == "EBIT":
data["ebit"] = float(inc.loc[idx, latest_year])
elif str(idx) == "Total Revenue":
data["revenue"] = float(inc.loc[idx, latest_year])
return data
# ═══════════════════════════════════════════════════════
# Reproduction Value 计算
# ═══════════════════════════════════════════════════════
def calc_reproduction_value(data):
"""
格林沃尔德 Reproduction Value 计算核心逻辑
逐项估算:
1. 现金及现金等价物 → 按账面值(100%)
2. 短期投资 → 按账面值(95%,扣除交易成本)
3. 应收账款 → 账面值 × 90%(回收折扣)
4. 存货 → 账面值 × 60-90%(按行业调整)
5. 固定资产(PP&E) → 账面值 × (1 + 通胀调整)
6. 无形资产(可辨认) → 账面值 × 50%(内部研发价值打折)
7. 商誉 → 0(不予重置)
8. 长期投资 → 账面值 × 80%
9. 其他资产 → 账面值 × 50%
"""
bs = data.get("balance_sheet", {})
sector = data.get("sector", "")
industry = data.get("industry", "")
result = {
"ticker": data["ticker"],
"company_name": data["company_name"],
"currency": data.get("currency", "USD"),
"balance_sheet_year": data.get("balance_sheet_year", ""),
"items": [],
"summary": {},
}
total_book = 0 # 账面值合计
total_repro = 0 # 重置值合计
# ── 辅助函数 ──
def add_item(name, book_value, repro_pct, repro_value, note=""):
nonlocal total_book, total_repro
book = book_value or 0
repro = repro_value or 0
total_book += abs(book)
total_repro += abs(repro)
result["items"].append({
"item": name,
"book_value": round(book, 2),
"reproduction_pct": repro_pct,
"reproduction_value": round(repro, 2),
"note": note,
})
def g(key):
return bs.get(key, 0)
# ── 1. 现金 ──
cash = g("Cash And Cash Equivalents") or g("Cash Equivalents") or 0
add_item("现金及现金等价物", cash, "100%", cash, "按账面值")
# ── 2. 短期投资 ──
st_inv = g("Short Term Investments") or 0
st_inv_val = st_inv * 0.95 if st_inv else 0
add_item("短期投资", st_inv, "95%", st_inv_val, "扣除交易成本")
# ── 3. 应收账款 ──
receivables = g("Net Receivables") or g("Accounts Receivable") or 0
# 行业调整:不同行业的应收可回收率不同
if "Technology" in sector or "Software" in industry:
rec_pct = 0.85
rec_note = "科技行业应收回收率较低"
elif "Bank" in sector or "Financial" in sector:
rec_pct = 0.95
rec_note = "金融行业应收质量较高"
else:
rec_pct = 0.90
rec_note = "通用应收回收率90%"
add_item("应收账款", receivables, f"{int(rec_pct*100)}%", receivables * rec_pct, rec_note)
# ── 4. 存货 ──
inventory = g("Inventory") or 0
if "Technology" in sector or "Semiconductor" in industry:
inv_pct = 0.60
inv_note = "科技存货贬值快"
elif "Consumer Defensive" in sector or "Food" in industry or "Beverage" in industry:
inv_pct = 0.85
inv_note = "快消品存货周转快"
elif "Energy" in sector or "Oil" in industry:
inv_pct = 0.75
inv_note = "大宗商品价格波动大"
elif "Retail" in sector or "Fashion" in industry:
inv_pct = 0.50
inv_note = "时尚/零售存货过季贬值严重"
else:
inv_pct = 0.80
inv_note = "通用存货80%"
add_item("存货", inventory, f"{int(inv_pct*100)}%", inventory * inv_pct, inv_note)
# ── 5. 固定资产(PP&E) ──
ppe = g("Property Plant Equipment") or g("PP&E Net") or g("Net PPE") or 0
if ppe:
# 制造业PP&E重置需加通胀溢价,但考虑折旧后的账面值可能已低于重置值
ppe_repro = ppe * 1.5 # 保守估计重置成本比账面值高50%
add_item("固定资产(PP&E)", ppe, "150%", ppe_repro, "重置成本溢价50%(通胀+技术升级)")
# ── 6. 商誉 ──
goodwill = g("Goodwill") or 0
if goodwill:
add_item("商誉(Goodwill)", goodwill, "0%", 0, "商誉不予重置——Greenwald原则")
# ── 7. 无形资产(不含商誉) ──
intangibles = g("Intangible Assets") or g("Intangibles") or 0
if intangibles:
# 可辨认无形资产(专利、版权等)按50%重置
add_item("无形资产(不含商誉)", intangibles, "50%", intangibles * 0.50, "内部研发形成的无形资产重置价值打折")
# ── 8. 长期投资 ──
lt_inv = g("Long Term Investments") or g("Investments") or 0
if lt_inv:
add_item("长期投资", lt_inv, "80%", lt_inv * 0.80, "长期投资按80%重置")
# ── 9. 其他流动资产 ──
oca = g("Other Current Assets") or 0
if oca:
add_item("其他流动资产", oca, "70%", oca * 0.70, "其他流动资产打7折")
# ── 10. 其他非流动资产 ──
# 计算"其他非流动资产" = 总资产 - 以上已列出的所有资产
total_assets = g("Total Assets") or 0
listed_assets = (
cash + st_inv + receivables + inventory + ppe + goodwill + intangibles + lt_inv + oca
)
other_assets = total_assets - listed_assets if total_assets > listed_assets else 0
if other_assets > 0:
add_item("其他未分类资产", other_assets, "50%", other_assets * 0.50, "其他资产按50%保守估算")
# ── 负债扣除 ──
total_liab = g("Total Liabilities Net Minority Interest") or g("Total Liabilities") or 0
add_item("总负债(扣除)", -total_liab, "100%", -total_liab, "负债按账面值100%扣除")
# ── 少数股东权益 ──
minority = g("Minority Interest") or 0
if minority:
add_item("少数股东权益(扣除)", -minority, "100%", -minority, "扣除少数股东权益")
# ── 摘要 ──
total_book = round(total_book, 2)
total_repro = round(total_repro, 2)
shares = data.get("shares_outstanding", 0)
mkt_cap = data.get("market_cap", 0)
ev = data.get("enterprise_value", 0)
result["summary"] = {
"total_book_value": total_book,
"total_reproduction_value": total_repro,
"reproduction_to_book_ratio": round(total_repro / total_book, 4) if total_book else 0,
"reproduction_per_share": round(total_repro / shares, 2) if shares else 0,
"current_price": data.get("current_price"),
"market_cap": mkt_cap,
"enterprise_value": ev,
"price_to_reproduction_ratio": round(mkt_cap / total_repro, 4) if total_repro and mkt_cap else None,
"premium_to_reproduction_pct": round((mkt_cap / total_repro - 1) * 100, 1) if total_repro and mkt_cap else None,
"shares_outstanding": shares,
}
return result
def to_markdown(result):
"""输出人类可读的 Markdown 版本"""
lines = []
s = result["summary"]
lines.append(f"# 资产重置价值(ARV) — {result['ticker']}")
lines.append(f"")
lines.append(f"**{result['company_name']}**")
lines.append(f"报表日期: {result['balance_sheet_year']}")
lines.append(f"")
lines.append(f"## 逐项重置计算")
lines.append(f"")
lines.append(f"| 科目 | 账面值 | 重置比例 | 重置价值 | 说明 |")
lines.append(f"|------|--------|---------|---------|------|")
for item in result["items"]:
bv = _fmt(item["book_value"], result["currency"])
rv = _fmt(item["reproduction_value"], result["currency"])
lines.append(f"| {item['item']} | {bv} | {item['reproduction_pct']} | {rv} | {item['note']} |")
lines.append(f"")
lines.append(f"## 汇总")
lines.append(f"")
lines.append(f"| 指标 | 数值 |")
lines.append(f"|------|------|")
lines.append(f"| 账面净资产合计 | {_fmt(s['total_book_value'], result['currency'])} |")
lines.append(f"| **重置价值合计** | **{_fmt(s['total_reproduction_value'], result['currency'])}** |")
lines.append(f"| 重置/账面比 | {s['reproduction_to_book_ratio']}x |")
lines.append(f"| 每股重置价值 | {_fmt_per_share(s['reproduction_per_share'], result['currency'])} |")
lines.append(f"| 当前股价 | {_fmt_per_share(s.get('current_price'), result['currency'])} |")
lines.append(f"| 股价/重置价值比 | {s.get('price_to_reproduction_ratio', 'N/A')}x |")
lines.append(f"| 市值溢价于重置值 | {s.get('premium_to_reproduction_pct', 'N/A')}% |")
lines.append(f"")
if s.get("premium_to_reproduction_pct") is not None:
prem = s["premium_to_reproduction_pct"]
if prem < 0:
lines.append(f"**结论:当前股价低于重置价值 {abs(prem):.1f}%——格林沃尔德意义上的安全边际存在。**")
elif prem < 30:
lines.append(f"**结论:当前股价溢价 {prem:.1f}% 于重置价值——估值合理偏低,有适度安全边际。**")
elif prem < 60:
lines.append(f"**结论:当前股价溢价 {prem:.1f}% 于重置价值——估值在市场合理区间,需继续看EPV和FV。**")
else:
lines.append(f"**结论:当前股价溢价 {prem:.1f}% 于重置价值——远高于ARV地板,价值主要来自盈利能力和增长预期。**")
return "\n".join(lines)
def _fmt(v, currency="USD"):
if v is None or v == 0:
return f"{currency}0"
try:
v = float(v)
symbol = "$" if currency == "USD" else "¥" if currency == "CNY" else currency + " "
if abs(v) >= 1e12:
return f"{symbol}{v/1e12:.2f}T"
elif abs(v) >= 1e9:
return f"{symbol}{v/1e9:.2f}B"
elif abs(v) >= 1e6:
return f"{symbol}{v/1e6:.2f}M"
else:
return f"{symbol}{v:,.0f}"
except:
return str(v)
def _fmt_per_share(v, currency="USD"):
if v is None:
return "N/A"
symbol = "$" if currency == "USD" else "¥" if currency == "CNY" else currency + " "
return f"{symbol}{float(v):.2f}"
# ═══════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="格林沃尔德资产重置价值(ARV)计算器")
parser.add_argument("--ticker", type=str, required=True,
help="股票代码 (e.g., AAPL, 601398.SS)")
parser.add_argument("--output", type=str, default=None,
help="输出文件路径")
parser.add_argument("--format", type=str, default="json",
choices=["json", "markdown"],
help="输出格式 (default: json)")
args = parser.parse_args()
print(f"[arv] 正在获取 {args.ticker} 的资产负债表...")
data = fetch_data(args.ticker)
if not data.get("balance_sheet"):
print(f"[arv] ❌ 未能获取资产负债表数据", file=sys.stderr)
sys.exit(1)
print(f"[arv] 计算重置价值...")
result = calc_reproduction_value(data)
print(f"[arv] 账面净资产: {_fmt(result['summary']['total_book_value'], data.get('currency','USD'))}")
print(f"[arv] 重置价值: {_fmt(result['summary']['total_reproduction_value'], data.get('currency','USD'))}")
print(f"[arv] 每股重置值: {_fmt_per_share(result['summary']['reproduction_per_share'], data.get('currency','USD'))}")
prem = result['summary'].get('premium_to_reproduction_pct', None)
if prem is not None:
print(f"[arv] 市值溢价: {prem:.1f}%")
if args.format == "markdown":
output = to_markdown(result)
else:
output = json.dumps(result, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"[arv] ✅ 已写入 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
scripts/financial-analysis/cross_validate.py#!/usr/bin/env python3
"""
cross_validate.py — 报告生成前的数据交叉校验
在投资备忘录生成之前运行此脚本,检查所有数据的完整性、一致性和合理性。
如有任何检查未通过(非 warnings),报告生成应暂停并提示用户修正。
用法:
python3 cross_validate.py --ticker BZ --data ../data/financials_BZ.json
python3 cross_validate.py --ticker BZ --data ../data/financials_BZ.json --verbose
返回码:
0 = 全部通过(可安全生成报告)
1 = 有警告(可生成报告但需注意标注)
2 = 有错误(应暂停报告生成)
输出:打印验证摘要到 stdout,同时写入 ../data/validation_[代码].json
"""
import argparse
import json
import os
import sys
# ── 检查项权重 ──
CRITICAL = "CRITICAL" # 必须修复才能继续
WARNING = "WARNING" # 应在报告中注明
INFO = "INFO" # 仅供参考
def load_data(data_path):
with open(data_path) as f:
return json.load(f)
def _fmt(v):
"""格式化大数字为可读形式"""
if v is None:
return "N/A"
v = float(v)
if abs(v) >= 1e8:
return f"¥{v/1e8:.1f}亿" if v < 1e12 else f"${v/1e9:.2f}B"
if abs(v) >= 1e4:
return f"¥{v/1e4:.0f}万"
return f"{v:.0f}"
# ═══════════════════════════════════════════════════════
# 校验函数
# ═══════════════════════════════════════════════════════
def check_cash_completeness(data):
"""
C1: 现金类科目完整性
检查 total_cash_and_st_investments 是否远大于 cash_and_equivalents
"""
bs_list = data.get("balance_sheet", [])
if not bs_list:
return []
results = []
latest = bs_list[0]
cash_only = latest.get("cash_and_equivalents") or 0
total_cash = (latest.get("total_cash_and_st_investments") or
cash_only + (latest.get("other_short_term_investments") or 0))
if total_cash == 0:
return [{"check": "C1", "level": WARNING, "message": "现金数据为空,可能未获取到资产负债表"}]
# 如果 total_cash 远大于 cash_only,说明有大量现金不在活期账户中
if cash_only > 0 and total_cash > cash_only * 1.5:
pct = (total_cash - cash_only) / total_cash * 100
return [{
"check": "C1",
"level": WARNING,
"message": (
f"非活期现金占现金总额的 {pct:.0f}% "
f"(现金+等价物={_fmt(cash_only)}, "
f"现金+短期投资合计={_fmt(total_cash)})。"
f"请确认这些短期投资的真实流动性—"
f"理财产品/定存赎回是否有锁定期限制?"
)
}]
return [{"check": "C1", "level": INFO, "message": f"现金类科目完整: {_fmt(total_cash)}"}]
def check_currency_consistency(data):
"""
C2: 币种一致性检查
净现金 vs 市值 — 如果比值异常高,可能是币种不匹配
"""
bs_list = data.get("balance_sheet", [])
info = data.get("info", {})
if not bs_list:
return []
latest = bs_list[0]
total_cash = (latest.get("total_cash_and_st_investments") or
(latest.get("cash_and_equivalents") or 0) +
(latest.get("other_short_term_investments") or 0))
debt = latest.get("total_debt") or 0
net_cash = total_cash - debt
market_cap = info.get("market_cap")
currency = info.get("currency", "?")
results = []
if market_cap and market_cap > 0 and net_cash > 0:
ratio = net_cash / market_cap
if ratio > 0.8:
results.append({
"check": "C2",
"level": CRITICAL,
"message": (
f"净现金/市值 = {ratio*100:.0f}% > 80%, "
f"极可能有币种不匹配。"
f"资产负债表中现金标注为 {currency},"
f"但市值通常为 USD。请统一币种后重算。"
f"净现金={_fmt(net_cash)}, 市值=${market_cap/1e9:.2f}B"
)
})
elif ratio > 0.5:
results.append({
"check": "C2",
"level": WARNING,
"message": (
f"净现金/市值 = {ratio*100:.0f}%, "
f"现金占市值比例较高。请确认币种一致性(现金在{currency} vs 市值在USD)。"
)
})
else:
results.append({
"check": "C2",
"level": INFO,
"message": f"净现金/市值 = {ratio*100:.0f}%, 在合理范围内"
})
return results
def check_roic_sanity(data):
"""
C3: ROIC 合理性
如果 ROIC > 200%,说明分母(投入资本)极小,可能因净现金≈权益
"""
metrics = data.get("derived_metrics", {})
roic = metrics.get("roic_pct")
if roic is None:
return [{"check": "C3", "level": WARNING, "message": "ROIC 未计算,无法检查"}]
if roic > 200:
return [{
"check": "C3",
"level": WARNING,
"message": (
f"ROIC = {roic:.0f}% > 200%, "
f"因净现金≈权益导致投入资本过小,ROIC 分母失真。"
f"报告的 ROIC 不代表真实的业务投资回报率。"
f"建议:使用剔除冗余现金后的 tangible ROIC"
)
}]
if roic < -50:
return [{
"check": "C3",
"level": WARNING,
"message": f"ROIC = {roic:.0f}%, 大幅为负,企业可能在毁损价值"
}]
return [{"check": "C3", "level": INFO, "message": f"ROIC = {roic:.1f}%, 在合理范围内"}]
def check_balance_sheet_integrity(data):
"""
C4: 资产负债表完整性
总资产 ≈ 流动资产 + 非流动资产
"""
bs_list = data.get("balance_sheet", [])
if not bs_list:
return []
latest = bs_list[0]
ta = latest.get("total_assets")
ca = latest.get("total_current_assets")
nca = latest.get("total_non_current_assets")
if ta and ca and nca and ta > 0:
expected = ca + nca
diff_pct = abs(expected - ta) / ta * 100
if diff_pct > 5:
return [{
"check": "C4",
"level": WARNING,
"message": (
f"资产负债表分项合计({_fmt(expected)}) "
f"与 total_assets({_fmt(ta)}) 差异 {diff_pct:.0f}%。"
f"可能存在未映射的大额科目。"
)
}]
equity = latest.get("shareholders_equity")
liabilities = latest.get("total_liabilities")
if ta and equity and liabilities:
calc_equity = ta - liabilities
diff_pct = abs(calc_equity - equity) / abs(equity) * 100 if equity else 0
if diff_pct > 1:
return [{
"check": "C4",
"level": WARNING,
"message": (
f"权益(资产-负债={_fmt(calc_equity)}) "
f"与 shareholders_equity({_fmt(equity)}) 差异 {diff_pct:.1f}%。"
f"可能存在少数股东权益等科目。"
)
}]
return [{"check": "C4", "level": INFO, "message": "资产负债表结构完整"}]
def check_earnings_quality(data):
"""
C5: 利润质量 — 现金流 vs 净利润
"""
cf_list = data.get("cash_flow", [])
is_list = data.get("income_statement", [])
if not cf_list or not is_list:
return []
results = []
for i in range(min(3, len(cf_list), len(is_list))):
ocf = cf_list[i].get("operating_cash_flow")
ni = is_list[i].get("net_income")
sbc = cf_list[i].get("stock_based_compensation")
year = cf_list[i].get("fiscal_year", str(i))
if ocf and ni and ni > 0:
ratio = ocf / ni
if ratio < 0.5:
results.append({
"check": "C5",
"level": WARNING,
"message": f"{year}年 OCF/净利润 = {ratio:.1f}x < 0.5x, 利润含金量低,有大量应计项"
})
elif ratio > 3:
results.append({
"check": "C5",
"level": INFO,
"message": f"{year}年 OCF/净利润 = {ratio:.1f}x, 现金流远高于利润(可能因预收/营运资本释放)"
})
if sbc and ni and ni > 0:
sbc_ratio = abs(sbc) / ni
if sbc_ratio > 0.5:
results.append({
"check": "C5",
"level": WARNING,
"message": f"{year}年 SBC/净利润 = {sbc_ratio*100:.0f}%, SBC稀释严重,报告净利润被大幅高估"
})
if not results:
results.append({"check": "C5", "level": INFO, "message": "利润质量未发现异常"})
return results
def check_valuation_order(data):
"""
C6: 估值大小顺序检查
通常: ARV ≤ EPV ≤ 市值(或反之也可接受)
如果市场价 < ARV,说明市场认为公司清盘也值这个价
"""
metrics = data.get("derived_metrics", {})
info = data.get("info", {})
price = info.get("current_price")
book_per_share = None
bs_list = data.get("balance_sheet", [])
if bs_list:
equity = bs_list[0].get("shareholders_equity")
shares = info.get("shares_outstanding")
if equity and shares and shares > 0:
book_per_share = equity / shares
results = []
if price and book_per_share:
if price < book_per_share * 0.5:
results.append({
"check": "C6",
"level": WARNING,
"message": (
f"股价 ${price:.2f} < PB/2 = ${book_per_share/2:.2f}, 市净率 < 0.5x。"
f"市场认为资产质量堪忧。请检查资产负债表是否有大额商誉或不良资产。"
)
})
if not results:
results.append({"check": "C6", "level": INFO, "message": "估值顺序无异常"})
return results
def check_debt_safety(data):
"""
C7: 债务安全性
检查总负债/总资产、有息负债率
"""
bs_list = data.get("balance_sheet", [])
if not bs_list:
return []
latest = bs_list[0]
ta = latest.get("total_assets")
tl = latest.get("total_liabilities")
debt = latest.get("total_debt")
equity = latest.get("shareholders_equity")
results = []
if ta and tl and ta > 0:
ratio = tl / ta
if ratio > 0.7:
results.append({
"check": "C7",
"level": WARNING,
"message": f"资产负债率 = {ratio*100:.0f}%, 杠杆偏高"
})
elif ratio < 0.1:
results.append({
"check": "C7",
"level": INFO,
"message": f"资产负债率 = {ratio*100:.0f}%, 非常保守的资本结构"
})
if debt and equity and equity > 0:
d_e = debt / equity
if d_e > 2:
results.append({
"check": "C7",
"level": WARNING,
"message": f"有息负债/权益 = {d_e:.1f}x, 杠杆偏高"
})
if not results:
results.append({"check": "C7", "level": INFO, "message": "债务结构安全"})
return results
def check_sbc_impact(data):
"""
C8: SBC 稀释影响
"""
cf_list = data.get("cash_flow", [])
is_list = data.get("income_statement", [])
if not cf_list or not is_list:
return []
results = []
for i in range(min(3, len(cf_list), len(is_list))):
sbc = cf_list[i].get("stock_based_compensation")
ocf = cf_list[i].get("operating_cash_flow")
rev = is_list[i].get("revenue")
year = cf_list[i].get("fiscal_year", str(i))
if sbc and ocf and ocf > 0:
sbc_ocf = abs(sbc) / ocf
if sbc_ocf > 0.3:
results.append({
"check": "C8",
"level": WARNING,
"message": f"{year}年 SBC/经营现金流 = {sbc_ocf*100:.0f}%, 员工激励大量以股权支付,真实利润被高估"
})
if sbc and rev and rev > 0:
sbc_rev = abs(sbc) / rev
if sbc_rev > 0.15:
results.append({
"check": "C8",
"level": INFO,
"message": f"{year}年 SBC/营收 = {sbc_rev*100:.1f}%, 高于典型水平(通常 < 10%)"
})
if not results:
results.append({"check": "C8", "level": INFO, "message": "SBC 影响在可接受范围内"})
return results
# ═══════════════════════════════════════════════════════
# 主函数
# ═══════════════════════════════════════════════════════
ALL_CHECKS = [
("C1 现金完整性", check_cash_completeness),
("C2 币种一致性", check_currency_consistency),
("C3 ROIC合理性", check_roic_sanity),
("C4 资产负债表完整性", check_balance_sheet_integrity),
("C5 利润质量", check_earnings_quality),
("C6 估值顺序", check_valuation_order),
("C7 债务安全性", check_debt_safety),
("C8 SBC稀释影响", check_sbc_impact),
]
def main():
parser = argparse.ArgumentParser(description="报告生成前数据交叉校验")
parser.add_argument("--ticker", required=True, help="股票代码")
parser.add_argument("--data", required=True, help="financials JSON 路径")
parser.add_argument("--verbose", action="store_true", help="详细输出")
args = parser.parse_args()
data_path = os.path.abspath(args.data)
if not os.path.exists(data_path):
print(f"[cross_validate] ❌ 未找到数据文件: {data_path}", file=sys.stderr)
sys.exit(2)
data = load_data(data_path)
print(f"╔═══════════════════════════════════════════╗")
print(f"║ 交叉校验报告 — {args.ticker:<12s} ║")
print(f"╚═══════════════════════════════════════════╝")
print()
all_items = []
errors = 0
warnings = 0
for check_name, check_fn in ALL_CHECKS:
try:
items = check_fn(data)
except Exception as e:
items = [{"check": check_name.split()[0], "level": WARNING, "message": f"校验执行异常: {e}"}]
for item in items:
level = item.get("level", INFO)
msg = item.get("message", "")
all_items.append(item)
if level == CRITICAL:
print(f" ❌ [{item['check']}] {msg}")
errors += 1
elif level == WARNING:
print(f" ⚠️ [{item['check']}] {msg}")
warnings += 1
elif args.verbose:
print(f" ✅ [{item['check']}] {msg}")
print()
print(f"─── 汇总 ───")
print(f" 校验项数: {len(all_items)}")
print(f" 错误(需修复): {errors}")
print(f" 警告(需关注): {warnings}")
print(f" 通过: {len(all_items) - errors - warnings}")
print()
if errors > 0:
print(f" ❌ 存在 {errors} 项关键问题,建议暂停报告生成并修复。")
print(f" 修正后重新运行:")
print(f" python3 cross_validate.py --ticker {args.ticker} --data {data_path}")
print()
exit_code = 2
elif warnings > 0:
print(f" ⚠️ 存在 {warnings} 项警告,报告中应注明。可继续生成报告。")
exit_code = 1
else:
print(f" ✅ 全部通过,可安全生成报告。")
exit_code = 0
# 写入校验结果
result = {
"ticker": args.ticker,
"data_file": data_path,
"items": all_items,
"errors": errors,
"warnings": warnings,
"passed": len(all_items) - errors - warnings,
"status": "fail" if errors > 0 else "warn" if warnings > 0 else "pass",
}
report_dir = os.path.dirname(data_path)
out_path = os.path.join(report_dir, f"validation_{args.ticker}.json")
with open(out_path, "w") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
sys.exit(exit_code)
if __name__ == "__main__":
main()
scripts/financial-analysis/detect_accounting_anomalies.py#!/usr/bin/env python3
"""
detect_accounting_anomalies.py — 会计异常检测
从财务报表中自动识别红旗信号,包括:
1. 应收天数(DSO)异常增加
2. 存货天数(DIO)异常增加
3. 应付天数(DPO)异常减少
4. 商誉占总资产比过高
5. 经营现金流与净利润持续背离
6. 营收增速与应收增速背离
7. 毛利率与营收增速背离
8. 资本开支与折旧的比值异常
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
def detect(ticker):
"""会计异常检测主函数"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
inc = t.income_stmt
bs = t.balance_sheet
cf = t.cashflow
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
}
anomalies = []
score = 10
# ── 1. 商誉占比 ──
if bs is not None and not bs.empty:
year = sorted(bs.columns, reverse=True)[0]
gw = float(bs.loc["Goodwill", year]) if "Goodwill" in bs.index else 0
ta = float(bs.loc["Total Assets", year]) if "Total Assets" in bs.index else 0
if ta and gw / ta > 0.3:
anomalies.append({
"type": "high_goodwill", "severity": "高",
"signal": f"商誉占总资产{gw/ta*100:.0f}%,减值风险高",
"detail": f"商誉${gw/1e9:.1f}B / 总资产${ta/1e9:.1f}B"
})
score -= 1.5
# ── 2. 经营现金流 vs 净利润 ──
if cf is not None and not cf.empty and inc is not None and not inc.empty:
cf_years = sorted(cf.columns, reverse=True)[:3]
inc_years = sorted(inc.columns, reverse=True)[:3]
ocf_vs_ni = []
for cy, iy in zip(cf_years, inc_years):
ocf = float(cf.loc["Operating Cash Flow", cy]) if "Operating Cash Flow" in cf.index else 0
ni = float(inc.loc["Net Income", iy]) if "Net Income" in inc.index else 0
if ni and ocf / ni < 0.5:
ocf_vs_ni.append({"year": str(cy)[:4], "ocf": ocf, "ni": ni, "ratio": round(ocf/ni, 2)})
if ocf_vs_ni:
anomalies.append({
"type": "ocf_ni_divergence", "severity": "高",
"signal": "经营现金流/净利润 < 0.5x",
"detail": f"利润质量差,赚到的利润未转化为现金:{ocf_vs_ni}"
})
score -= 2
# ── 3. 营收增速 vs 应收增速 ──
if inc is not None and not inc.empty and bs is not None and not bs.empty:
inc_years = sorted(inc.columns, reverse=True)[:3]
bs_years = sorted(bs.columns, reverse=True)[:3]
dso_trend = []
for iy, by in zip(inc_years, bs_years):
rev = float(inc.loc["Total Revenue", iy]) if "Total Revenue" in inc.index else 0
ar = float(bs.loc["Net Receivables", by]) if "Net Receivables" in bs.index else 0
if rev and ar:
dso = ar / rev * 365
dso_trend.append({"year": str(iy)[:4], "dso": round(dso, 1)})
if len(dso_trend) >= 2 and dso_trend[0]["dso"] > dso_trend[-1]["dso"] * 1.3:
anomalies.append({
"type": "dso_spike", "severity": "中",
"signal": f"应收天数从{dso_trend[-1]['dso']}天升至{dso_trend[0]['dso']}天",
"detail": "可能通过放宽信用条件虚增营收"
})
score -= 1
# ── 4. CapEx/折旧比 ──
if cf is not None and not cf.empty:
cf_years = sorted(cf.columns, reverse=True)[:3]
capex_da = []
for y in cf_years:
capex = abs(float(cf.loc["Capital Expenditure", y])) if "Capital Expenditure" in cf.index else 0
da = float(cf.loc["Depreciation And Amortization", y]) if "Depreciation And Amortization" in cf.index else 0
if da:
capex_da.append({"year": str(y)[:4], "ratio": round(capex/da, 2)})
if len(capex_da) >= 2 and capex_da[0]["ratio"] < 0.5:
anomalies.append({
"type": "under_investing", "severity": "中",
"signal": f"CapEx/折旧={capex_da[0]['ratio']}x < 1.0x",
"detail": "资本开支不足以维持现有产能,长期看资产基础在萎缩"
})
score -= 1
# ── 5. 营收增速 vs 存货增速 ──
if inc is not None and not inc.empty and bs is not None and not bs.empty:
inc_years = sorted(inc.columns, reverse=True)[:3]
bs_years = sorted(bs.columns, reverse=True)[:3]
inv_trend = []
for iy, by in zip(inc_years, bs_years):
rev = float(inc.loc["Total Revenue", iy]) if "Total Revenue" in inc.index else 0
inv = float(bs.loc["Inventory", by]) if "Inventory" in bs.index else 0
if rev and inv:
inv_trend.append({"year": str(iy)[:4], "inv_turnover_days": round(inv / rev * 365, 1)})
if len(inv_trend) >= 2 and inv_trend[0]["inv_turnover_days"] > inv_trend[-1]["inv_turnover_days"] * 1.5:
anomalies.append({
"type": "inventory_buildup", "severity": "中",
"signal": f"存货周转天数从{inv_trend[-1]['inv_turnover_days']}天升至{inv_trend[0]['inv_turnover_days']}天",
"detail": "存货积压,可能面临跌价风险"
})
score -= 1
result["anomalies"] = anomalies
result["count"] = len(anomalies)
result["score"] = round(max(0, score), 1)
if not anomalies:
result["verdict"] = "✅ 未发现明显会计异常"
elif score >= 7:
result["verdict"] = f"⚠️ 发现{len(anomalies)}项异常,需进一步确认"
else:
result["verdict"] = f"🔴 发现{len(anomalies)}项高风险异常,建议谨慎"
return result
def to_markdown(result):
lines = [f"# 会计异常检测 — {result['ticker']}", f""]
lines.append(f"**{result.get('company_name','')}** | 得分:{result.get('score','N/A')}/10")
lines.append(f"**{result.get('verdict','')}**")
lines.append(f"")
if not result.get("anomalies"):
lines.append("无异常信号。")
return "\n".join(lines)
lines.append(f"| 严重度 | 信号 | 详情 |")
lines.append(f"|--------|------|------|")
for a in result["anomalies"]:
sev = {"高": "🔴", "中": "🟡", "低": "🟢"}.get(a.get("severity",""), "⚪")
lines.append(f"| {sev} {a['severity']} | {a['signal']} | {a['detail']} |")
return "\n".join(lines)
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--ticker", required=True)
parser.add_argument("--output", default=None)
parser.add_argument("--format", default="markdown", choices=["json", "markdown"])
args = parser.parse_args()
result = detect(args.ticker)
out = json.dumps(result, indent=2, default=str) if args.format == "json" else to_markdown(result)
if args.output:
with open(args.output, "w") as f:
f.write(out)
else:
print(out)
if __name__ == "__main__":
main()
scripts/financial-analysis/extract_maintenance_capex.py#!/usr/bin/env python3
"""
extract_maintenance_capex.py — 维护性资本开支提取器
从 SEC 10-K 中提取/估算 Maintenance CapEx(维护性资本开支)。
这是计算真实自由现金流和格林沃尔德EPV的关键输入。
核心理念:
Total CapEx = Maintenance CapEx(维持现有产能) + Growth CapEx(扩张新产能)
在10-K中,部分公司会在MD&A中披露维护vs增长的拆分。
如果未披露,使用 Depreciation & Amortization 作为 Maintenance CapEx 的代理变量。
用法:
python3 scripts/extract_maintenance_capex.py --ticker AAPL
python3 scripts/extract_maintenance_capex.py --ticker CROX --format markdown
python3 scripts/extract_maintenance_capex.py --ticker MSFT --output /tmp/mc.json
输出:
- maintenance_capex: 维护性资本开支(估算值)
- growth_capex: 增长性资本开支(total - maintenance)
- disclosure_method: 如何获取的(SEC披露 / D&A代理 / yfinance推算)
"""
import json
import os
import sys
import re
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
os.makedirs(DATA_DIR, exist_ok=True)
# ═══════════════════════════════════════════════════════
# SEC EDGAR 工具函数(复用自 fetch_financials.py 的逻辑)
# ═══════════════════════════════════════════════════════
SEC_HEADERS = {
"User-Agent": "Hermes Financial Research (research@example.com)",
"Accept": "application/json",
}
def _lookup_cik(ticker):
"""通过 ticker→CIK 映射文件查找"""
import urllib.request
import json as jsonlib
try:
url = "https://www.sec.gov/files/company_tickers.json"
req = urllib.request.Request(url, headers=SEC_HEADERS)
with urllib.request.urlopen(req, timeout=15) as resp:
mapping = jsonlib.loads(resp.read().decode("utf-8"))
for entry in mapping.values():
if entry.get("ticker", "").upper() == ticker.upper():
return str(entry["cik_str"])
except:
pass
return None
def _get_latest_10k(cik_padded):
"""获取最新 10-K 的归档信息"""
import urllib.request
import json as jsonlib
url = f"https://data.sec.gov/submissions/CIK{cik_padded}.json"
try:
req = urllib.request.Request(url, headers=SEC_HEADERS)
with urllib.request.urlopen(req, timeout=15) as resp:
data = jsonlib.loads(resp.read().decode("utf-8"))
filings = data.get("filings", {}).get("recent", {})
forms = filings.get("form", [])
for i, form in enumerate(forms):
if form in ("10-K", "10-K/A"):
acc = filings["accessionNumber"][i]
doc = filings["primaryDocument"][i]
period = filings.get("reportDate", [""])[i]
filing_url = (
f"https://www.sec.gov/Archives/edgar/data/"
f"{cik_padded.lstrip('0')}/{acc.replace('-','')}/{doc}"
)
return {"period": period, "url": filing_url, "accession": acc}
except:
pass
return None
def _download_10k_text(url):
"""下载10-K并提取纯文本"""
import urllib.request
try:
req = urllib.request.Request(url, headers=SEC_HEADERS)
with urllib.request.urlopen(req, timeout=30) as resp:
html = resp.read().decode("utf-8", errors="replace")
# 去HTML标签
text = re.sub(r"<[^>]+>", " ", html)
text = re.sub(r"\s+", " ", text)
text = text.replace(" ", " ").replace("&", "&").replace(" ", " ")
return text
except Exception as e:
print(f"[mc] 下载失败: {e}", file=sys.stderr)
return None
# ═══════════════════════════════════════════════════════
# 维护性 CapEx 提取逻辑
# ═══════════════════════════════════════════════════════
def extract_maintenance_capex_from_10k(ticker):
"""
主函数:从10-K中提取维护性CapEx
返回 dict:
{
"ticker": "AAPL",
"method": "sec_disclosure" | "da_proxy" | "yfinance_estimate",
"ten_k_period": "2024-09-28",
"total_capex": {2024: -, 2023: -, ...},
"depreciation": {2024: -, 2023: -, ...},
"maintenance_capex": {2024: -, 2023: -, ...},
"growth_capex": {2024: -, 2023: -, ...},
"maintenance_pct": 0.85, # 维护性占总额比例
"disclosure_text": "...", # 10-K 原文中提及维护性CapEx的部分
"analysis": "...", # 分析结论
}
"""
result = {
"ticker": ticker.upper(),
"fetch_time": datetime.now().isoformat(),
}
# ── Step 1: CIK 查找 ──
print(f"[mc] 查找 {ticker} 的CIK...", file=sys.stderr)
cik = _lookup_cik(ticker)
if not cik:
print(f"[mc] ❌ 未找到 CIK", file=sys.stderr)
return _fallback_yfinance(ticker, result)
result["cik"] = cik
cik_padded = cik.zfill(10)
# ── Step 2: 获取最新10-K ──
print(f"[mc] 获取最新10-K (CIK={cik})...", file=sys.stderr)
filing = _get_latest_10k(cik_padded)
if not filing:
print(f"[mc] ⚠️ 未找到10-K,回退yfinance", file=sys.stderr)
return _fallback_yfinance(ticker, result)
result["ten_k_period"] = filing["period"]
result["ten_k_url"] = filing["url"]
# ── Step 3: 下载10-K文本 ──
print(f"[mc] 下载10-K ({filing['period']})...", file=sys.stderr)
text = _download_10k_text(filing["url"])
if not text:
return _fallback_yfinance(ticker, result)
# ── Step 4: 搜索维护性CapEx披露 ──
print(f"[mc] 搜索维护性CapEx披露...", file=sys.stderr)
# 搜索关键词模式
patterns = [
r"maintenance\s+capital\s+(expenditures|expenses|spending)",
r"capital\s+(expenditures|spending)\s+.*?maintenance",
r"sustaining\s+capital\s+(expenditures|expenses)",
r"maintenance\s+capex",
r"capex\s+.*?maintenance",
r"growth\s+capital\s+(expenditures|expenses|spending)",
r"capital\s+(expenditures|spending)\s+.*?growth",
]
disclosure_text = None
for pat in patterns:
matches = re.finditer(pat, text, re.IGNORECASE)
for m in matches:
start = max(0, m.start() - 200)
end = min(len(text), m.end() + 800)
snippet = text[start:end]
# 尝试提取数字
numbers = re.findall(r'[\$]?([0-9,]+(?:\.[0-9]+)?)\s*(million|billion|M|B)?', snippet)
if numbers:
disclosure_text = snippet.strip()
result["disclosure_text"] = disclosure_text[:2000]
break
if disclosure_text:
break
if disclosure_text:
print(f"[mc] ✅ 在10-K中发现维护性CapEx披露", file=sys.stderr)
result["method"] = "sec_disclosure"
# 从披露文本中提取数字(简化处理)
result["disclosure_summary"] = _parse_disclosure_numbers(disclosure_text)
else:
print(f"[mc] ⚠️ 10-K中未明确披露,使用D&A代理法", file=sys.stderr)
result["method"] = "da_proxy"
# ── Step 5: 从 yfinance 获取总 CapEx 和 D&A ──
_add_yfinance_capex_data(ticker, result)
# ── Step 6: 计算维护性/增长性拆分 ──
_compute_split(result)
return result
def _parse_disclosure_numbers(text):
"""尝试从披露文本中解析维护性CapEx的具体数字"""
summary = {}
# 寻找"maintenance capex of $X million"模式
patterns = [
(r"(?:maintenance|sustaining)\s+capex\s+(?:of|was|is|were|totaled|approximately)\s*\$?([0-9,]+(?:\.[0-9]+)?)\s*(million|billion|M|B)?", "maintenance_capex_text"),
(r"(?:growth|expansion)\s+capex\s+(?:of|was|is|were|totaled|approximately)\s*\$?([0-9,]+(?:\.[0-9]+)?)\s*(million|billion|M|B)?", "growth_capex_text"),
]
for pat, key in patterns:
m = re.search(pat, text, re.IGNORECASE)
if m:
num = float(m.group(1).replace(",", ""))
unit = (m.group(2) or "").lower()
if unit in ("billion", "b"):
num *= 1000
summary[key] = num
return summary
def _add_yfinance_capex_data(ticker, result):
"""从 yfinance 获取 CapEx 和 D&A 数据"""
import yfinance as yf
try:
t = yf.Ticker(ticker)
cf = t.cashflow
if cf is None or cf.empty:
return
years = sorted(cf.columns, reverse=True)[:5]
capex_data = {}
da_data = {}
for y in years:
year_str = str(y)[:10] if hasattr(y, 'strftime') else str(y)[:4]
if "Capital Expenditure" in cf.index:
val = cf.loc["Capital Expenditure", y]
if val is not None and val == val:
capex_data[year_str] = abs(float(val))
if "Depreciation And Amortization" in cf.index:
val = cf.loc["Depreciation And Amortization", y]
if val is not None and val == val:
da_data[year_str] = float(val)
result["total_capex"] = capex_data
result["depreciation"] = da_data
except Exception as e:
print(f"[mc] yfinance数据获取失败: {e}", file=sys.stderr)
def _compute_split(result):
"""计算维护性/增长性CapEx拆分"""
total = result.get("total_capex", {})
da = result.get("depreciation", {})
mc = {}
gc = {}
for year in total:
t = total[year]
d = da.get(year)
if result.get("method") == "sec_disclosure":
# 如果有SEC披露的数字,优先使用
disclosed = result.get("disclosure_summary", {})
if "maintenance_capex_text" in disclosed:
mc[year] = disclosed["maintenance_capex_text"]
gc[year] = t - mc[year]
continue
# D&A 代理法(标准做法)
if d and d > 0:
mc[year] = min(d, t) # 维护性不能超过总CapEx
gc[year] = t - mc[year]
else:
mc[year] = t * 0.7 # 完全无数据时,假设70%为维护性
gc[year] = t * 0.3
result["maintenance_capex"] = {k: round(v, 2) for k, v in mc.items()}
result["growth_capex"] = {k: round(v, 2) for k, v in gc.items()}
# 计算维护性占比
latest_total = list(total.values())[0] if total else 0
latest_mc = list(mc.values())[0] if mc else 0
result["maintenance_pct"] = round(latest_mc / latest_total, 4) if latest_total else 0.7
# 分析结论
_generate_analysis(result)
def _generate_analysis(result):
"""生成分析结论"""
method = result.get("method", "da_proxy")
pct = result.get("maintenance_pct", 0.7)
analyses = {
"sec_disclosure": f"公司在10-K中直接披露了维护性/增长性CapEx的拆分。维护性CapEx占总CapEx的{pct*100:.0f}%。",
"da_proxy": f"公司未在10-K中直接披露维护性/增长性拆分。使用D&A作为维护性CapEx的代理变量:维护性CapEx = D&A = 总CapEx的{pct*100:.0f}%。这种方法假设D&A近似反映了维持当前产能所需的资本支出。",
"yfinance_estimate": f"仅使用yfinance数据。由于无10-K和详细D&A数据,假设总CapEx的70%为维护性支出。",
}
result["analysis"] = analyses.get(method, analyses["yfinance_estimate"])
# 维护性 vs 增长性趋势
mc = result.get("maintenance_capex", {})
gc = result.get("growth_capex", {})
total = result.get("total_capex", {})
trends = []
for year in sorted(mc.keys(), reverse=True)[:3]:
t = total.get(year, 0)
m = mc.get(year, 0)
g = gc.get(year, 0)
m_pct = round(m/t*100, 1) if t else 0
g_pct = round(g/t*100, 1) if t else 0
trends.append(f"{year}: 总CapEx={_fmt_num(t)}, 维护性={_fmt_num(m)}({m_pct}%), 增长性={_fmt_num(g)}({g_pct}%)")
result["trend_summary"] = trends
def _fallback_yfinance(ticker, result):
"""完全回退到yfinance"""
print(f"[mc] 回退:仅使用 yfinance 数据", file=sys.stderr)
result["method"] = "yfinance_estimate"
_add_yfinance_capex_data(ticker, result)
_compute_split(result)
return result
def _fmt_num(v):
if v is None:
return "N/A"
if abs(v) >= 1e9:
return f"${v/1e9:.1f}B"
elif abs(v) >= 1e6:
return f"${v/1e6:.1f}M"
else:
return f"${v:.0f}"
# ═══════════════════════════════════════════════════════
# 输出
# ═══════════════════════════════════════════════════════
def to_markdown(result):
"""Markdown 输出"""
lines = []
lines.append(f"# 维护性CapEx分析 — {result['ticker']}")
lines.append(f"")
lines.append(f"**获取方法:** {_method_desc(result.get('method',''))}")
lines.append(f"**10-K期间:** {result.get('ten_k_period','N/A')}")
lines.append(f"")
total = result.get("total_capex", {})
mc = result.get("maintenance_capex", {})
gc = result.get("growth_capex", {})
if total:
lines.append(f"## CapEx拆分(最近3年)")
lines.append(f"")
lines.append(f"| 年份 | 总CapEx | 维护性 | 增长性 | 维护占比 |")
lines.append(f"|------|--------|-------|-------|---------|")
for year in sorted(mc.keys(), reverse=True)[:3]:
t = total.get(year, 0)
m = mc.get(year, 0)
g = gc.get(year, 0)
pct = round(m/t*100, 1) if t else 0
lines.append(f"| {year} | {_fmt_num(t)} | {_fmt_num(m)} | {_fmt_num(g)} | {pct}% |")
lines.append(f"")
if result.get("disclosure_text"):
lines.append(f"## 10-K原文摘录")
lines.append(f"")
lines.append(f"```")
lines.append(result["disclosure_text"][:1500])
lines.append(f"```")
lines.append(f"")
lines.append(f"## 分析结论")
lines.append(f"")
lines.append(f"{result.get('analysis','')}")
lines.append(f"")
if result.get("trend_summary"):
lines.append(f"### 趋势")
lines.append(f"")
for t in result["trend_summary"]:
lines.append(f"- {t}")
# EPV中的应用说明
lines.append(f"")
lines.append(f"## 在EPV计算中的应用")
method = result.get("method", "da_proxy")
pct = result.get("maintenance_pct", 0.7)
if method == "sec_disclosure":
lines.append(f"使用10-K中披露的维护性CapEx(${pct*100:.0f}% 占总CapEx)作为EPV中的维护性支出。")
elif method == "da_proxy":
lines.append(f"使用D&A(${pct*100:.0f}% 占总CapEx)作为维护性CapEx的代理。注意:在增长型公司中,D&A通常低估实际维护性支出。")
else:
lines.append(f"按总CapEx的{pct*100:.0f}%估算维护性支出。建议在10-K可用时更新此数字。")
return "\n".join(lines)
def _method_desc(method):
descs = {
"sec_disclosure": "✅ 10-K直接披露维护性/增长性拆分(最可靠)",
"da_proxy": "⚠️ D&A代理法(维护性CapEx ≈ 折旧摊销,次优方案)",
"yfinance_estimate": "❌ 仅yfinance估算(最不可靠,建议补充10-K)",
}
return descs.get(method, "未知方法")
# ═══════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="维护性资本开支提取器")
parser.add_argument("--ticker", type=str, required=True, help="股票代码")
parser.add_argument("--output", type=str, default=None, help="输出路径")
parser.add_argument("--format", type=str, default="json", choices=["json", "markdown"])
args = parser.parse_args()
result = extract_maintenance_capex_from_10k(args.ticker)
if args.format == "markdown":
output = to_markdown(result)
else:
output = json.dumps(result, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"[mc] ✅ 已写入 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
scripts/financial-analysis/insider_alignment_check.py#!/usr/bin/env python3
"""
insider_alignment_check.py — 内部人利益一致性检查
检查"管理层是否和股东坐在同一条船上":
1. 内部人持股比例
2. 内部人近期买卖交易(通过Finnhub)
3. 股权激励(SBC)占营收比
4. 薪酬结构(现金vs股权比例)
5. 董事会结构
6. 大股东质押情况(A股特有)
输出利益一致性评分和红旗警报。
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
API_KEYS_PATH = os.path.join(BASE_DIR, "..", "config", "api_keys.json")
def _read_api_key(key_name):
try:
with open(API_KEYS_PATH) as f:
return json.load(f).get(key_name, "")
except:
return ""
def check(ticker):
"""内部人利益一致性检查主函数"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
cf = t.cashflow
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"sector": info.get("sector", ""),
"fetch_time": datetime.now().isoformat(),
}
score = 10
red_flags = []
green_flags = []
# ── 1. 内部人持股 ──
insider_pct = info.get("heldPercentInsiders", 0)
institution_pct = info.get("heldPercentInstitutions", 0)
if insider_pct is not None:
result["insider_ownership_pct"] = round(insider_pct * 100, 2)
if insider_pct > 0.3:
green_flags.append(f"内部人持股{insider_pct*100:.0f}%,利益高度一致")
score += 0.5
elif insider_pct > 0.1:
green_flags.append(f"内部人持股{insider_pct*100:.0f}%,利益基本一致")
else:
red_flags.append(f"内部人持股仅{insider_pct*100:.1f}%,利益绑定不足")
score -= 1
if institution_pct is not None:
result["institution_ownership_pct"] = round(institution_pct * 100, 2)
# ── 2. 股权激励(SBC)审查 ──
if cf is not None and not cf.empty:
cf_years = sorted(cf.columns, reverse=True)[:3]
sbc_data = []
for y in cf_years:
sbc = abs(float(cf.loc["Stock Based Compensation", y])) if "Stock Based Compensation" in cf.index else 0
if sbc > 0:
ocf = float(cf.loc["Operating Cash Flow", y]) if "Operating Cash Flow" in cf.index else 0
sbc_data.append({"year": str(y)[:4], "sbc": sbc, "sbc_to_ocf_pct": round(sbc / ocf * 100, 1) if ocf else 0})
if sbc_data:
result["sbc_analysis"] = sbc_data
avg_sbc_ratio = sum(s["sbc_to_ocf_pct"] for s in sbc_data) / len(sbc_data)
if avg_sbc_ratio > 20:
red_flags.append(f"SBC/经营现金流={avg_sbc_ratio:.0f}%,激励严重膨胀")
score -= 2
elif avg_sbc_ratio > 10:
red_flags.append(f"SBC/经营现金流={avg_sbc_ratio:.0f}%,激励偏高")
score -= 1
else:
green_flags.append(f"SBC/经营现金流={avg_sbc_ratio:.0f}%,激励适度")
# ── 3. 通过Finnhub获取内部交易 ──
api_key = _read_api_key("finnhub_api_key")
if api_key:
try:
import finnhub
fh = finnhub.Client(api_key=api_key)
insider = fh.stock_insider_transactions(ticker, "US")
if insider and insider.get("data"):
transactions = []
net_shares = 0
for t in insider["data"][:10]:
change = t.get("change", 0) or 0
net_shares += change
transactions.append({
"name": t.get("name", ""),
"shares": change,
"price": t.get("transactionPrice", 0),
"date": t.get("transactionDate", ""),
})
result["recent_insider_transactions"] = transactions
if net_shares < 0:
red_flags.append(f"近90天内内部人净卖出{abs(net_shares):,}股")
score -= 1
else:
green_flags.append(f"近90天内内部人净买入{net_shares:,}股")
score += 0.5
except:
pass
# ── 4. 短期行为信号 ──
payout = info.get("payoutRatio", 0)
if payout and payout > 0.9:
red_flags.append(f"派息率高达{payout*100:.0f}%,可能牺牲增长维持分红")
score -= 1
result["red_flags"] = red_flags
result["green_flags"] = green_flags
result["score"] = {"score": round(max(0, min(10, score)), 1), "max": 10}
if score >= 7:
result["verdict"] = "管理层与股东利益高度一致 ✅"
elif score >= 4:
result["verdict"] = "管理层利益基本一致,但有需要注意的地方 ⚠️"
else:
result["verdict"] = "管理层利益与股东偏离,需高度警惕 🔴"
return result
def to_markdown(result):
lines = [f"# 内部人利益一致性检查 — {result['ticker']}", f""]
lines.append(f"**{result.get('company_name','')}** | {result.get('sector','')}")
lines.append(f"")
s = result.get("score", {})
lines.append(f"**评分:{s.get('score','N/A')}/{s.get('max',10)}**")
lines.append(f"**{result.get('verdict','')}**")
lines.append(f"")
if result.get("insider_ownership_pct") is not None:
lines.append(f"- 内部人持股:{result['insider_ownership_pct']}%")
if result.get("institution_ownership_pct") is not None:
lines.append(f"- 机构持股:{result['institution_ownership_pct']}%")
sbc = result.get("sbc_analysis", [])
if sbc:
lines.append(f"- SBC分析:")
for s in sbc:
lines.append(f" - {s['year']}: SBC=${s['sbc']/1e6:.0f}M = OCF的{s['sbc_to_ocf_pct']}%")
txs = result.get("recent_insider_transactions", [])
if txs:
lines.append(f"- 近90天内部交易:")
for tx in txs:
buy_sell = "卖出" if tx["shares"] < 0 else "买入"
lines.append(f" - {tx['name']} {buy_sell} {abs(tx['shares']):,}股 @ ${tx['price']}")
lines.append(f"")
for f in result.get("red_flags", []):
lines.append(f"- 🚩 {f}")
for f in result.get("green_flags", []):
lines.append(f"- ✅ {f}")
return "\n".join(lines)
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--ticker", required=True)
parser.add_argument("--output", default=None)
parser.add_argument("--format", default="markdown", choices=["json", "markdown"])
args = parser.parse_args()
result = check(args.ticker)
out = json.dumps(result, indent=2, default=str) if args.format == "json" else to_markdown(result)
if args.output:
with open(args.output, "w") as f:
f.write(out)
else:
print(out)
if __name__ == "__main__":
main()
scripts/financial-analysis/margin_decomposition.py#!/usr/bin/env python3
"""
margin_decomposition.py — 利润率分解分析器
逐层拆解企业的利润结构,追溯利润率变化的根本原因:
营收 100%
- 营业成本 (COGS)
= 毛利润 → 毛利率
- 销售费用 (SG&A)
- 研发费用 (R&D)
- 折旧摊销 (D&A)
= 营业利润 → 营业利润率
- 利息费用 + 其他
= 税前利润
- 所得税
= 净利润 → 净利率
趋势分析(5年)+ 同业对比 + 利润质量评分
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
# ═══════════════════════════════════════════════════════
# 利润率分解
# ═══════════════════════════════════════════════════════
def decompose_margin(ticker):
"""
利润率分解主函数
从 yfinance 获取5年利润表数据,逐层拆解利润率
返回 dict 包含:
- 每年度的逐层利润率分解
- 5年趋势
- 利润质量评分
- 风险信号
"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
inc = t.income_stmt
cf = t.cashflow
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"sector": info.get("sector", ""),
"industry": info.get("industry", ""),
"currency": info.get("financialCurrency", "USD"),
}
if inc is None or inc.empty:
result["error"] = "无法获取利润表数据"
return result
years = sorted(inc.columns, reverse=True)[:5]
yearly = []
for year in years:
year_str = str(year)[:10] if hasattr(year, 'strftime') else str(year)[:4]
row = {"year": year_str}
try:
revenue = float(inc.loc["Total Revenue", year]) if "Total Revenue" in inc.index else 0
except:
revenue = 0
if revenue == 0:
continue
row["revenue"] = revenue
# ── 各利润层级 ──
try:
row["cost_of_revenue"] = float(inc.loc["Cost Of Revenue", year]) if "Cost Of Revenue" in inc.index else 0
except:
row["cost_of_revenue"] = 0
try:
row["gross_profit"] = float(inc.loc["Gross Profit", year]) if "Gross Profit" in inc.index else 0
except:
row["gross_profit"] = revenue - row["cost_of_revenue"]
# 营业费用
try:
row["rd_expense"] = float(inc.loc["Research And Development", year]) if "Research And Development" in inc.index else 0
except:
row["rd_expense"] = 0
try:
row["sga_expense"] = float(inc.loc["Selling General And Administration", year]) if "Selling General And Administration" in inc.index else 0
except:
row["sga_expense"] = 0
# 营业利润
try:
row["operating_income"] = float(inc.loc["Operating Income", year]) if "Operating Income" in inc.index else 0
except:
row["operating_income"] = 0
# 净利率
try:
row["net_income"] = float(inc.loc["Net Income", year]) if "Net Income" in inc.index else 0
except:
row["net_income"] = 0
# 利息与税收
try:
row["interest_expense"] = float(inc.loc["Interest Expense", year]) if "Interest Expense" in inc.index else 0
except:
row["interest_expense"] = 0
try:
row["interest_income"] = float(inc.loc["Interest Income", year]) if "Interest Income" in inc.index else 0
except:
row["interest_income"] = 0
try:
row["tax_provision"] = float(inc.loc["Tax Provision", year]) if "Tax Provision" in inc.index else 0
except:
row["tax_provision"] = 0
# ── 计算利润率 ──
row["gross_margin_pct"] = round(row["gross_profit"] / revenue * 100, 2)
row["rd_to_revenue_pct"] = round(abs(row["rd_expense"]) / revenue * 100, 2)
row["sga_to_revenue_pct"] = round(abs(row["sga_expense"]) / revenue * 100, 2)
row["operating_margin_pct"] = round(row["operating_income"] / revenue * 100, 2)
row["net_margin_pct"] = round(row["net_income"] / revenue * 100, 2)
row["effective_tax_rate_pct"] = round(abs(row["tax_provision"]) / (abs(row["net_income"]) + abs(row["tax_provision"])) * 100, 1) if row["net_income"] != 0 else 0
# ── 增量分析(每赚¥1营收的分配) ──
row["per_dollar_breakdown"] = {
"cost_of_goods": round(row["cost_of_revenue"] / revenue * 100, 1),
"rd": round(abs(row["rd_expense"]) / revenue * 100, 1),
"sga": round(abs(row["sga_expense"]) / revenue * 100, 1),
"interest_net": round((abs(row["interest_expense"]) - row["interest_income"]) / revenue * 100, 1),
"tax": round(abs(row["tax_provision"]) / revenue * 100, 1),
"net_profit": row["net_margin_pct"],
}
yearly.append(row)
result["yearly"] = yearly
# ── 5年趋势摘要 ──
if len(yearly) >= 2:
result["trends"] = _calc_trends(yearly)
result["signals"] = _detect_signals(yearly)
result["quality_score"] = _quality_score(yearly)
# ── 营收质量(从现金流量表验证) ──
if cf is not None and not cf.empty:
cf_years = sorted(cf.columns, reverse=True)[:5]
cash_quality = []
for cy in cf_years:
cy_str = str(cy)[:10] if hasattr(cy, 'strftime') else str(cy)[:4]
try:
ocf = float(cf.loc["Operating Cash Flow", cy]) if "Operating Cash Flow" in cf.index else 0
ni_for_year = None
for yd in yearly:
if yd["year"] == cy_str:
ni_for_year = yd["net_income"]
break
if ni_for_year and ni_for_year != 0:
ratio = round(ocf / ni_for_year, 2)
tag = "✅ 优质" if ratio > 1.0 else "⚠️ 存疑" if ratio > 0.5 else "❌ 危险"
cash_quality.append({"year": cy_str, "ocf": ocf, "net_income": ni_for_year, "ocf_to_ni_ratio": ratio, "tag": tag})
except:
pass
result["cash_quality"] = cash_quality
return result
def _calc_trends(yearly):
"""计算5年趋势"""
first = yearly[-1]
last = yearly[0]
metrics = [
("revenue", "营收"),
("gross_margin_pct", "毛利率"),
("operating_margin_pct", "营业利润率"),
("net_margin_pct", "净利率"),
("rd_to_revenue_pct", "研发费用率"),
("sga_to_revenue_pct", "销售管理费用率"),
]
trends = []
for key, label in metrics:
start = first.get(key, 0)
end = last.get(key, 0)
if start != 0:
if key.endswith("_pct") or key.endswith("_rate_pct"):
change = round(end - start, 2)
direction = "↑" if change > 0 else "↓" if change < 0 else "→"
else:
change = round((end - start) / abs(start) * 100, 1)
direction = "↑" if change > 0 else "↓" if change < 0 else "→"
trends.append({"metric": label, "start": start, "end": end, "change": change, "direction": direction})
return trends
def _detect_signals(yearly):
"""检测利润率相关的风险信号"""
signals = []
# 信号1:毛利率连续下降
gms = [y["gross_margin_pct"] for y in yearly[:4]]
if len(gms) >= 3 and gms[0] < gms[1] < gms[2]:
signals.append({"type": "⚠️", "signal": "毛利率连续2年下降", "detail": f"{gms[2]}% → {gms[1]}% → {gms[0]}%"})
# 信号2:SG&A占比持续上升
sgas = [y["sga_to_revenue_pct"] for y in yearly[:4]]
if len(sgas) >= 3 and sgas[0] > sgas[1] > sgas[2]:
signals.append({"type": "⚠️", "signal": "管理费用率持续上升", "detail": f"{sgas[2]}% → {sgas[1]}% → {sgas[0]}%"})
# 信号3:营业利润率 vs 毛利率背离
if len(yearly) >= 2:
gm_diff = yearly[0]["gross_margin_pct"] - yearly[-1]["gross_margin_pct"]
om_diff = yearly[0]["operating_margin_pct"] - yearly[-1]["operating_margin_pct"]
if gm_diff > 0 and om_diff < 0:
signals.append({"type": "🔴", "signal": "毛利率上升但营业利润率下降", "detail": "费用端失控,需检查SG&A和R&D趋势"})
if gm_diff < 0 and om_diff > 0:
signals.append({"type": "✅", "signal": "毛利率下降但营业利润率上升", "detail": "费用管控改善,或规模效应显现"})
# 信号4:净利率波动过大
nms = [y["net_margin_pct"] for y in yearly[:4]]
if len(nms) >= 3:
max_nm, min_nm = max(nms), min(nms)
if max_nm - min_nm > 10:
signals.append({"type": "⚠️", "signal": "净利率波动过大", "detail": f"5年内最高{max_nm}%→最低{min_nm}%,幅度>{10}%:可能存在非经常性损益"})
# 信号5:研发费用率异常
rds = [y["rd_to_revenue_pct"] for y in yearly[:3]]
if len(rds) >= 2 and all(r < 1.0 for r in rds[:2]):
signals.append({"type": "ℹ️", "signal": "研发费用率低于1%", "detail": "这是典型的重营销/轻研发模式,关注技术迭代风险"})
return signals
def _quality_score(yearly):
"""利润质量评分(0-10)"""
score = 10
reasons = []
# 扣分项
gms = [y["gross_margin_pct"] for y in yearly[:4]]
om = [y["operating_margin_pct"] for y in yearly[:4]]
nms = [y["net_margin_pct"] for y in yearly[:4]]
if len(gms) >= 3 and gms[0] < gms[-1]:
score -= 1
reasons.append(f"毛利率趋势下降: {gms[-1]}%→{gms[0]}%")
# 毛利率绝对值
avg_gm = sum(gms[:3]) / len(gms[:3]) if gms else 0
if avg_gm < 20:
score -= 1
reasons.append(f"毛利率偏低({avg_gm:.1f}%)")
elif avg_gm > 40:
score += 0 # 不扣分
# SG&A占比
avg_sga = sum([y["sga_to_revenue_pct"] for y in yearly[:3]]) / 3
if avg_sga > 30:
score -= 1
reasons.append(f"SG&A占比偏高({avg_sga:.1f}%)")
# 净利率稳定性
if len(nms) >= 3 and max(nms[:3]) - min(nms[:3]) > 5:
score -= 1
reasons.append("净利率波动较大")
# 运营利润率
avg_om = sum(om[:3]) / len(om[:3]) if om else 0
if avg_om < 5:
score -= 1
reasons.append(f"营业利润率偏低({avg_om:.1f}%)")
score = max(0, min(10, score))
return {"score": score, "max": 10, "reasons": reasons}
# ═══════════════════════════════════════════════════════
# 输出
# ═══════════════════════════════════════════════════════
def to_markdown(result):
lines = []
if "error" in result:
return f"# ❌ 利润率分解失败\n\n{result['error']}"
lines.append(f"# 利润率分解 — {result['ticker']}")
lines.append(f"")
lines.append(f"**{result.get('company_name','')}** | {result.get('sector','')} | {result.get('industry','')}")
lines.append(f"")
yearly = result.get("yearly", [])
# ── 逐层分解表 ──
lines.append(f"## 利润率逐层分解(5年)")
lines.append(f"")
lines.append(f"| 指标 | {' | '.join([y['year'] for y in yearly])} |")
lines.append(f"|------|{'|'.join(['---']*len(yearly))}|")
rows = [
("revenue", "营收", True),
("cost_of_revenue", "减:营业成本", True),
("gross_profit", "毛利润", True),
("gross_margin_pct", "毛利率", False),
("rd_expense", "减:研发费用", True),
("sga_expense", "减:销售管理费用", True),
("operating_income", "营业利润", True),
("operating_margin_pct", "营业利润率", False),
("net_income", "净利润", True),
("net_margin_pct", "净利率", False),
]
for key, label, is_abs in rows:
vals = [label]
for y in yearly:
v = y.get(key, 0)
if v is None or v == 0:
vals.append("-")
elif not is_abs:
vals.append(f"{v}%")
else:
if abs(v) >= 1e9:
vals.append(f"${v/1e9:.2f}B")
elif abs(v) >= 1e6:
vals.append(f"${v/1e6:.2f}M")
else:
vals.append(f"${v:,.0f}")
lines.append(f"| {' | '.join(vals)} |")
lines.append(f"")
# ── 每¥1营收的分配 ──
lines.append(f"## 每¥1营收的分配(最新年)")
lines.append(f"")
if yearly:
bd = yearly[0].get("per_dollar_breakdown", {})
lines.append(f"```")
lines.append(f" ¥1.00 营收")
lines.append(f" - ¥{bd.get('cost_of_goods',0)/100:.2f} 营业成本")
lines.append(f" - ¥{bd.get('rd',0)/100:.2f} 研发")
lines.append(f" - ¥{bd.get('sga',0)/100:.2f} 销售管理")
lines.append(f" - ¥{bd.get('interest_net',0)/100:.2f} 利息")
lines.append(f" - ¥{bd.get('tax',0)/100:.2f} 税")
lines.append(f" ─────────────────")
lines.append(f" = ¥{bd.get('net_profit',0)/100:.2f} 净利润")
lines.append(f"```")
lines.append(f"")
# ── 趋势 ──
trends = result.get("trends", [])
if trends:
lines.append(f"## 5年趋势")
lines.append(f"")
lines.append(f"| 指标 | 最早 | 最近 | 变化 | 方向 |")
lines.append(f"|------|------|------|------|------|")
for t in trends:
start = t["start"]
end = t["end"]
if isinstance(start, float):
start_s = f"{start:.2f}%"
end_s = f"{end:.2f}%"
else:
start_s = f"${start/1e9:.1f}B" if abs(start) >= 1e9 else str(start)
end_s = f"${end/1e9:.1f}B" if abs(end) >= 1e9 else str(end)
lines.append(f"| {t['metric']} | {start_s} | {end_s} | {t['change']} | {t['direction']} |")
lines.append(f"")
# ── 信号 ──
signals = result.get("signals", [])
if signals:
lines.append(f"## 风险信号")
lines.append(f"")
for sig in signals:
lines.append(f"- {sig['type']} **{sig['signal']}**")
lines.append(f" - {sig['detail']}")
lines.append(f"")
# ── 利润质量 ──
qs = result.get("quality_score", {})
if qs:
score = qs.get("score", 0)
bar = "█" * score + "░" * (10 - score)
lines.append(f"## 利润质量评分")
lines.append(f"")
lines.append(f"**{score}/10** [{bar}]")
if qs.get("reasons"):
for r in qs["reasons"]:
lines.append(f"- 扣分:{r}")
lines.append(f"")
# ── 现金流验证 ──
cq = result.get("cash_quality", [])
if cq:
lines.append(f"## 现金流验证(OCF/净利比)")
lines.append(f"")
lines.append(f"| 年份 | 经营现金流 | 净利润 | OCF/净利 | 评价 |")
lines.append(f"|------|-----------|-------|---------|------|")
for c in cq:
lines.append(f"| {c['year']} | ${c['ocf']/1e9:.2f}B | ${c['net_income']/1e9:.2f}B | {c['ocf_to_ni_ratio']}x | {c['tag']} |")
lines.append(f"")
return "\n".join(lines)
# ═══════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="利润率分解分析器")
parser.add_argument("--ticker", type=str, required=True, help="股票代码")
parser.add_argument("--output", type=str, default=None, help="输出路径")
parser.add_argument("--format", type=str, default="markdown", choices=["json", "markdown"])
args = parser.parse_args()
result = decompose_margin(args.ticker)
if args.format == "markdown":
output = to_markdown(result)
else:
output = json.dumps(result, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"[margin] ✅ 已写入 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
scripts/financial-analysis/stress_test_margin_of_safety.py#!/usr/bin/env python3
"""
stress_test_margin_of_safety.py — 安全边际压力测试
基于格林沃尔德三段估值框架,对内在价值进行多情景压力测试。
测试场景:
- 基准场景:当前假设不变
- 衰退场景:营收下降 + 利润率收窄
- 竞争场景:ROIC回归 + 护城河缩短
- 极端场景:最坏情况组合
输出每场景下的内在价值和安全边际变化。
"""
import json
import os
import sys
import warnings
from datetime import datetime
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, "..", "data")
def stress_test(ticker):
"""
安全边际压力测试
从 yfinance 获取数据后,构建格林沃尔德三段的基准估值,
然后对关键变量施加冲击,生成多情景下的内在价值和安全边际。
"""
import yfinance as yf
t = yf.Ticker(ticker)
info = t.info or {}
inc = t.income_stmt
bs = t.balance_sheet
cf = t.cashflow
result = {
"ticker": ticker.upper(),
"company_name": info.get("longName") or info.get("shortName", ""),
"current_price": info.get("currentPrice"),
"market_cap": info.get("marketCap"),
}
# ── 获取基准参数 ──
if inc is None or inc.empty:
result["error"] = "数据不足"
return result
iy = sorted(inc.columns, reverse=True)[0]
by = sorted(bs.columns, reverse=True)[0] if bs is not None and not bs.empty else iy
revenue = float(inc.loc["Total Revenue", iy]) if "Total Revenue" in inc.index else 1
ebit = float(inc.loc["EBIT", iy]) if "EBIT" in inc.index else 0
op_inc = float(inc.loc["Operating Income", iy]) if "Operating Income" in inc.index else ebit
tax_r = 0.20 # 默认税率
cash = float(bs.loc["Cash Cash Equivalents And Short Term Investments", by]) if bs is not None and "Cash Cash Equivalents And Short Term Investments" in bs.index else (float(bs.loc["Cash And Cash Equivalents", by]) if bs is not None and "Cash And Cash Equivalents" in bs.index else 0)
debt = float(bs.loc["Total Debt", by]) if bs is not None and "Total Debt" in bs.index else 0
equity = float(bs.loc["Stockholders Equity", by]) if bs is not None and "Stockholders Equity" in bs.index else 0
nopat = op_inc * (1 - tax_r)
invested_capital = debt + equity - cash
roic = nopat / invested_capital if invested_capital else 0.10
# CapEx拆分
capex = 0
da = 0
if cf is not None and not cf.empty:
cy = sorted(cf.columns, reverse=True)[0]
capex = abs(float(cf.loc["Capital Expenditure", cy])) if "Capital Expenditure" in cf.index else 0
da = float(cf.loc["Depreciation And Amortization", cy]) if "Depreciation And Amortization" in cf.index else 0
sbc = abs(float(cf.loc["Stock Based Compensation", cy])) if "Stock Based Compensation" in cf.index else 0
else:
sbc = 0
maint_capex = min(da, capex) if da else capex * 0.7
growth_capex = max(0, capex - maint_capex)
# ── 基准场景参数 ──
base = {
"nopat": nopat,
"roic": roic,
"wacc": 0.09,
"growth_b": growth_capex / nopat if nopat else 0,
"moat_years": 10,
}
scenarios = {
"基准场景(Base Case)": {
"desc": "当前假设不变",
"params": base,
},
"温和衰退(Mild Recession)": {
"desc": "营收-15%,毛利率-3%,OPM-2%",
"params": {
"nopat": nopat * 0.75,
"roic": max(0.05, roic * 0.7),
"wacc": 0.10,
"growth_b": base["growth_b"] * 0.5,
"moat_years": 7,
},
},
"激烈竞争(Competition)": {
"desc": "ROIC腰斩、护城河缩短",
"params": {
"nopat": nopat * 0.65,
"roic": max(0.03, roic * 0.4),
"wacc": 0.11,
"growth_b": base["growth_b"] * 0.3,
"moat_years": 3,
},
},
"极端情况(Worst Case)": {
"desc": "营收-30%、利润率砍半",
"params": {
"nopat": nopat * 0.4,
"roic": max(0.01, roic * 0.2),
"wacc": 0.14,
"growth_b": 0,
"moat_years": 0,
},
},
}
price = info.get("currentPrice") or info.get("regularMarketPrice", 0)
shares = info.get("sharesOutstanding", 1)
# ── 对每个场景计算内在价值 ──
scenario_results = []
for name, sc in scenarios.items():
p = sc["params"]
# ARV(简化版:净资产×调整系数)
arv = (cash + (equity - cash) * 0.7 - debt) / shares if shares else 0
# EPV
epv = p["nopat"] / p["wacc"] / shares if shares and p["wacc"] else 0
# FV
spread = p["roic"] - p["wacc"]
if spread > 0 and p["growth_b"] > 0:
ann_val = p["nopat"] * p["growth_b"] * spread / p["wacc"]
annuity = (1 - 1 / (1 + p["wacc"]) ** p["moat_years"]) / p["wacc"]
fv = ann_val * annuity / shares if shares else 0
else:
fv = 0
intrinsic = arv + epv + fv
safety = round((intrinsic - price) / intrinsic * 100, 1) if intrinsic > 0 else -999
scenario_results.append({
"scenario": name,
"description": sc["desc"],
"assumptions": {
"nopat_adjusted_pct": round(p["nopat"] / nopat * 100 - 100, 1) if nopat else 0,
"roic_pct": round(p["roic"] * 100, 1),
"wacc_pct": round(p["wacc"] * 100, 1),
"spread_pct": round(spread * 100, 1),
"moat_years": p["moat_years"],
},
"valuation": {
"arv_ps": round(arv, 2),
"epv_ps": round(epv, 2),
"fv_ps": round(fv, 2),
"intrinsic_ps": round(intrinsic, 2),
"current_price": price,
"safety_margin_pct": safety,
},
})
result["scenarios"] = scenario_results
result["base_case"] = scenario_results[0]
return result
def to_markdown(result):
if "error" in result:
return f"# 压力测试失败\n\n{result['error']}"
lines = [f"# 安全边际压力测试 — {result['ticker']}", f""]
lines.append(f"**{result.get('company_name','')}**")
lines.append(f"**当前价格:** {result.get('current_price')} | **市值:** {result.get('market_cap')}")
lines.append(f"")
lines.append(f"## 多情景内在价值对比")
lines.append(f"")
lines.append(f"| 场景 | ROIC | WACC | Spread | 护城河 | ARV | EPV | FV | 内在价值 | 安全边际 |")
lines.append(f"|------|------|------|--------|-------|-----|-----|-----|---------|---------|")
for sc in result.get("scenarios", []):
a = sc["assumptions"]
v = sc["valuation"]
sm = v.get("safety_margin_pct", 0)
sm_s = f"{sm:+}%" if sm > -999 else "N/A"
color = "✅" if sm > 20 else "⚠️" if sm > 0 else "🔴"
lines.append(f"| {sc['scenario']} | {a['roic_pct']}% | {a['wacc_pct']}% | {a['spread_pct']:+}% | {a['moat_years']}年 | ${v['arv_ps']} | ${v['epv_ps']} | ${v['fv_ps']} | **${v['intrinsic_ps']}** | {color} {sm_s} |")
lines.append(f"")
# 可视化:安全边际分布
lines.append(f"## 可视化")
lines.append(f"")
lines.append(f"```")
for sc in result.get("scenarios", []):
v = sc["valuation"]
sm = v.get("safety_margin_pct", 0)
bar_len = 30
if sm > 0:
bars = int(sm / 100 * bar_len) if sm < 100 else bar_len
bar = "🟩" * bars + "⬜" * (bar_len - bars)
else:
bars = min(int(abs(sm) / 100 * bar_len), bar_len)
bar = "⬜" * (bar_len - bars) + "🟥" * bars
lines.append(f"{sc['scenario'][:12]:<12} |{bar}| ${v['intrinsic_ps']:>6.0f}")
lines.append(f"```")
return "\n".join(lines)
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--ticker", required=True)
parser.add_argument("--output", default=None)
parser.add_argument("--format", default="markdown", choices=["json", "markdown"])
args = parser.parse_args()
result = stress_test(args.ticker)
out = json.dumps(result, indent=2, default=str) if args.format == "json" else to_markdown(result)
if args.output:
with open(args.output, "w") as f:
f.write(out)
else:
print(out)
if __name__ == "__main__":
main()
scripts/financial-data-acquisition/fetch_financials.py#!/usr/bin/env python3
"""
fetch_financials.py — 金融数据采集器
从 yfinance 拉取过去 5 年完整财报数据,输出结构化 JSON。
对 yfinance 缺失的数据字段标记为 null,由调用方通过 web 搜索补充。
用法:
python3 scripts/fetch_financials.py --ticker AAPL
python3 scripts/fetch_financials.py --ticker AAPL --output /path/to/output.json
python3 scripts/fetch_financials.py --ticker 600519.SS --format markdown
python3 scripts/fetch_financials.py --ticker GOOGL --years 3
"""
import json
import os
import sys
import warnings
from datetime import datetime
from pathlib import Path
warnings.filterwarnings("ignore")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SKILL_DIR = os.path.dirname(BASE_DIR)
DATA_DIR = os.path.join(SKILL_DIR, "..", "data") # financial-screener/data
# 如果没有 financial-screener,存在自身 skill 目录下
FALLBACK_DIR = os.path.join(BASE_DIR, "..", "data")
os.makedirs(DATA_DIR, exist_ok=True)
# ═══════════════════════════════════════════════════════
# yfinance 财务报表字段映射
# ═══════════════════════════════════════════════════════
# 利润表字段映射 (yfinance item name -> 标准输出名)
INCOME_MAP = {
"Total Revenue": "revenue",
"Cost Of Revenue": "cost_of_revenue",
"Gross Profit": "gross_profit",
"Research And Development": "rd_expense",
"Selling General And Administration": "sga_expense",
"Operating Expenses": "operating_expenses",
"Operating Income": "operating_income",
"EBIT": "ebit",
"Interest Expense": "interest_expense",
"Interest Income": "interest_income",
"Other Income Expense": "other_income",
"Income Before Tax": "pretax_income",
"Tax Provision": "tax_provision",
"Net Income": "net_income",
"Net Income From Continuing Ops": "net_income_continuing",
"Minority Interest": "minority_interest",
"Basic EPS": "basic_eps",
"Diluted EPS": "diluted_eps",
"Diluted Average Shares": "diluted_shares",
"Basic Average Shares": "basic_shares",
}
# 资产负债表字段映射
BALANCE_MAP = {
"Cash And Cash Equivalents": "cash_and_equivalents",
"Short Term Investments": "short_term_investments",
"Other Short Term Investments": "other_short_term_investments",
"Cash Cash Equivalents And Short Term Investments": "total_cash_and_st_investments",
"Cash And Short Term Investments": "cash_and_investments",
"Net Receivables": "accounts_receivable",
"Inventory": "inventory",
"Other Current Assets": "other_current_assets",
"Total Current Assets": "total_current_assets",
"Property Plant Equipment": "pp_and_e",
"Goodwill": "goodwill",
"Intangible Assets": "intangible_assets",
"Goodwill And Intangible Assets": "goodwill_and_intangibles",
"Long Term Investments": "long_term_investments",
"Other Non Current Assets": "other_non_current_assets",
"Total Non Current Assets": "total_non_current_assets",
"Total Assets": "total_assets",
"Accounts Payable": "accounts_payable",
"Short Long Term Debt": "short_term_debt",
"Current Debt": "current_debt",
"Other Current Liabilities": "other_current_liabilities",
"Total Current Liabilities": "total_current_liabilities",
"Long Term Debt": "long_term_debt",
"Total Debt": "total_debt",
"Deferred Revenue": "deferred_revenue",
"Other Non Current Liabilities": "other_non_current_liabilities",
"Total Non Current Liabilities": "total_non_current_liabilities",
"Total Liabilities Net Minority Interest": "total_liabilities",
"Stockholders Equity": "shareholders_equity",
"Retained Earnings": "retained_earnings",
"Accumulated Other Comprehensive Income": "accumulated_oci",
}
# 现金流量表字段映射
CASHFLOW_MAP = {
"Operating Cash Flow": "operating_cash_flow",
"Investing Cash Flow": "investing_cash_flow",
"Financing Cash Flow": "financing_cash_flow",
"Capital Expenditure": "capital_expenditures",
"Free Cash Flow": "free_cash_flow",
"Stock Based Compensation": "stock_based_compensation",
"Dividends Paid": "dividends_paid",
"Common Stock Dividend Paid": "common_dividends",
"Repurchase Of Capital Stock": "share_repurchases",
"Issuance Of Capital Stock": "share_issuance",
"Change In Working Capital": "change_in_working_capital",
"Depreciation And Amortization": "depreciation_and_amortization",
"Net Borrowings": "net_borrowings",
"Cash From Discontinued Ops": "cash_from_discontinued",
"Other Financing Activities": "other_financing",
}
def _safe_val(val):
"""安全取值,NaN/None -> None"""
if val is None:
return None
try:
if isinstance(val, float) and (val != val): # NaN check
return None
return int(val) if abs(val) > 1e9 else float(val)
except (ValueError, TypeError, OverflowError):
return None
def extract_financials(ticker_symbol):
"""
从 yfinance 提取 5 年财报数据
返回 dict:
{
"ticker": "...",
"company_name": "...",
"source": "yfinance",
"income_statement": [{...}, ...],
"balance_sheet": [{...}, ...],
"cash_flow": [{...}, ...],
"info": {...}
}
"""
import yfinance as yf
ticker = yf.Ticker(ticker_symbol)
# ── 基本信息 ──
info = {}
try:
raw_info = ticker.info or {}
info = {
"company_name": raw_info.get("longName") or raw_info.get("shortName", ""),
"sector": raw_info.get("sector", ""),
"industry": raw_info.get("industry", ""),
"current_price": raw_info.get("currentPrice") or raw_info.get("regularMarketPrice"),
"market_cap": raw_info.get("marketCap"),
"enterprise_value": raw_info.get("enterpriseValue"),
"shares_outstanding": raw_info.get("sharesOutstanding"),
"fiscal_year_end": raw_info.get("fiscalYearEnd", ""),
"trailing_pe": raw_info.get("trailingPE"),
"forward_pe": raw_info.get("forwardPE"),
"price_to_book": raw_info.get("priceToBook"),
"dividend_yield": raw_info.get("dividendYield"),
"payout_ratio": raw_info.get("payoutRatio"),
"beta": raw_info.get("beta"),
"fifty_two_week_high": raw_info.get("fiftyTwoWeekHigh"),
"fifty_two_week_low": raw_info.get("fiftyTwoWeekLow"),
"employees": raw_info.get("fullTimeEmployees"),
"country": raw_info.get("country"),
"exchange": raw_info.get("exchange"),
"currency": raw_info.get("financialCurrency", "USD"),
}
except Exception as e:
info = {"error": str(e)}
# ── 利润表 ──
income_stmt = []
try:
raw_income = ticker.income_stmt
if raw_income is not None and not raw_income.empty:
years = sorted(raw_income.columns, reverse=True)[:5]
for year in years:
record = {"fiscal_year": str(year)[:4] if hasattr(year, 'strftime') else str(year)[:4]}
for yf_name, out_name in INCOME_MAP.items():
if yf_name in raw_income.index:
record[out_name] = _safe_val(raw_income.loc[yf_name, year])
income_stmt.append(record)
except Exception as e:
print(f"[fetch] ⚠️ 利润表读取失败: {e}", file=sys.stderr)
# ── 资产负债表 ──
balance_sheet = []
try:
raw_bs = ticker.balance_sheet
if raw_bs is not None and not raw_bs.empty:
years = sorted(raw_bs.columns, reverse=True)[:5]
for year in years:
record = {"fiscal_year": str(year)[:4]}
# ── 白名单映射 ──
for yf_name, out_name in BALANCE_MAP.items():
if yf_name in raw_bs.index:
record[out_name] = _safe_val(raw_bs.loc[yf_name, year])
# ── 自动发现未映射字段(特别是现金/投资类) ──
CASH_KEYWORDS = ['cash', 'short_term_investment', 'short term investment']
for idx in raw_bs.index:
if idx not in BALANCE_MAP:
idx_lower = idx.lower()
if any(kw in idx_lower for kw in CASH_KEYWORDS):
safe_name = idx.lower().replace(' ', '_').replace('-', '_')
if safe_name not in record:
record["unmapped_" + safe_name] = _safe_val(raw_bs.loc[idx, year])
if record["unmapped_" + safe_name] is not None:
print(f"[fetch] 🔍 自动发现未映射的现金类字段: {idx} = {record['unmapped_' + safe_name]:,}", file=sys.stderr)
# ── 现金自动汇总 ──
cash_field_keys = ['cash_and_equivalents', 'short_term_investments',
'other_short_term_investments', 'cash_and_investments']
existing_cash_keys = [k for k in cash_field_keys if k in record and record[k] is not None]
unmapped_cash_keys = [k for k in sorted(record.keys()) if k.startswith('unmapped_') and ('cash' in k.lower() or 'short_term' in k.lower())]
auto_total_cash = sum(record[k] for k in existing_cash_keys if record.get(k)) + \
sum(record[k] for k in unmapped_cash_keys if record.get(k) and k not in existing_cash_keys)
if 'total_cash_and_st_investments' in record and record['total_cash_and_st_investments'] is not None:
record['_total_cash_check'] = {
'auto_summed_cash': auto_total_cash,
'yfinance_total': record['total_cash_and_st_investments'],
}
if auto_total_cash > 0:
diff_pct = abs(auto_total_cash - record['total_cash_and_st_investments']) / auto_total_cash * 100
if diff_pct > 1:
print(f"[fetch] ⚠️ 现金合计不匹配: 自动汇总={auto_total_cash:,} vs yfinance合计={record['total_cash_and_st_investments']:,} 差异{diff_pct:.1f}%", file=sys.stderr)
else:
record['_auto_total_cash'] = auto_total_cash
print(f"[fetch] ℹ️ 现金自动汇总: {auto_total_cash:,}", file=sys.stderr)
# 派生指标
equity = record.get("shareholders_equity")
shares = info.get("shares_outstanding")
if equity and shares and shares > 0:
record["book_value_per_share"] = round(equity / shares, 2)
balance_sheet.append(record)
except Exception as e:
print(f"[fetch] ⚠️ 资产负债表读取失败: {e}", file=sys.stderr)
# ── 现金流量表 ──
cash_flow = []
try:
raw_cf = ticker.cashflow
if raw_cf is not None and not raw_cf.empty:
years = sorted(raw_cf.columns, reverse=True)[:5]
for year in years:
record = {"fiscal_year": str(year)[:4]}
for yf_name, out_name in CASHFLOW_MAP.items():
if yf_name in raw_cf.index:
record[out_name] = _safe_val(raw_cf.loc[yf_name, year])
# 派生:真实 FCF(经营现金流 - 资本开支 - SBC)
ocf = record.get("operating_cash_flow")
capex = record.get("capital_expenditures")
sbc = record.get("stock_based_compensation")
if ocf is not None and capex is not None:
raw_fcf = ocf + capex # capex 是负值
record["free_cash_flow"] = _safe_val(raw_fcf)
if sbc is not None:
record["true_owner_earnings"] = _safe_val(raw_fcf - sbc)
else:
record["true_owner_earnings"] = None
# 真实股东回报 = 回购 + 分红
buybacks = record.get("share_repurchases")
divs = record.get("dividends_paid") or record.get("common_dividends")
total_return = 0
if buybacks is not None:
total_return += abs(buybacks) if buybacks < 0 else buybacks
if divs is not None:
total_return += abs(divs) if divs < 0 else divs
record["total_shareholder_return"] = _safe_val(total_return)
cash_flow.append(record)
except Exception as e:
print(f"[fetch] ⚠️ 现金流量表读取失败: {e}", file=sys.stderr)
return {
"ticker": ticker_symbol.upper(),
"source": "yfinance",
"fetch_time": datetime.now().isoformat(),
"info": info,
"income_statement": income_stmt,
"balance_sheet": balance_sheet,
"cash_flow": cash_flow,
}
# ═══════════════════════════════════════════════════════
# 派生指标计算
# ═══════════════════════════════════════════════════════
def compute_derived_metrics(data):
"""计算派生指标:FCF Yield、ROIC、股东收益率等"""
metrics = {}
info = data.get("info", {})
market_cap = info.get("market_cap") or info.get("marketCap")
currency = info.get("currency", "USD")
cf = data.get("cash_flow", [])
is_ = data.get("income_statement", [])
bs = data.get("balance_sheet", [])
if market_cap and cf:
# 最近一年的 FCF Yield
latest_cf = cf[0] if cf else {}
fcf = latest_cf.get("free_cash_flow")
true_earnings = latest_cf.get("true_owner_earnings")
shareholder_return = latest_cf.get("total_shareholder_return")
if fcf and market_cap > 0:
metrics["fcf_yield_pct"] = round(abs(fcf) / market_cap * 100, 2)
if true_earnings and market_cap > 0:
metrics["true_owner_yield_pct"] = round(abs(true_earnings) / market_cap * 100, 2)
if shareholder_return and market_cap > 0:
metrics["shareholder_yield_pct"] = round(shareholder_return / market_cap * 100, 2)
# ROIC 计算(最近一年)
if is_ and bs:
latest_is = is_[0] if is_ else {}
latest_bs = bs[0] if bs else {}
op_income = latest_is.get("operating_income") or latest_is.get("ebit")
total_debt = latest_bs.get("total_debt", 0) or 0
equity = latest_bs.get("shareholders_equity", 0) or 0
# ⚠️ [2026-05-03] 修正: 优先使用 total_cash_and_st_investments (含短期投资)
# 之前只用了 cash_and_equivalents, 导致 BZ 等中概股漏掉 ¥158亿理财产品
total_cash = (latest_bs.get("total_cash_and_st_investments")
or (latest_bs.get("cash_and_equivalents", 0) or 0)
+ (latest_bs.get("short_term_investments", 0) or 0)
+ (latest_bs.get("other_short_term_investments", 0) or 0))
if op_income is not None and (total_debt + equity - total_cash) != 0:
# 简单税率假设 20%
nopat = op_income * 0.8
invested_capital = (total_debt or 0) + (equity or 0) - total_cash
if invested_capital > 0:
metrics["roic_pct"] = round(nopat / invested_capital * 100, 2)
# ROIC > 200% 通常是净现金≈权益导致的分母过小,非真实超额回报
if metrics["roic_pct"] > 200:
metrics["_warnings"] = metrics.get("_warnings", []) + [
f"⚠️ ROIC={metrics['roic_pct']:.0f}% > 200%, "
f"因投入资本(¥{invested_capital/1e6:.0f}M)过小(净现金≈权益),"
f"ROIC分母失真。请改用 tangible ROIC 或剔除冗余现金后重新计算"
]
# 毛利率趋势
gross_margins = []
for rec in is_[:3]:
rev = rec.get("revenue")
gp = rec.get("gross_profit")
if rev and gp and rev > 0:
gross_margins.append(round(gp / rev * 100, 1))
if gross_margins:
metrics["gross_margin_trend_pct"] = gross_margins
metrics["gross_margin_avg_3y_pct"] = round(sum(gross_margins) / len(gross_margins), 1)
# 净利率趋势
net_margins = []
for rec in is_[:3]:
rev = rec.get("revenue")
ni = rec.get("net_income")
if rev and ni and rev > 0:
net_margins.append(round(ni / rev * 100, 1))
if net_margins:
metrics["net_margin_trend_pct"] = net_margins
# 营收增长率
revenues = [r.get("revenue") for r in is_[:3] if r.get("revenue")]
if len(revenues) >= 2:
growth = (revenues[0] - revenues[1]) / abs(revenues[1]) * 100
metrics["revenue_growth_yoy_pct"] = round(growth, 1)
# 现金占市值比
if bs and market_cap:
latest = bs[0]
total_cash = (latest.get("total_cash_and_st_investments")
or (latest.get("cash_and_equivalents") or 0)
+ (latest.get("short_term_investments") or 0)
+ (latest.get("other_short_term_investments") or 0))
if total_cash and market_cap > 0:
metrics["cash_to_market_cap_pct"] = round(total_cash / market_cap * 100, 1)
# ⚠️ 数据合理性检查: 如果现金/市值比例异常高, 告警
if total_cash / market_cap > 1.0:
metrics["_warnings"] = metrics.get("_warnings", []) + [
f"⚠️ 现金/市值={total_cash/market_cap*100:.0f}% > 100%, 请检查币种一致性: "
f"现金在{info.get('currency','?')}, 市值在USD"
]
# SBC 占营收比
sbc_ratios = []
for i, cf_rec in enumerate(cf[:3]):
sbc = cf_rec.get("stock_based_compensation")
rev = is_[i].get("revenue") if i < len(is_) else None
if sbc and rev and rev > 0:
sbc_ratios.append(round(abs(sbc) / rev * 100, 2))
if sbc_ratios:
metrics["sbc_to_revenue_pct"] = sbc_ratios
# ═══════════════════════════════════════════════════
# 资产负债表完整性校验
# ═══════════════════════════════════════════════════
if bs:
latest = bs[0]
ta = latest.get("total_assets")
if ta and ta > 0:
# 尝试从可识别的分项推算总资产
asset_parts = [
latest.get(k) for k in
["total_current_assets", "total_non_current_assets"]
if latest.get(k) is not None
]
if len(asset_parts) == 2:
expected = sum(asset_parts)
diff_pct = abs(expected - ta) / ta * 100
if diff_pct > 5:
metrics["_warnings"] = metrics.get("_warnings", []) + [
f"⚠️ 资产负债表完整性: current+non-current={expected:,} vs total_assets={ta:,} 差异{diff_pct:.0f}%"
]
return metrics
# ═══════════════════════════════════════════════════════
# Markdown 格式化输出
# ═══════════════════════════════════════════════════════
def to_markdown(data):
"""输出人类可读的 Markdown 摘要"""
info = data.get("info", {})
lines = []
lines.append(f"# 财务数据摘要 — {data['ticker']}")
lines.append(f"")
lines.append(f"**{info.get('company_name', 'N/A')}**")
lines.append(f"")
lines.append(f"| 指标 | 数值 |")
lines.append(f"|------|------|")
lines.append(f"| 股价 | {info.get('current_price', 'N/A')} {info.get('currency', '')} |")
lines.append(f"| 市值 | {_fmt_large(info.get('market_cap'))} |")
lines.append(f"| 已发行股份 | {_fmt_large(info.get('shares_outstanding'))} |")
lines.append(f"| 行业 | {info.get('sector', 'N/A')} / {info.get('industry', 'N/A')} |")
lines.append(f"| PE(TTM) | {info.get('trailing_pe', 'N/A')} |")
lines.append(f"| 数据源 | {data.get('source', 'yfinance')} |")
lines.append(f"")
lines.append(f"---")
lines.append(f"")
# 利润表
is_ = data.get("income_statement", [])
if is_:
lines.append(f"## 利润表")
lines.append(f"")
headers = ["科目"] + [r["fiscal_year"] for r in is_]
lines.append(f"| {' | '.join(headers)} |")
lines.append(f"| {' | '.join(['---'] * len(headers))} |")
rows = ["revenue", "gross_profit", "operating_income", "net_income",
"basic_eps", "diluted_eps", "diluted_shares"]
labels = ["营收", "毛利润", "营业利润", "净利润", "基本EPS", "稀释EPS", "稀释股份"]
for row, label in zip(rows, labels):
vals = [label]
for r in is_:
v = r.get(row)
vals.append(_fmt_val(v))
lines.append(f"| {' | '.join(vals)} |")
lines.append(f"")
# 现金流量表
cf = data.get("cash_flow", [])
if cf:
lines.append(f"## 现金流量表")
lines.append(f"")
headers = ["科目"] + [r["fiscal_year"] for r in cf]
lines.append(f"| {' | '.join(headers)} |")
lines.append(f"| {' | '.join(['---'] * len(headers))} |")
rows = [("operating_cash_flow", "经营现金流"),
("capital_expenditures", "资本开支"),
("free_cash_flow", "自由现金流"),
("stock_based_compensation", "股权激励(SBC)"),
("true_owner_earnings", "真实所有者盈余(扣SBC)"),
("share_repurchases", "股票回购"),
("dividends_paid", "分红(总额)"),
("common_dividends", "普通股分红"),
("total_shareholder_return", "股东总回报(含回购)"),
("depreciation_and_amortization", "折旧摊销")]
for row_key, label in rows:
vals = [label]
for r in cf:
v = r.get(row_key)
vals.append(_fmt_val(v))
lines.append(f"| {' | '.join(vals)} |")
lines.append(f"")
# 资产负债表
bs = data.get("balance_sheet", [])
if bs:
lines.append(f"## 资产负债表")
lines.append(f"")
headers = ["科目"] + [r["fiscal_year"] for r in bs]
lines.append(f"| {' | '.join(headers)} |")
lines.append(f"| {' | '.join(['---'] * len(headers))} |")
rows = [("cash_and_equivalents", "现金"),
("short_term_investments", "短期投资"),
("accounts_receivable", "应收账款"),
("inventory", "存货"),
("goodwill_and_intangibles", "商誉+无形资产"),
("total_current_assets", "流动资产合计"),
("pp_and_e", "固定资产(PP&E)"),
("total_assets", "总资产"),
("total_debt", "总负债"),
("shareholders_equity", "股东权益"),
("book_value_per_share", "每股账面价值")]
for row_key, label in rows:
vals = [label]
for r in bs:
v = r.get(row_key)
vals.append(_fmt_val(v))
lines.append(f"| {' | '.join(vals)} |")
lines.append(f"")
# 派生指标
metrics = data.get("derived_metrics", {})
if metrics:
lines.append(f"## 派生指标")
lines.append(f"")
for k, v in metrics.items():
label = k.replace("_pct", "%").replace("_", " ").title()
if isinstance(v, list):
lines.append(f"- **{label}**: {v}")
else:
lines.append(f"- **{label}**: {v}")
lines.append(f"")
# 标记缺失字段
lines.append(f"## 数据完整性")
lines.append(f"")
missing = []
for section_name, section in [("利润表", is_), ("资产负债表", bs), ("现金流量表", cf)]:
if not section:
missing.append(f"- ❌ **{section_name}**: 无数据")
if missing:
lines.extend(missing)
lines.append(f"")
lines.append(f"> ⚠️ 以上缺失数据需要通过 web 搜索补充。")
else:
lines.append(f"- ✅ 三张报表数据完整")
lines.append(f"")
return "\n".join(lines)
def _fmt_large(v):
if v is None:
return "N/A"
try:
v = float(v)
if abs(v) >= 1e12:
return f"${v/1e12:.2f}T"
elif abs(v) >= 1e9:
return f"${v/1e9:.2f}B"
elif abs(v) >= 1e6:
return f"${v/1e6:.2f}M"
else:
return f"${v:.2f}"
except (TypeError, ValueError):
return str(v)
def _fmt_val(v):
if v is None:
return "N/A"
try:
v = float(v)
if abs(v) >= 1e9:
return f"{v/1e9:.2f}B"
elif abs(v) >= 1e6:
return f"{v/1e6:.2f}M"
elif v == int(v):
return str(int(v))
else:
return f"{v:.2f}"
except (TypeError, ValueError):
return str(v)
# ═══════════════════════════════════════════════════════
# Baostock 补充数据(A股分红 + 指数成分股 + 行业)
# ═══════════════════════════════════════════════════════
def enrich_with_baostock(data):
"""
使用 Baostock 补充 A 股数据(针对 .SS / .SZ 后缀的代码)
补充内容:
- 分红数据(每股税前分红、除权日)
- 行业分类
返回补充后的 data 字典(新增 "baostock" key)
"""
ticker = data.get("ticker", "")
# 只对 A 股执行
is_a_share = ticker.endswith(".SS") or ticker.endswith(".SZ")
if not is_a_share:
return data
# 去掉后缀获取纯代码
raw_code = ticker.replace(".SS", "").replace(".SZ", "")
# 补上 sh/sz 前缀
prefix = "sh" if ticker.endswith(".SS") else "sz"
bs_code = f"{prefix}.{raw_code}"
try:
import baostock as bs
lg = bs.login()
if lg.error_code != "0":
print(f"[baostock] 登录失败: {lg.error_msg}", file=sys.stderr)
return data
bs_result = {}
# ── 1. 分红数据(近3年) ──
dividends = []
for year in ["2024", "2023", "2022"]:
try:
rs = bs.query_dividend_data(code=bs_code, year=year)
df = rs.get_data()
if df is not None and not df.empty:
for _, row in df.iterrows():
d = {
"year": year,
"announce_date": row.get("dividPlanAnnounceDate", ""),
"ex_date": row.get("dividOperateDate", ""),
"cash_before_tax": _safe_float_bs(row.get("dividCashPsBeforeTax")),
"cash_after_tax": row.get("dividCashPsAfterTax", ""),
"description": row.get("dividCashStock", ""),
}
if d["cash_before_tax"] is not None:
dividends.append(d)
except Exception as e:
print(f"[baostock] {year}年分红数据获取失败: {e}", file=sys.stderr)
if dividends:
bs_result["dividends"] = dividends
# 计算最近一年分红率
latest_div = dividends[0].get("cash_before_tax")
if latest_div:
bs_result["latest_dividend_per_share"] = latest_div
# 从 info 获取股价算股息率
price = data.get("info", {}).get("current_price")
if price and price > 0:
bs_result["dividend_yield_pct"] = round(latest_div / price * 100, 2)
# ── 2. 行业分类 ──
try:
rs = bs.query_stock_basic(code=bs_code)
df = rs.get_data()
if df is not None and not df.empty:
row = df.iloc[0]
bs_result["ipo_date"] = str(row.get("ipoDate", ""))
bs_result["status"] = str(row.get("status", ""))
except Exception as e:
print(f"[baostock] 股票基本信息获取失败: {e}", file=sys.stderr)
# ── 3. 行业归属 ──
try:
rs = bs.query_stock_industry()
df = rs.get_data()
if df is not None and not df.empty:
match = df[df["code"] == bs_code]
if not match.empty:
bs_result["industry"] = str(match.iloc[0].get("industryName", ""))
except Exception as e:
print(f"[baostock] 行业信息获取失败: {e}", file=sys.stderr)
bs.logout()
if bs_result:
data["baostock"] = bs_result
print(f"[baostock] ✅ 补充数据: {list(bs_result.keys())}")
except ImportError:
print("[baostock] ⚠️ baostock 未安装,跳过 (pip install baostock)")
except Exception as e:
print(f"[baostock] ⚠️ 执行失败: {e}", file=sys.stderr)
return data
def _safe_float_bs(v):
"""Baostock 返回的数值安全转换"""
if v is None or v == "" or v == "-":
return None
try:
return float(v)
except (ValueError, TypeError):
return None
# ═══════════════════════════════════════════════════════
# Tushare 补充数据(A股分红 + 前十大股东)
# ═══════════════════════════════════════════════════════
def enrich_with_tushare(data):
"""
使用 Tushare Pro 补充 A 股数据。
可用接口(120积分):
- dividend(分红明细,含除权除息)
- top10_holders(前十大股东,含持股变动)
返回补充后的 data 字典(新增 "tushare" key)
仅在 Tushare 可用且结果为高质量时覆盖 Baostock 的同类数据。
"""
ticker = data.get("ticker", "")
is_a_share = ticker.endswith(".SS") or ticker.endswith(".SZ")
if not is_a_share:
return data
try:
import tushare as ts
except ImportError:
print("[tushare] ⚠️ tushare 未安装,跳过 (pip install tushare)")
return data
try:
token = _read_api_key("tushare_token")
if not token:
print("[tushare] ⚠️ 未找到 tushare_token,跳过")
return data
ts.set_token(token)
pro = ts.pro_api()
result = {}
# A 股代码格式转换:002327.SZ → 002327.SZ(Tushare也接受)
ts_code = ticker
# ── 1. 分红数据(优先于 Baostock) ──
try:
df = pro.dividend(ts_code=ts_code, start_date="20210101", end_date="20261231")
if df is not None and not df.empty:
div_list = []
for _, row in df.iterrows():
cash = row.get("cash_div_tax")
if cash is not None and cash != "":
cash = float(cash)
else:
continue
div_list.append({
"end_date": str(row.get("end_date", "")),
"ann_date": str(row.get("ann_date", "")),
"cash_div_tax": cash,
"stk_div": row.get("stk_div", 0),
"div_proc": str(row.get("div_proc", "")),
})
if div_list:
result["dividends"] = div_list
# 最新一期分红
latest = div_list[0]
latest_cash = latest["cash_div_tax"]
result["latest_dividend_per_share"] = latest_cash
price = data.get("info", {}).get("current_price")
if price and price > 0:
result["dividend_yield_pct"] = round(latest_cash / price * 100, 2)
print(f"[tushare] ✅ 分红: {len(div_list)} 期, 最新¥{latest_cash:.2f}/股")
except Exception as e:
print(f"[tushare] ⚠️ 分红获取失败: {e}", file=sys.stderr)
# ── 2. 前十大股东(新增数据源) ──
try:
df = pro.top10_holders(ts_code=ts_code, start_date="20240101", end_date="20261231")
if df is not None and not df.empty:
holders = []
for _, row in df.iterrows():
hold_amt = row.get("hold_amount", 0)
if hold_amt is not None and hold_amt != "":
hold_amt = float(hold_amt)
holders.append({
"ann_date": str(row.get("ann_date", "")),
"end_date": str(row.get("end_date", "")),
"holder_name": str(row.get("holder_name", "")),
"hold_amount": hold_amt,
"hold_ratio": row.get("hold_ratio"),
})
if holders:
result["top10_holders"] = holders
print(f"[tushare] ✅ 前十大股东: {len(holders)} 条记录")
except Exception as e:
print(f"[tushare] ⚠️ 前十大股东获取失败: {e}", file=sys.stderr)
# ── 3. 写入数据 ──
if result:
data["tushare"] = result
print(f"[tushare] ✅ 补充数据: {list(result.keys())}")
else:
print("[tushare] ℹ️ 无有效数据返回")
except Exception as e:
print(f"[tushare] ⚠️ 执行失败: {e}", file=sys.stderr)
return data
# ═══════════════════════════════════════════════════════
# Baostock 成分股 + K线工具
# ═══════════════════════════════════════════════════════
def get_index_constituents_bs(index="hs300"):
"""
使用 Baostock 获取指数成分股
返回 [(code_ss, name), ...] 其中 code_ss 是 sh.601398 格式
"""
import baostock as bs
lg = bs.login()
func_map = {
"hs300": bs.query_hs300_stocks,
"sz50": bs.query_sz50_stocks,
"zz500": bs.query_zz500_stocks,
}
query_func = func_map.get(index)
if not query_func:
print(f"[baostock] 未知指数: {index}")
bs.logout()
return []
try:
rs = query_func()
df = rs.get_data()
bs.logout()
if df is None or df.empty:
return []
results = []
for _, row in df.iterrows():
code = str(row.get("code", ""))
name = str(row.get("code_name", ""))
# 转换成 yfinance 兼容的格式
if code.startswith("sh."):
yahoo_code = code.replace("sh.", "") + ".SS"
elif code.startswith("sz."):
yahoo_code = code.replace("sz.", "") + ".SZ"
else:
yahoo_code = code
results.append((yahoo_code, name, code))
print(f"[baostock] {index}: {len(results)} 只成分股")
return results
except Exception as e:
print(f"[baostock] 获取 {index} 失败: {e}", file=sys.stderr)
bs.logout()
return []
# ═══════════════════════════════════════════════════════
# SEC EDGAR 数据补充(美股定性信息)
# ═══════════════════════════════════════════════════════
def enrich_with_sec(data):
"""
使用 SEC EDGAR 补充美股定性信息(仅对美股生效)
补充内容:
- 最新 10-K 的 Business Description(业务描述)
- Risk Factors(风险因素)摘要
- MD&A(管理层讨论与分析)
- Executive Compensation(高管薪酬,含SBC详情)
数据来源:SEC EDGAR (data.sec.gov) —— 免费、无需注册
"""
ticker = data.get("ticker", "")
# 仅对美股执行(不包含 .SS / .SZ / .HK)
if any(ticker.endswith(s) for s in [".SS", ".SZ", ".HK"]):
return data
try:
import urllib.request
import json as jsonlib
import re
sec_data = {}
headers = {
"User-Agent": "Hermes Financial Research (your-email@example.com)",
"Accept": "application/json",
}
# ── Step 1: 查找 CIK ──
cik = _sec_lookup_cik(ticker, headers)
if not cik:
print(f"[sec-edgar] ⚠️ 未找到 {ticker} 的 CIK 编号", file=sys.stderr)
return data
sec_data["cik"] = cik
cik_padded = str(cik).zfill(10)
# ── Step 2: 获取最近 10-K 提交信息 ──
filing_info = _sec_get_filing_info(cik_padded, headers)
if not filing_info:
print(f"[sec-edgar] ⚠️ 未找到 {ticker} 的近期 10-K", file=sys.stderr)
data["sec_edgar"] = sec_data
return data
sec_data["latest_10k"] = filing_info
# ── Step 3: 下载 10-K 文本并提取关键章节 ──
sections = _sec_extract_sections(filing_info["filing_url"], headers)
if sections:
sec_data["sections"] = sections
for s_name, s_text in sections.items():
char_count = len(s_text) if s_text else 0
print(f"[sec-edgar] {s_name}: {char_count:,} 字符", file=sys.stderr)
data["sec_edgar"] = sec_data
print(f"[sec-edgar] ✅ {ticker} (CIK={cik}): 10-K({filing_info.get('period','')})")
except ImportError:
print("[sec-edgar] ⚠️ 需要 Python 标准库 urllib")
except Exception as e:
print(f"[sec-edgar] ⚠️ 执行失败: {e}", file=sys.stderr)
return data
def _sec_lookup_cik(ticker, headers):
"""通过 ticker 查找 CIK 编号"""
import urllib.request
import json as jsonlib
# 使用 SEC 官方 ticker→CIK 映射文件(最可靠)
try:
url = "https://www.sec.gov/files/company_tickers.json"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read().decode("utf-8")
mapping = jsonlib.loads(raw)
ticker_upper = ticker.upper()
for key, entry in mapping.items():
if entry.get("ticker", "").upper() == ticker_upper:
return str(entry["cik_str"])
return None
except Exception:
pass
# 备用:通过 SEC EDGAR 全文搜索 API
try:
url = f"https://efts.sec.gov/LATEST/search-index?q={ticker}+AND+ticker&dateRange=all&counts=5"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as resp:
raw = resp.read().decode("utf-8")
data = jsonlib.loads(raw)
if data.get("hits", {}).get("total", {}).get("value", 0) > 0:
hits = data["hits"]["hits"]
for hit in hits:
source = hit.get("_source", {})
tickers_list = source.get("tickers", [])
if ticker.upper() in [t.upper() for t in tickers_list]:
ciks = source.get("ciks", [])
if ciks:
return ciks[0]
return None
except Exception:
return None
def _sec_get_filing_info(cik_padded, headers):
"""
获取最近 10-K 提交信息
返回 {period, accession, filing_url} 或 None
"""
import urllib.request
import json as jsonlib
url = f"https://data.sec.gov/submissions/CIK{cik_padded}.json"
try:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read().decode("utf-8")
data = jsonlib.loads(raw)
filings = data.get("filings", {}).get("recent", {})
if not filings:
return None
forms = filings.get("form", [])
for i, form in enumerate(forms):
if form in ("10-K", "10-K/A"):
accession = filings["accessionNumber"][i]
primary_doc = filings["primaryDocument"][i]
period = filings.get("reportDate", [""])[i]
filing_url = f"https://www.sec.gov/Archives/edgar/data/{cik_padded.lstrip('0')}/{accession.replace('-','')}/{primary_doc}"
return {
"period": period,
"accession": accession,
"primary_document": primary_doc,
"filing_url": filing_url,
"form": form,
}
return None
except Exception as e:
print(f"[sec-edgar] 获取提交信息失败: {e}", file=sys.stderr)
return None
def _sec_extract_sections(filing_url, headers):
"""
下载 10-K HTML 并提取关键章节
返回 dict:
{
"business_description": "...",
"risk_factors": "...(最多2000字摘要)...",
"md_and_a": "...(最多3000字摘要)...",
"executive_compensation": "...(最多2000字摘要)..."
}
"""
import urllib.request
import re
try:
req = urllib.request.Request(filing_url, headers=headers)
with urllib.request.urlopen(req, timeout=30) as resp:
html = resp.read().decode("utf-8", errors="replace")
except Exception as e:
print(f"[sec-edgar] 下载 10-K 失败: {e}", file=sys.stderr)
return None
# 去除 HTML 标签
text = re.sub(r"<[^>]+>", " ", html)
text = re.sub(r"\s+", " ", text)
text = text.replace(" ", " ").replace("&", "&")
sections = {}
# ── 提取 Business (Item 1) ──
sections["business_description"] = _sec_extract_section_text(
text,
[r"ITEM\s+1\.\s*BUSINESS", r"ITEM\s+1[.,]\s*BUSINESS"],
[r"ITEM\s+1A\.", r"ITEM\s+1[.,]\s*RISK\s+FACTORS", r"ITEM\s+2\."],
max_chars=100000,
)
# ── 提取 Risk Factors (Item 1A) ──
sections["risk_factors"] = _sec_extract_section_text(
text,
[r"ITEM\s+1A\.\s*RISK\s+FACTORS", r"ITEM\s+1A[.,]\s*RISK"],
[r"ITEM\s+1B\.", r"ITEM\s+2\."],
max_chars=100000,
)
# ── 提取 MD&A (Item 7) ──
sections["md_and_a"] = _sec_extract_section_text(
text,
[r"ITEM\s+7\.\s*MANAGEMENT", r"ITEM\s+7[.,]\s*MANAGEMENT"],
[r"ITEM\s+7A\.", r"ITEM\s+8\."],
max_chars=100000,
)
# ── 提取 Executive Compensation (Item 11) ──
sections["executive_compensation"] = _sec_extract_section_text(
text,
[r"ITEM\s+11\.\s*EXECUTIVE", r"ITEM\s+11[.,]\s*EXECUTIVE"],
[r"ITEM\s+12\."],
max_chars=100000,
)
# 移除空章节
return {k: v for k, v in sections.items() if v}
def _sec_extract_section_text(text, start_patterns, end_patterns, max_chars=3000):
"""从 10-K 文本中提取特定章节"""
import re
start_idx = None
for pat in start_patterns:
m = re.search(pat, text, re.IGNORECASE)
if m:
start_idx = m.start()
break
if start_idx is None:
return None
end_idx = len(text)
for pat in end_patterns:
m = re.search(pat, text[start_idx + 200:], re.IGNORECASE)
if m:
end_idx = start_idx + 200 + m.start()
break
# 限制长度
raw_text = text[start_idx:end_idx].strip()
if len(raw_text) > max_chars:
raw_text = raw_text[:max_chars] + "..."
return raw_text
# ═══════════════════════════════════════════════════════
# Finnhub 补充数据(美股 Earnings Call + 新闻)
# ═══════════════════════════════════════════════════════
def enrich_with_finnhub(data):
"""
使用 Finnhub API 补充美股数据
补充内容:
- 最新 Earnings Call Transcript(管理层在电话会上的真实发言)
- 近期公司新闻
- 内部交易数据(管理层买卖股票信号)
需要 finnhub-python 包 + config/api_keys.json 中的 API Key
"""
ticker = data.get("ticker", "")
if any(ticker.endswith(s) for s in [".SS", ".SZ", ".HK"]):
return data
# 读取 API Key
api_key = _read_api_key("finnhub_api_key")
if not api_key:
print("[finnhub] ⚠️ 未配置 API Key", file=sys.stderr)
return data
try:
import finnhub
fh_data = {}
finnhub_client = finnhub.Client(api_key=api_key)
# ── 1. 最新 Earnings Call Transcript ──
try:
# 查找最近一期财报的日期
info = data.get("info", {})
# 先从公司财报日历获取最新财报日期
earnings = finnhub_client.earnings_calendar(
_from="2025-01-01", to="2026-12-31", symbol=ticker
)
if earnings and earnings.get("earningsCalendar"):
for e in earnings["earningsCalendar"]:
eps_actual = e.get("epsActual")
if eps_actual is not None:
report_date = e.get("date", "")
eps_est = e.get("epsEstimate", "N/A")
revenue_est = e.get("revenueEstimate", "N/A")
revenue_actual = e.get("revenueActual", "N/A")
fh_data["latest_earnings"] = {
"date": report_date,
"eps_actual": eps_actual,
"eps_estimate": eps_est,
"revenue_actual": revenue_actual,
"revenue_estimate": revenue_est,
}
# 基于财报日期获取 Transcript
try:
if report_date:
trans = finnhub_client.earnings_call_transcript(
ticker, report_date
)
if trans and trans.get("transcript"):
fh_data["earnings_transcript"] = trans["transcript"][:5000]
except Exception:
pass
break
except Exception as e:
print(f"[finnhub] Earnings Call 获取失败: {e}", file=sys.stderr)
# ── 2. 近期公司新闻(近30天,取前5条) ──
try:
from datetime import datetime, timedelta
end_date = datetime.now().strftime("%Y-%m-%d")
start_date = (datetime.now() - timedelta(days=90)).strftime("%Y-%m-%d")
news = finnhub_client.company_news(ticker, _from=start_date, to=end_date)
if news and len(news) > 0:
fh_data["recent_news"] = [
{
"date": n.get("datetime", ""),
"headline": n.get("headline", ""),
"summary": n.get("summary", "")[:300],
"url": n.get("url", ""),
"source": n.get("source", ""),
}
for n in news[:5]
]
except Exception as e:
print(f"[finnhub] 新闻获取失败: {e}", file=sys.stderr)
# ── 3. 内部交易(管理层买卖信号) ──
try:
insider = finnhub_client.stock_insider_transactions(ticker, "US")
if insider and insider.get("data"):
fh_data["insider_transactions"] = [
{
"name": t.get("name", ""),
"share": t.get("share", 0),
"change": t.get("change", 0),
"transaction_price": t.get("transactionPrice", 0),
"transaction_date": t.get("transactionDate", ""),
}
for t in insider["data"][:5]
]
except Exception as e:
print(f"[finnhub] 内部交易获取失败: {e}", file=sys.stderr)
if fh_data:
data["finnhub"] = fh_data
print(f"[finnhub] ✅ {list(fh_data.keys())}")
except ImportError:
print("[finnhub] ⚠️ finnhub-python 未安装 (pip install finnhub-python)")
except Exception as e:
print(f"[finnhub] ⚠️ 执行失败: {e}", file=sys.stderr)
return data
def _read_api_key(key_name):
"""从环境变量、skill 本地配置和 Hermes 旧配置读取 API Key"""
import json
env_names = {
"finnhub_api_key": ["FINNHUB_API_KEY", "FINNHUB_TOKEN"],
"fred_api_key": ["FRED_API_KEY"],
"tushare_token": ["TUSHARE_TOKEN"],
"mx_api_key": ["MX_APIKEY", "MX_API_KEY"],
"polygon_api_key": ["POLYGON_API_KEY"],
}
for env_name in env_names.get(key_name, []):
value = os.environ.get(env_name)
if value:
return value
script_path = Path(__file__).resolve()
skill_root = script_path.parents[2]
config_paths = [
script_path.parent.parent / "config" / "api_keys.json",
skill_root / ".env.local",
Path.home() / ".hermes" / "skills" / "data-science" /
"financial-data-acquisition" / "config" / "api_keys.json",
Path.home() / ".hermes" / ".env",
]
def load_env_file(path):
data = {}
for raw_line in path.read_text(errors="ignore").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
data[key.strip()] = value.strip().strip('"').strip("'")
return data
for path in config_paths:
if not path.exists():
continue
try:
if path.suffix == ".json":
config = json.loads(path.read_text())
value = config.get(key_name, "")
if value:
return value
for env_name in env_names.get(key_name, []):
value = config.get(env_name, "")
if value:
return value
else:
config = load_env_file(path)
for env_name in env_names.get(key_name, []):
value = config.get(env_name, "")
if value:
return value
except Exception:
continue
return ""
# ═══════════════════════════════════════════════════════
# FRED 宏观经济数据补充(无风险利率、CPI等)
# ═══════════════════════════════════════════════════════
def enrich_with_fred(data):
"""
使用 FRED(圣路易斯联储)API 补充宏观经济数据
补充内容:
- DGS10: 10年期国债收益率(用于 WACC 计算)
- CPIAUCSL: CPI 通胀率
- GDP: GDP 增长率
- UNRATE: 失业率
- FEDFUNDS: 联邦基金利率
需要 fredapi 包 + config/api_keys.json 中的 FRED API Key
"""
api_key = _read_api_key("fred_api_key")
if not api_key:
print("[fred] ⚠️ 未配置 API Key", file=sys.stderr)
return data
try:
from fredapi import Fred
import json
fred_data = {}
fred = Fred(api_key=api_key)
# 核心宏数据系列
series = {
"DGS10": "10y_treasury_yield_pct", # 10年期国债收益率
"FEDFUNDS": "fed_funds_rate_pct", # 联邦基金利率
"CPIAUCSL": "cpi_yoy_change_pct", # CPI(需要算同比)
"UNRATE": "unemployment_rate_pct", # 失业率
"GDP": "gdp_billions", # GDP(名义)
}
# 获取每个系列的最近值
for fred_code, output_key in series.items():
try:
series_data = fred.get_series(fred_code)
if series_data is not None and not series_data.empty:
latest = series_data.iloc[-1]
# CPI需要算同比变化
if fred_code == "CPIAUCSL":
if len(series_data) >= 13:
year_ago = series_data.iloc[-13]
latest_val = round((latest / year_ago - 1) * 100, 2)
else:
latest_val = round(float(latest), 2)
elif fred_code == "GDP":
latest_val = round(float(latest) / 1000, 1) # 转为万亿美元
else:
latest_val = round(float(latest), 2)
fred_data[output_key] = latest_val
except Exception as e:
print(f"[fred] {fred_code} 获取失败: {e}", file=sys.stderr)
if fred_data:
data["fred"] = fred_data
print(f"[fred] ✅ {list(fred_data.keys())}")
except ImportError:
print("[fred] ⚠️ fredapi 未安装 (pip install fredapi)")
except Exception as e:
print(f"[fred] ⚠️ 执行失败: {e}", file=sys.stderr)
return data
# ═══════════════════════════════════════════════════════
# CLI 入口
# ═══════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Fetch 5-year financial data for a ticker")
parser.add_argument("--ticker", type=str, required=True,
help="Stock ticker symbol (e.g., AAPL, GOOGL)")
parser.add_argument("--output", type=str, default=None,
help="Output JSON file path")
parser.add_argument("--format", type=str, default="json",
choices=["json", "markdown"],
help="Output format (default: json)")
parser.add_argument("--years", type=int, default=5,
help="Number of years of data (default: 5)")
args = parser.parse_args()
print(f"[fetch] 正在获取 {args.ticker} 的财务数据...")
# ── 采集 ──
data = extract_financials(args.ticker)
# ── 派生指标 ──
metrics = compute_derived_metrics(data)
data["derived_metrics"] = metrics
# ── 数据预警 ──
warnings = metrics.get("_warnings", [])
for w in warnings:
print(f"[fetch] {w}", file=sys.stderr)
# ── Baostock 补充(A股分红数据) ──
data = enrich_with_baostock(data)
# ── Tushare 补充(A股分红 + 前十大股东,优先于 Baostock) ──
data = enrich_with_tushare(data)
# ── SEC EDGAR 补充(美股10-K定性信息) ──
data = enrich_with_sec(data)
# ── Finnhub 补充(美股Earnings Call + 新闻) ──
data = enrich_with_finnhub(data)
# ── FRED 补充(宏观经济数据) ──
data = enrich_with_fred(data)
# ── 数据完整性报告 ──
is_count = len(data.get("income_statement", []))
bs_count = len(data.get("balance_sheet", []))
cf_count = len(data.get("cash_flow", []))
print(f"[fetch] 利润表: {is_count} 年 | 资产负债表: {bs_count} 年 | 现金流量表: {cf_count} 年")
print(f"[fetch] 派生指标: {len(metrics)} 项")
if is_count == 0 or bs_count == 0 or cf_count == 0:
print(f"[fetch] ⚠️ 部分报表为空,需要通过 web 搜索补充")
# ── 输出 ──
if args.format == "markdown":
output = to_markdown(data)
else:
output = json.dumps(data, indent=2, default=str)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"[fetch] ✅ 已写入 {args.output}")
else:
print(output)
if __name__ == "__main__":
main()
scripts/notion-upload/upload_to_notion.py#!/usr/bin/env python3
"""
upload_to_notion.py — 将投资报告上传到 Notion(子页面形式)
用法:
python3 upload_to_notion.py --ticker DUOL
python3 upload_to_notion.py --files "report1.md,report2.md"
"""
import json
import os
import re
import sys
import urllib.request
NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "")
NOTION_VERSION = "2022-06-28"
API_BASE = "https://api.notion.com/v1"
def notion_request(method, path, data=None):
url = f"{API_BASE}/{path}"
headers = {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
body = json.dumps(data).encode() if data else None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
err_body = e.read().decode()
print(f" ❌ API 错误 {e.code}: {err_body}", file=sys.stderr)
return None
def extract_page_id(url_or_id):
if not url_or_id:
return None
url_or_id = url_or_id.strip().rstrip("/")
if re.match(r"^[a-f0-9]{32}$", url_or_id):
return url_or_id
if re.match(r"^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", url_or_id):
return url_or_id.replace("-", "")
m = re.search(r"([a-f0-9]{32})", url_or_id)
if m:
return m.group(1)
m = re.search(r"([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})", url_or_id)
if m:
return m.group(1).replace("-", "")
return url_or_id
def rich_text(text):
parts = []
segments = re.split(r"(\*\*.*?\*\*)", text)
for seg in segments:
if seg.startswith("**") and seg.endswith("**"):
parts.append({"type": "text", "text": {"content": seg[2:-2]}, "annotations": {"bold": True}})
elif seg.strip():
parts.append({"type": "text", "text": {"content": seg}})
if not parts:
parts.append({"type": "text", "text": {"content": text or " "}})
return parts
def md_to_notion_blocks(md_text):
lines = md_text.split("\n")
blocks = []
i = 0
table_buffer = []
def flush_table():
nonlocal table_buffer
if not table_buffer:
return
max_cols = max(len(row) for row in table_buffer)
rows = []
for row_parts in table_buffer:
cells = row_parts[:max_cols]
while len(cells) < max_cols:
cells.append("")
cells_formatted = [[{"type": "text", "text": {"content": c.strip()}}] for c in cells]
rows.append({"type": "table_row", "table_row": {"cells": cells_formatted}})
if rows:
blocks.append({
"object": "block", "type": "table",
"table": {"table_width": max_cols, "children": rows}
})
table_buffer = []
while i < len(lines):
line = lines[i]
stripped = line.strip()
if not stripped:
flush_table()
i += 1
continue
if stripped.startswith("|") and stripped.endswith("|"):
cells = [c.strip() for c in stripped.split("|")[1:-1]]
if all(re.match(r"^[-:\s]+$", c) for c in cells):
i += 1
continue
table_buffer.append(cells)
i += 1
continue
else:
flush_table()
if stripped.startswith("```"):
lang = stripped[3:].strip()
code_lines = []
i += 1
while i < len(lines) and not lines[i].strip().startswith("```"):
code_lines.append(lines[i])
i += 1
code_text = "\n".join(code_lines)
blocks.append({
"object": "block", "type": "code",
"code": {"language": lang or "plain text",
"rich_text": [{"type": "text", "text": {"content": code_text}}]}
})
i += 1
continue
if re.match(r"^---+\s*$", stripped):
blocks.append({"object": "block", "type": "divider", "divider": {}})
i += 1
continue
if stripped.startswith(">"):
blocks.append({
"object": "block", "type": "quote",
"quote": {"rich_text": rich_text(stripped[1:].strip())}
})
i += 1
continue
if stripped.startswith("# ") and not stripped.startswith("## "):
blocks.append({
"object": "block", "type": "heading_1",
"heading_1": {"rich_text": rich_text(stripped[2:])}
})
i += 1
continue
if stripped.startswith("## ") and not stripped.startswith("### "):
blocks.append({
"object": "block", "type": "heading_2",
"heading_2": {"rich_text": rich_text(stripped[3:])}
})
i += 1
continue
if stripped.startswith("### "):
blocks.append({
"object": "block", "type": "heading_3",
"heading_3": {"rich_text": rich_text(stripped[4:])}
})
i += 1
continue
if stripped.startswith("- "):
blocks.append({
"object": "block", "type": "bulleted_list_item",
"bulleted_list_item": {"rich_text": rich_text(stripped[2:])}
})
i += 1
continue
if re.match(r"^\d+[\.\)]\s", stripped):
text = re.sub(r"^\d+[\.\)]\s", "", stripped)
blocks.append({
"object": "block", "type": "numbered_list_item",
"numbered_list_item": {"rich_text": rich_text(text)}
})
i += 1
continue
if stripped:
blocks.append({
"object": "block", "type": "paragraph",
"paragraph": {"rich_text": rich_text(stripped)}
})
i += 1
flush_table()
return blocks
def upload_blocks(page_id, blocks):
batch_size = 100
total_uploaded = 0
total_blocks = len(blocks)
for start in range(0, total_blocks, batch_size):
batch = blocks[start:start + batch_size]
result = notion_request("PATCH", f"blocks/{page_id}/children", {"children": batch})
if result:
total_uploaded += len(batch)
print(f" 已上传 {total_uploaded}/{total_blocks} blocks", end="\r")
else:
print(f"\n ⚠️ batch {start//batch_size + 1} 上传失败")
return False
print(f"\n ✅ 全部 {total_uploaded} blocks 上传完成")
return True
def add_separator(parent_id, ticker, date, version):
"""在父页面底部插入分隔标记:批次标题 + 分割线。"""
label = f"▶ {ticker} — {date} V{version}"
blocks = [
{
"object": "block", "type": "callout",
"callout": {
"rich_text": [{"type": "text", "text": {"content": label}}],
"color": "gray_background",
"icon": {"type": "emoji", "emoji": "📌"}
}
},
{"object": "block", "type": "divider", "divider": {}}
]
result = notion_request("PATCH", f"blocks/{parent_id}/children", {"children": blocks})
if result:
print(f" ✅ 已插入分隔标记: {label}")
else:
print(f" ⚠️ 分隔标记插入失败(非致命)")
def create_child_page(parent_id, title, report_type_emoji, md_content):
"""创建子页面(默认追加到父页面底部),上传内容。"""
display_title = f"{report_type_emoji} {title}"
print(f"\n 创建子页面: {display_title}")
page_data = {
"parent": {"page_id": parent_id},
"properties": {
"title": {"title": [{"type": "text", "text": {"content": display_title}}]}
}
}
result = notion_request("POST", "pages", page_data)
if not result:
print(f" ❌ 页面创建失败: {display_title}")
return False
page_id = result.get("id", "")
if not page_id:
print(" ❌ 未获取到 page_id")
return False
print(f" ✅ 页面已创建: {page_id}")
blocks = md_to_notion_blocks(md_content)
print(f" 解析了 {len(blocks)} 个 block")
if not upload_blocks(page_id, blocks):
return False
# 末尾分割线
notion_request("PATCH", f"blocks/{page_id}/children", {
"children": [{"object": "block", "type": "divider", "divider": {}}]
})
return True
def main():
import argparse
parser = argparse.ArgumentParser(description="上传报告到 Notion(子页面形式)")
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--ticker", default=None)
mode.add_argument("--files", default=None)
args = parser.parse_args()
parent_url = "https://www.notion.so/Report-for-Hermes-3551dbfc72bb80a6849cdc1292697c14"
parent_id = extract_page_id(parent_url)
print(f"目标页面 ID: {parent_id}")
if not NOTION_API_KEY:
print("❌ NOTION_API_KEY 未设置")
sys.exit(1)
print("验证 API key...")
result = notion_request("GET", "users/me")
if result:
print(f" ✅ 已连接 Notion: {result.get('name', 'Unknown')}")
else:
print(" ❌ API key 无效,请检查")
sys.exit(1)
reports_dir = os.path.expanduser(
"~/.hermes/skills/data-science/out/out_reports"
)
all_files = sorted(os.listdir(reports_dir), reverse=True)
matched = []
if args.files:
file_list = [f.strip() for f in args.files.split(",")]
for f in file_list:
fpath = os.path.join(reports_dir, f) if not f.startswith("/") else f
fname = os.path.basename(fpath)
if os.path.exists(fpath):
matched.append(fname)
else:
alt = os.path.expanduser(f)
if os.path.exists(alt):
matched.append(os.path.basename(alt))
else:
print(f" ⚠️ 未找到: {f}")
if not matched:
print("❌ 所有指定的文件均未找到")
sys.exit(1)
elif args.ticker:
ticker = args.ticker.upper()
all_matched = [f for f in all_files
if f.startswith(("investment_memo_", "reasoning_trace_", "reasoning_review_",
"critical_review_", "buffett_review_"))
and "_V" in f and ticker in f]
if not all_matched:
print(f"❌ 未找到 {ticker} 的分析报告")
sys.exit(1)
matched = []
for prefix in ["investment_memo_", "reasoning_trace_", "reasoning_review_", "critical_review_", "buffett_review_"]:
candidates = [f for f in all_matched if f.startswith(prefix)]
if candidates:
matched.append(sorted(candidates, reverse=True)[0])
if not matched:
print(f"❌ 未找到 {ticker} 的最新分析报告")
sys.exit(1)
if not matched:
print("❌ 未找到匹配的报告文件")
sys.exit(1)
print(f"找到 {len(matched)} 份报告:", ", ".join(matched))
print()
type_config = {
"investment_memo": ("📊", "投资备忘录"),
"reasoning_trace": ("🔍", "推理轨迹"),
"reasoning_review": ("🔍", "思考链"),
"critical_review": ("⚠️", "批判性审视"),
"buffett_review": ("🦅", "巴菲特视角"),
}
# 上传前先插入批次分隔标记(仅 --ticker 模式)
if args.ticker and matched:
ticker = args.ticker.upper()
# 从第一个文件名提取日期和版本:investment_memo_DUOL_Duolingo_20260505_V2.md
m = re.search(r"_(\d{8})_V(\d+)", matched[0])
if m:
add_separator(parent_id, ticker, m.group(1), m.group(2))
success = 0
for fname in matched:
fpath = os.path.join(reports_dir, fname)
if not os.path.exists(fpath):
print(f"\n❌ 未找到: {fname}")
continue
with open(fpath, encoding="utf-8") as f:
content = f.read()
prefix = fname.split("_")[0]
emoji, type_label = type_config.get(prefix, ("📄", "报告"))
# 重新匹配正确的前缀(文件名如 reasoning_review_002327_...)
for key in ["investment_memo", "reasoning_trace", "reasoning_review", "critical_review", "buffett_review"]:
if fname.startswith(key):
emoji, type_label = type_config[key]
break
title = fname.replace(".md", "")
if create_child_page(parent_id, title, emoji, content):
success += 1
else:
print(f" ❌ {fname} 上传失败")
print(f"\n{'='*50}")
print(f"上传完成: {success}/{len(matched)} 份报告")
print(f"💡 注意: Notion API 限制,子页面默认按创建顺序排列(最新的在底部)")
print(f"查看地址: {parent_url}")
if __name__ == "__main__":
main()
scripts/polygon-data/polygon_data.py#!/usr/bin/env python3
"""
Polygon.io 金融数据采集器
美股行情/财报/K线/股息/拆分 — 作为 yfinance 的补充
用法:
python3 polygon_data.py --ticker AAPL --type quote
python3 polygon_data.py --ticker AAPL --type financials --limit 5
python3 polygon_data.py --ticker AAPL --type bars --from 2023-01-01 --to 2025-12-31
python3 polygon_data.py --ticker AAPL --type dividends
python3 polygon_data.py --ticker AAPL --type all
"""
import os, sys, json, argparse
from datetime import datetime, timedelta
from pathlib import Path
import urllib.request
import urllib.error
BASE_URL = "https://api.polygon.io"
CACHE_DIR = os.path.expanduser("~/.polygon_data/output")
def read_polygon_api_key():
"""按本机约定自动查找 Polygon API Key"""
value = os.environ.get("POLYGON_API_KEY")
if value:
return value
script_path = Path(__file__).resolve()
skill_root = script_path.parents[2]
config_paths = [
skill_root / ".env.local",
script_path.parent.parent / "config" / "api_keys.json",
Path.home() / ".hermes" / "skills" / "data-science" /
"financial-data-acquisition" / "config" / "api_keys.json",
Path.home() / ".hermes" / ".env",
]
for path in config_paths:
if not path.exists():
continue
try:
if path.suffix == ".json":
config = json.loads(path.read_text())
value = config.get("polygon_api_key") or config.get("POLYGON_API_KEY")
if value:
return value
else:
for raw_line in path.read_text(errors="ignore").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key.strip() == "POLYGON_API_KEY" and value.strip():
return value.strip().strip('"').strip("'")
except Exception:
continue
return ""
API_KEY = read_polygon_api_key()
def api_get(path, params=None):
"""调用 Polygon.io REST API"""
if not API_KEY:
print("❌ POLYGON_API_KEY 未设置")
print(" 请 export POLYGON_API_KEY=your_key_here,或配置 data-science/.env.local / scripts/config/api_keys.json")
sys.exit(1)
url = f"{BASE_URL}{path}"
if params:
params["apiKey"] = API_KEY
qs = "&".join(f"{k}={v}" for k, v in sorted(params.items()))
url = f"{url}?{qs}"
else:
url = f"{url}?apiKey={API_KEY}"
try:
req = urllib.request.Request(url, headers={"User-Agent": "Hermes-CFO/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
print(f"❌ HTTP {e.code}: {e.reason}")
if e.code == 401:
print(" API Key 无效或未设置")
elif e.code == 429:
print(" 频率限制: Free tier 5 calls/min")
return None
except Exception as e:
print(f"❌ 请求失败: {e}")
return None
def save_output(data, name):
"""保存结果到文件"""
os.makedirs(CACHE_DIR, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
fpath = os.path.join(CACHE_DIR, f"polygon_{name}_{ts}.json")
with open(fpath, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f" ✅ 已保存: {fpath}")
return fpath
def query_quote(ticker):
"""当日/上日行情"""
print(f"\n📊 行情: {ticker}")
data = api_get(f"/v2/aggs/ticker/{ticker}/prev")
if not data or "results" not in data:
print(" 无数据")
return
r = data["results"][0]
print(f" 昨收: ${r.get('c', 'N/A')}")
print(f" 最高: ${r.get('h', 'N/A')}")
print(f" 最低: ${r.get('l', 'N/A')}")
print(f" 开盘: ${r.get('o', 'N/A')}")
print(f" 成交量: {r.get('v', 'N/A')}")
print(f" 日期: {r.get('t', 'N/A')}")
save_output(data, f"{ticker}_quote")
def query_financials(ticker, limit=4):
"""财报数据(10-K/10-Q)"""
print(f"\n📋 财报: {ticker} (最近{limit}期)")
data = api_get(f"/v2/reference/financials/{ticker}", {
"limit": limit,
})
if not data or "results" not in data:
print(" 无数据")
return
results = data.get("results", [])
print(f" 共 {len(results)} 期财报")
for i, rpt in enumerate(results[:limit]):
period = rpt.get("period", "?")
dt = rpt.get("calendarDate", "?")
print(f"\n [{i+1}] {period} ({dt})")
print(f" 营收: {rpt.get('revenues', 'N/A')}")
print(f" 毛利: {rpt.get('grossProfit', 'N/A')}")
print(f" 净利润: {rpt.get('netIncome', 'N/A')}")
print(f" 经营现金流: {rpt.get('netCashFlowFromOperations', 'N/A')}")
print(f" 资本支出: {rpt.get('capitalExpenditure', 'N/A')}")
print(f" SBC(股权激励): {rpt.get('shareBasedCompensation', 'N/A')}")
print(f" 自由现金流: {rpt.get('freeCashFlow', 'N/A')}")
print(f" 总资产: {rpt.get('assets', 'N/A')}")
print(f" 总负债: {rpt.get('totalLiabilities', 'N/A')}")
print(f" 股东权益: {rpt.get('shareholdersEquity', 'N/A')}")
save_output(data, f"{ticker}_financials")
def query_bars(ticker, from_date=None, to_date=None):
"""历史日K线"""
if not to_date:
to_date = datetime.now().strftime("%Y-%m-%d")
if not from_date:
from_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d")
print(f"\n📈 K线: {ticker} ({from_date} → {to_date})")
data = api_get(f"/v2/aggs/ticker/{ticker}/range/1/day/{from_date}/{to_date}")
if not data or "results" not in data:
print(" 无数据")
return
results = data.get("results", [])
print(f" 共 {len(results)} 个交易日")
for r in results[-5:]:
dt = datetime.fromtimestamp(r["t"]/1000).strftime("%Y-%m-%d")
print(f" {dt} O:{r['o']} H:{r['h']} L:{r['l']} C:{r['c']} V:{r['v']}")
save_output(data, f"{ticker}_bars")
def query_dividends(ticker):
"""分红历史"""
print(f"\n💰 分红: {ticker}")
data = api_get(f"/v2/reference/dividends/{ticker}")
if not data or "results" not in data:
print(" 无数据")
return
results = data.get("results", [])
print(f" 共 {len(results)} 条记录")
for r in results[:5]:
print(f" {r.get('exDate', '?')}: ${r.get('amount', '?')} (付款日: {r.get('paymentDate', '?')})")
save_output(data, f"{ticker}_dividends")
def query_all(ticker):
"""全量查询"""
print(f"\n{'='*60}")
print(f"全量采集: {ticker}")
print(f"{'='*60}")
query_quote(ticker)
query_financials(ticker, 4)
query_bars(ticker)
query_dividends(ticker)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Polygon.io 金融数据采集器")
parser.add_argument("--ticker", required=True, help="股票代码 (如 AAPL)")
parser.add_argument("--type", default="quote",
choices=["quote", "financials", "bars", "dividends", "all"],
help="查询类型")
parser.add_argument("--limit", type=int, default=4, help="财报期数限制")
parser.add_argument("--from", dest="from_date", help="K线开始日期 (YYYY-MM-DD)")
parser.add_argument("--to", dest="to_date", help="K线结束日期 (YYYY-MM-DD)")
args = parser.parse_args()
ticker = args.ticker.upper()
handlers = {
"quote": query_quote,
"financials": lambda t: query_financials(t, args.limit),
"bars": lambda t: query_bars(t, args.from_date, args.to_date),
"dividends": query_dividends,
"all": query_all,
}
handlers[args.type](ticker)
SKILL.md--- name: data-science description: Financial data science workflows for market data acquisition, Polygon.io queries, MX API A-share research, reproducible Python/Jupyter exploration, valuation scripts, Buffett-style investment review, critical review, reasoning trace generation, and optional Notion upload. Use when Codex needs to fetch financial data, analyze stocks, build investment memos, run the bundled finance scripts, or inspect the imported data-science references from data-science.zip. --- # Data Science ## 快速入口 先确认密钥来自环境变量或本机私有配置,不要把真实 key 写入输出报告或可提交文件。优先使用环境变量;如果没有,脚本会自动查找本 skill 的私有配置和本机 Hermes/Mi 旧配置: ```bash set -a test -f data-science/.env.local && source data-science/.env.local set +a ``` 本 skill 已从 `data-science.zip` 整理为一个可发现的 Codex skill。长说明保留在 `references/original-skills/`,执行脚本保留在 `scripts/`。 ## 任务选择 - 拉取基础财务数据:读 `references/original-skills/financial-data-acquisition.md`,优先运行 `scripts/financial-data-acquisition/fetch_financials.py`。 - 查询 Polygon.io:读 `references/original-skills/polygon-data.md`,运行 `scripts/polygon-data/polygon_data.py`,必须设置 `POLYGON_API_KEY`。 - A 股自然语言查询或东方财富妙想:优先直接调用本仓库/全局的 `$mx-data`、`$mx-search`、`$mx-xuangu`、`$mx-zixuan`、`$mx-moni`;需要背景说明时再读 `references/financial-data-acquisition/mx-api-guide.md` 和 `references/financial-analysis/china-a-share-data-sources.md`,必须设置 `MX_APIKEY`。 - 生成投资备忘录或估值分析:读 `references/original-skills/financial-analysis.md`,按里面的脚本顺序运行 `scripts/financial-analysis/`。 - 巴菲特视角、批判性审查、推理追踪:分别读 `references/original-skills/buffett-review.md`、`critical-review.md`、`reasoning-review.md`。 - Jupyter 交互式探索:读 `references/original-skills/jupyter-live-kernel.md`,仅在需要持久 Python 状态时使用。 - Notion 上传:只在用户明确要求上传时读 `references/original-skills/notion-upload.md`,并要求 `NOTION_API_KEY`。 ## 密钥与配置 `data-science/.env.local` 可保存本机私有密钥,已被 `.gitignore` 忽略。可提交的模板是 `data-science/.env.example`。 API 查找顺序: 1. 当前进程环境变量:`FINNHUB_API_KEY`、`FRED_API_KEY`、`POLYGON_API_KEY`、`MX_APIKEY`、`TUSHARE_TOKEN` 2. `data-science/scripts/config/api_keys.json` 3. `data-science/.env.local` 4. 本机 Hermes/Mi 旧配置:`~/.hermes/skills/data-science/financial-data-acquisition/config/api_keys.json` 5. `~/.hermes/.env` 部分旧脚本会从 `scripts/config/api_keys.json` 读取 Finnhub、FRED、Tushare、Polygon、MX 等 key。不要提交真实文件;需要时复制模板: ```bash cp data-science/scripts/config/api_keys.example.json data-science/scripts/config/api_keys.json ``` ## 常用命令 Polygon 全量查询: ```bash set -a; test -f data-science/.env.local && source data-science/.env.local; set +a python3 data-science/scripts/polygon-data/polygon_data.py --ticker AAPL --type all ``` yfinance/SEC/Finnhub/FRED 财务数据采集: ```bash python3 data-science/scripts/financial-data-acquisition/fetch_financials.py --ticker AAPL --format markdown ``` 深度分析脚本一般以采集出的 JSON 作为输入。执行前先读 `references/original-skills/financial-analysis.md`,再按目标选择 `scripts/financial-analysis/` 下的脚本。 ## 纪律 - 所有金额、比率、估值计算必须用 Python、脚本或可复现命令完成,不要在正文里心算。 - 每个外部数据点都要记录来源、抓取时间、币种和单位。 - 当现金/市值、ROIC、WACC、汇率或币种出现异常值时,先做数据一致性检查,再写结论。 - Notion 上传、外部 API 调用和联网采集前,先确认用户确实需要当前数据。