@pencil-agent/nano-pencil 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +267 -267
  2. package/dist/build-meta.json +3 -3
  3. package/dist/core/export-html/AGENT.md +11 -11
  4. package/dist/core/export-html/template.css +971 -971
  5. package/dist/core/export-html/template.html +54 -54
  6. package/dist/core/mcp/mcp-client.d.ts +3 -1
  7. package/dist/core/mcp/mcp-client.js +6 -6
  8. package/dist/core/mcp/mcp-config.d.ts +3 -3
  9. package/dist/core/mcp/mcp-config.js +1 -1
  10. package/dist/core/mcp/mcp-manager.d.ts +5 -1
  11. package/dist/core/mcp/mcp-manager.js +1 -1
  12. package/dist/core/platform/config/resource-loader.d.ts +2 -0
  13. package/dist/core/platform/config/resource-loader.js +2 -2
  14. package/dist/core/runtime/agent-session.d.ts +12 -0
  15. package/dist/core/runtime/agent-session.js +8 -8
  16. package/dist/core/runtime/sdk.d.ts +8 -0
  17. package/dist/core/runtime/sdk.js +1 -1
  18. package/dist/extensions/builtin/AGENT.md +115 -115
  19. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  20. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  91. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  92. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  93. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  94. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  95. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  96. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  97. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  98. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  99. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  100. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  101. package/dist/extensions/builtin/browser/browser.md +73 -73
  102. package/dist/extensions/builtin/browser/install.md +142 -142
  103. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  104. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  105. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  107. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  108. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  109. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  110. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  111. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  112. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  113. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  114. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  115. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  116. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  117. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  118. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  119. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  120. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  121. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  122. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  123. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  124. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  125. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  126. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  127. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  128. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  129. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  130. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  131. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  132. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  133. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  134. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  135. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  136. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  137. package/dist/extensions/builtin/goal/README.md +67 -67
  138. package/dist/extensions/builtin/grub/README.md +112 -112
  139. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  140. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  141. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  142. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  143. package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
  144. package/dist/extensions/builtin/loop/README.md +92 -92
  145. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  146. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  147. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  148. package/dist/extensions/builtin/sal/README.md +72 -72
  149. package/dist/extensions/builtin/security-audit/README.md +289 -289
  150. package/dist/extensions/builtin/team/AGENT.md +112 -112
  151. package/dist/extensions/builtin/team/TESTING.md +299 -299
  152. package/dist/extensions/builtin/token-save/README.md +56 -56
  153. package/dist/extensions/optional/AGENT.md +10 -10
  154. package/dist/modes/interactive/interactive-mode.js +36 -36
  155. package/dist/modes/interactive/theme/dark.json +85 -85
  156. package/dist/modes/interactive/theme/light.json +84 -84
  157. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  158. package/dist/modes/interactive/theme/warm.json +81 -81
  159. package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.js +3 -2
  160. package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.js +2 -1
  161. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  162. package/docs/cc-agent-design.md +1297 -0
  163. package/docs/cc-tui-design.md +1333 -0
  164. package/docs/codex-goal-command-impl.md +1055 -1055
  165. package/docs/codex-goal-vs-grub.md +500 -500
  166. package/docs/custom-provider.md +27 -27
  167. package/docs/extensions.md +27 -27
  168. package/docs/keybindings.md +27 -27
  169. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  170. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  171. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  172. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
  173. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
  174. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  175. package/docs/loop-usage-examples.md +214 -214
  176. package/docs/models.md +27 -27
  177. package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +170 -0
  178. package/docs/packages.md +27 -27
  179. package/docs/pi-design-philosophy.md +457 -457
  180. package/docs/planmode.md +1987 -1987
  181. package/docs/prompt-templates.md +27 -27
  182. package/docs/providers.md +27 -27
  183. package/docs/scan-report.md +3820 -0
  184. package/docs/sdk.md +27 -27
  185. package/docs/skills.md +27 -27
  186. package/docs/themes.md +27 -27
  187. package/docs/tui.md +27 -27
  188. package/docs//345/257/271/346/240/207Claude-Code.md +1775 -0
  189. package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +261 -0
  190. package/package.json +190 -190
  191. package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +0 -851
  192. package/docs/SDK-TESTING.md +0 -364
  193. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +0 -593
  194. package/docs/startup-performance-optimization.md +0 -301
  195. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +0 -47
@@ -1,537 +1,537 @@
1
- # Macrotrends — Data Extraction
2
-
3
- `https://www.macrotrends.net` — long-term historical financial and economic charts. Three access patterns depending on page type; all work with plain `http_get`, no browser required.
4
-
5
- All results validated against live site on 2026-04-18.
6
-
7
- ## Do this first: pick your access pattern
8
-
9
- | Goal | Pattern | Latency | Variable |
10
- |------|---------|---------|----------|
11
- | Stock OHLCV price history | Direct iframe PHP | ~190ms | `dataDaily` |
12
- | Stock market cap (daily) | Direct iframe PHP | ~200ms | `chartData` |
13
- | Stock fundamentals (PE, revenue, margins) | Direct iframe PHP | ~140ms | `chartData` |
14
- | S&P 500 / composite index charts | `chart_iframe_comp.php` | ~90ms | `originalData` |
15
- | Economic indicators (rates, yields, CPI) | `/economic-data/` JSON API | ~150ms | `data[]` array |
16
- | Gold, commodity prices | Either path (both work) | ~150ms | `data[]` or `originalData` |
17
-
18
- **Never use the browser for Macrotrends read-only tasks.** All endpoints are accessible via `http_get` with the default `Mozilla/5.0` UA. For pages that occasionally 403, switch to a Chrome UA (see gotchas).
19
-
20
- ---
21
-
22
- ## Pattern 1: Stock price history (OHLCV)
23
-
24
- Construct the iframe URL directly — no need to fetch the main page first.
25
-
26
- ```python
27
- import json, re
28
- from helpers import http_get
29
-
30
- def get_stock_ohlcv(ticker: str, years_back: int = None) -> list[dict]:
31
- """
32
- Returns daily OHLCV records for any US stock.
33
-
34
- ticker: uppercase ticker symbol, e.g. 'AAPL', 'MSFT', 'TSLA', 'NVDA'
35
- years_back: number of years of history (1=~250 records, 15=~3772 records).
36
- Omit (None) to get ALL available history (AAPL goes back to 1980).
37
- """
38
- url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/stock_price_history.php?t={ticker}"
39
- if years_back:
40
- url += f"&yb={years_back}"
41
-
42
- html = http_get(url)
43
- m = re.search(r'var\s+dataDaily\s*=\s*\[', html)
44
- if not m:
45
- raise ValueError(f"No dataDaily found for ticker {ticker!r}")
46
-
47
- si = html.index('[', m.start())
48
- bc = 0
49
- for j, ch in enumerate(html[si:], si):
50
- if ch == '[': bc += 1
51
- elif ch == ']':
52
- bc -= 1
53
- if bc == 0: ei = j; break
54
- return json.loads(html[si:ei+1])
55
-
56
- # Usage
57
- records = get_stock_ohlcv('AAPL', years_back=15)
58
- # [{'d': '2011-04-18', 'o': '9.771', 'h': '9.9547', 'l': '9.593', 'c': '9.9433', 'v': '18.275'}, ...]
59
-
60
- latest = records[-1]
61
- # {'d': '2026-04-17', 'o': '266.96', 'h': '272.3', 'l': '266.72', 'c': '270.23',
62
- # 'v': '55.211', 'ma50': '260.554', 'ma200': '251.828'}
63
-
64
- print(f"{latest['d']}: close=${latest['c']} vol={latest['v']}M shares")
65
- ```
66
-
67
- ### dataDaily field reference
68
-
69
- | Field | Meaning | Type |
70
- |-------|---------|------|
71
- | `d` | Date (YYYY-MM-DD) | str |
72
- | `o` | Open price (adjusted for splits) | str/float |
73
- | `h` | High | str/float |
74
- | `l` | Low | str/float |
75
- | `c` | Close | str/float |
76
- | `v` | Volume in **millions of shares** | str/float |
77
- | `ma50` | 50-day moving average | str/float (appears on recent records only) |
78
- | `ma200` | 200-day moving average | str/float (appears on recent records only) |
79
-
80
- **Note:** All price values are strings — cast with `float()`. Volume is millions: `55.211` = 55.2M shares traded.
81
-
82
- ### Confirmed tickers (2026-04-18)
83
-
84
- All tested with direct iframe URL, no page fetch needed:
85
-
86
- ```python
87
- # All work: AAPL, MSFT, TSLA, NVDA, GOOGL, AMZN, META, NFLX, etc.
88
- # 3772 records for yb=15 (goes back to 2011-04-18)
89
- # AAPL full history: 11428 records back to 1980-12-12
90
- ```
91
-
92
- ---
93
-
94
- ## Pattern 2: Stock fundamentals (PE ratio, revenue, market cap, margins)
95
-
96
- Different PHP files depending on metric. Construct directly.
97
-
98
- ### Market cap (daily, in billions USD)
99
-
100
- ```python
101
- import json, re
102
- from helpers import http_get
103
-
104
- def get_market_cap(ticker: str, years_back: int = 15) -> list[dict]:
105
- url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/market_cap.php?t={ticker}&yb={years_back}"
106
- html = http_get(url)
107
- m = re.search(r'var\s+chartData\s*=\s*\[', html)
108
- si = html.index('[', m.start())
109
- bc = 0
110
- for j, ch in enumerate(html[si:], si):
111
- if ch == '[': bc += 1
112
- elif ch == ']':
113
- bc -= 1
114
- if bc == 0: ei = j; break
115
- return json.loads(html[si:ei+1])
116
-
117
- data = get_market_cap('AAPL')
118
- # [{'date': '2026-04-15', 'v1': 3929.35}, {'date': '2026-04-16', 'v1': 3884.67}, ...]
119
- # v1 = market cap in billions USD
120
- ```
121
-
122
- ### PE ratio, revenue, current ratio (quarterly/annual fundamentals)
123
-
124
- ```python
125
- import json, re
126
- from helpers import http_get
127
-
128
- def get_fundamental(ticker: str, metric_type: str, statement: str,
129
- freq: str = 'Q', years_back: int = 15) -> list[dict]:
130
- """
131
- freq: 'Q' = quarterly, 'A' = annual
132
- """
133
- url = (
134
- f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
135
- f"fundamental_iframe.php?t={ticker}&type={metric_type}&statement={statement}"
136
- f"&freq={freq}&sub=&yb={years_back}"
137
- )
138
- html = http_get(url)
139
- m = re.search(r'var\s+chartData\s*=\s*\[', html)
140
- si = html.index('[', m.start())
141
- bc = 0
142
- for j, ch in enumerate(html[si:], si):
143
- if ch == '[': bc += 1
144
- elif ch == ']':
145
- bc -= 1
146
- if bc == 0: ei = j; break
147
- return json.loads(html[si:ei+1])
148
-
149
- # PE ratio
150
- pe = get_fundamental('AAPL', 'pe-ratio', 'price-ratios')
151
- # [{'date': '2025-09-30', 'v1': 254.146, 'v2': 7.46, 'v3': 34.07}, ...]
152
- # v1 = stock price, v2 = quarterly EPS, v3 = PE ratio
153
-
154
- # Revenue
155
- rev = get_fundamental('AAPL', 'revenue', 'income-statement')
156
- # [{'date': '2025-12-31', 'v1': 435.617, 'v2': 143.756, 'v3': 15.65}, ...]
157
- # v1 = TTM revenue ($B), v2 = quarterly revenue ($B), v3 = YoY growth %
158
-
159
- # Total assets
160
- assets = get_fundamental('AAPL', 'total-assets', 'balance-sheet')
161
-
162
- # Current ratio
163
- ratio = get_fundamental('AAPL', 'current-ratio', 'ratios')
164
- ```
165
-
166
- ### Profit margins
167
-
168
- ```python
169
- def get_profit_margins(ticker: str, years_back: int = 15) -> list[dict]:
170
- url = (
171
- f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
172
- f"fundamental_metric.php?t={ticker}&chart=profit-margin&sub=&yb={years_back}"
173
- )
174
- html = http_get(url)
175
- m = re.search(r'var\s+chartData\s*=\s*\[', html)
176
- si = html.index('[', m.start())
177
- bc = 0
178
- for j, ch in enumerate(html[si:], si):
179
- if ch == '[': bc += 1
180
- elif ch == ']':
181
- bc -= 1
182
- if bc == 0: ei = j; break
183
- return json.loads(html[si:ei+1])
184
-
185
- margins = get_profit_margins('AAPL')
186
- # [{'date': '2025-12-31', 'v1': 47.33, 'v2': 32.38, 'v3': 27.04}, ...]
187
- # v1 = gross margin %, v2 = operating margin %, v3 = net margin %
188
- ```
189
-
190
- ### Dividend yield
191
-
192
- ```python
193
- def get_dividend_yield(ticker: str, years_back: int = 15) -> list[dict]:
194
- url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/dividend_yield.php?t={ticker}&yb={years_back}"
195
- html = http_get(url)
196
- m = re.search(r'var\s+chartData\s*=\s*\[', html)
197
- si = html.index('[', m.start())
198
- bc = 0
199
- for j, ch in enumerate(html[si:], si):
200
- if ch == '[': bc += 1
201
- elif ch == ']':
202
- bc -= 1
203
- if bc == 0: ei = j; break
204
- return json.loads(html[si:ei+1])
205
-
206
- dy = get_dividend_yield('AAPL')
207
- # [{'date': '2026-04-17', 'c': 270.23, 'ttm_d': 1.03848, 'ttm_dy': 0.3843}, ...]
208
- # c = stock price, ttm_d = TTM dividend ($), ttm_dy = TTM yield (%)
209
- ```
210
-
211
- ### Stock metric URL reference
212
-
213
- | Metric | PHP file | Extra params |
214
- |--------|----------|-------------|
215
- | Stock price OHLCV | `stock_price_history.php` | — |
216
- | Market cap (daily) | `market_cap.php` | — |
217
- | Dividend yield | `dividend_yield.php` | — |
218
- | Stock splits (price history) | `stock_splits.php` | — |
219
- | PE ratio | `fundamental_iframe.php` | `type=pe-ratio&statement=price-ratios` |
220
- | Revenue | `fundamental_iframe.php` | `type=revenue&statement=income-statement` |
221
- | Total assets | `fundamental_iframe.php` | `type=total-assets&statement=balance-sheet` |
222
- | Current ratio | `fundamental_iframe.php` | `type=current-ratio&statement=ratios` |
223
- | Profit margins | `fundamental_metric.php` | `chart=profit-margin` |
224
-
225
- Base URL prefix: `https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/`
226
-
227
- All take `?t={TICKER}&yb={N}` (or `&sub=&yb={N}` for the fundamental ones).
228
-
229
- ---
230
-
231
- ## Pattern 3: Index and composite charts (S&P 500, Shiller PE, etc.)
232
-
233
- These pages embed chart data via `chart_iframe_comp.php`. The variable is `originalData`.
234
-
235
- ```python
236
- import json, re
237
- from helpers import http_get
238
-
239
- def extract_index_chart(page_id: int, url_slug: str) -> list[dict]:
240
- """
241
- page_id: the numeric ID from the page URL, e.g. 2577
242
- url_slug: last segment of the page URL, e.g. 'sp500-pe-ratio-price-to-earnings-chart'
243
- """
244
- url = f"https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id={page_id}&url={url_slug}"
245
- html = http_get(url)
246
- m = re.search(r'var\s+originalData\s*=\s*\[', html)
247
- if not m:
248
- raise ValueError("originalData not found — this page may use a different pattern")
249
- si = html.index('[', m.start())
250
- bc = 0
251
- for j, ch in enumerate(html[si:], si):
252
- if ch == '[': bc += 1
253
- elif ch == ']':
254
- bc -= 1
255
- if bc == 0: ei = j; break
256
- return json.loads(html[si:ei+1])
257
-
258
- # S&P 500 PE ratio (1180 monthly records, 1927-2026)
259
- pe_data = extract_index_chart(2577, 'sp500-pe-ratio-price-to-earnings-chart')
260
- # [{'date': '1927-12-01', 'close': '15.9099'}, ..., {'date': '2026-03-01', 'close': '27.8925'}]
261
- # 'close' is the PE ratio value
262
-
263
- # Gold prices (1336 monthly records, 1915-2026)
264
- gold_data = extract_index_chart(1333, 'historical-gold-prices-100-year-chart')
265
- # [{'id': 'GOLDAMGBD228NLBM', 'date': '1915-01-01', 'close': '629.36', 'close1': '19.250'}, ...]
266
- # 'close' = inflation-adjusted price, 'close1' = nominal USD price
267
-
268
- print(f"Latest S&P PE: {pe_data[-1]}") # {'date': '2026-03-01', 'close': '27.8925'}
269
- print(f"Latest gold: {gold_data[-1]}") # {'id': ..., 'date': '2026-04-01', 'close': '5177.19', 'close1': '5177.190'}
270
- ```
271
-
272
- ### Detecting which pattern a page uses
273
-
274
- ```python
275
- def get_page_pattern(page_url: str) -> str:
276
- html = http_get(page_url)
277
- if 'chart_iframe_comp.php' in html:
278
- return 'index_chart' # use extract_index_chart()
279
- elif 'generateChart' in html and 'highchartsURL' in html:
280
- return 'economic_api' # use get_economic_data()
281
- elif '/production/stocks/desktop/PRODUCTION/' in html:
282
- return 'stock_iframe' # use get_stock_ohlcv() etc.
283
- return 'unknown'
284
- ```
285
-
286
- ### To get the ID and slug from a page
287
-
288
- ```python
289
- import re
290
- from helpers import http_get
291
-
292
- page_url = "https://www.macrotrends.net/2577/sp500-pe-ratio-price-to-earnings-chart"
293
- html = http_get(page_url)
294
-
295
- # Option A: parse from the iframe src in the HTML
296
- m = re.search(r'chart_iframe_comp\.php\?id=(\d+)&url=([^"&]+)', html)
297
- if m:
298
- page_id, url_slug = int(m.group(1)), m.group(2)
299
-
300
- # Option B: derive from the page URL (works when slug matches)
301
- import urllib.parse
302
- parts = page_url.rstrip('/').split('/')
303
- page_id = int(parts[-2]) # 2577
304
- url_slug = parts[-1] # 'sp500-pe-ratio-price-to-earnings-chart'
305
- ```
306
-
307
- ---
308
-
309
- ## Pattern 4: Economic indicator API
310
-
311
- Pages that use `generateChart()` in their JS load data from `/economic-data/{pageID}/{freq}`.
312
- This endpoint requires a `Referer` header matching the page URL.
313
-
314
- ```python
315
- import json, datetime, gzip, urllib.request
316
- from helpers import http_get
317
-
318
- def get_economic_data(page_id: int, referer_url: str, freq: str = 'D') -> dict:
319
- """
320
- page_id: numeric ID from the page URL (e.g. 2015 for Fed Funds Rate)
321
- referer_url: the full page URL — required as Referer header
322
- freq: 'D' = daily, 'M' = monthly (not all support both)
323
-
324
- Returns {'data': [[ts_ms, value], ...], 'metadata': {...}}
325
- """
326
- url = f"https://www.macrotrends.net/economic-data/{page_id}/{freq}"
327
- headers = {
328
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
329
- "Accept": "application/json, */*",
330
- "Accept-Encoding": "gzip",
331
- "Referer": referer_url,
332
- }
333
- with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=20) as r:
334
- raw = r.read()
335
- if r.headers.get("Content-Encoding") == "gzip":
336
- raw = gzip.decompress(raw)
337
- result = json.loads(raw)
338
- if result is None:
339
- raise ValueError(f"pageID={page_id} does not support freq={freq!r}")
340
- return result
341
-
342
- # Fed Funds Rate (daily, 25319 records)
343
- ffr = get_economic_data(2015, "https://www.macrotrends.net/2015/fed-funds-rate-historical-chart", freq='D')
344
- print(ffr['metadata']['name']) # 'Fed Funds Interest Rate'
345
- print(ffr['metadata']['label']) # '%'
346
-
347
- # Convert timestamps to dates
348
- for ts_ms, value in ffr['data'][-3:]:
349
- dt = datetime.datetime.fromtimestamp(ts_ms / 1000, datetime.UTC)
350
- print(f"{dt.strftime('%Y-%m-%d')}: {value}%")
351
- # 2026-04-13: 3.64%
352
- # 2026-04-14: 3.64%
353
- # 2026-04-15: 3.64%
354
-
355
- # 10-Year Treasury yield (daily, 16074 records)
356
- t10 = get_economic_data(2016, "https://www.macrotrends.net/2016/10-year-treasury-bond-rate-yield-chart", freq='D')
357
- # Last: 2026-04-15: 4.29%
358
-
359
- # Gold prices (monthly, 1336 records, 1915-present) — template=5
360
- gold = get_economic_data(1333, "https://www.macrotrends.net/1333/historical-gold-prices-100-year-chart", freq='M')
361
- # metadata: {'name': 'Gold Prices', 'currency': '$', 'label': ''}
362
-
363
- # US Unemployment Rate (monthly, 938 records)
364
- unemp = get_economic_data(1316, "https://www.macrotrends.net/1316/us-national-unemployment-rate", freq='M')
365
- # metadata: {'name': 'U.S. Unemployment Rate', 'label': '%'}
366
-
367
- # Debt-to-GDP ratio (monthly, 712 records)
368
- debt_gdp = get_economic_data(1381, "https://www.macrotrends.net/1381/debt-to-gdp-ratio-historical-chart", freq='M')
369
- ```
370
-
371
- ### metadata fields
372
-
373
- ```python
374
- {
375
- 'name': 'Fed Funds Interest Rate', # chart title
376
- 'tableHeaderName': 'Fed Funds Interest Rate',
377
- 'currency': '', # '$' for dollar-denominated series
378
- 'label': '%', # units label
379
- 'chartType': 'line',
380
- 'mobileChartType': 'line',
381
- 'lineWidth': 2,
382
- 'positiveColor': '#2caffe',
383
- 'negativeColor': '',
384
- 'decimals': '',
385
- 'chartScale': 'linear',
386
- 'seriesUnits': ''
387
- }
388
- ```
389
-
390
- ### Available frequency codes
391
-
392
- | Code | Meaning | Notes |
393
- |------|---------|-------|
394
- | `D` | Daily | Most series support this |
395
- | `M` | Monthly | Returns `null` if not available |
396
- | `Q` | Quarterly | Usually `null` — use `M` instead |
397
- | `A` | Annual | Usually `null` — use `M` instead |
398
- | `DEFAULT` | Default (usually monthly) | Same data as `M` for most series |
399
- | `INDEXMONTHLY` | Monthly index close | Some commodity/index series |
400
- | `INDEXDAILY` | Daily index | Some series |
401
- | `DAILYEXCHANGERATE` | Daily FX rate | Currency pairs |
402
- | `10YD` | 10-year daily | Specialized series |
403
-
404
- Try `D` first, fall back to `M` if you get `null`.
405
-
406
- ### Known economic page IDs
407
-
408
- | ID | URL slug | Description |
409
- |----|----------|-------------|
410
- | 1316 | us-national-unemployment-rate | U.S. Unemployment Rate (monthly, back to 1948) |
411
- | 1333 | historical-gold-prices-100-year-chart | Gold Prices (monthly, back to 1915) |
412
- | 1381 | debt-to-gdp-ratio-historical-chart | U.S. Debt to GDP Ratio |
413
- | 2015 | fed-funds-rate-historical-chart | Fed Funds Interest Rate (daily, back to 1954) |
414
- | 2016 | 10-year-treasury-bond-rate-yield-chart | 10-Year Treasury Yield (daily, back to 1962) |
415
- | 2577 | sp500-pe-ratio-price-to-earnings-chart | S&P 500 PE Ratio (uses `chart_iframe_comp.php`) |
416
-
417
- ---
418
-
419
- ## Generic extraction helper
420
-
421
- One function that handles all three embedded-JS patterns:
422
-
423
- ```python
424
- import json, re
425
- from helpers import http_get
426
-
427
- def extract_chart_var(html: str, var_name: str) -> list:
428
- """Extract a JS array variable from Macrotrends iframe HTML."""
429
- m = re.search(rf'var\s+{re.escape(var_name)}\s*=\s*\[', html)
430
- if not m:
431
- return []
432
- si = html.index('[', m.start())
433
- bc = 0
434
- for j, ch in enumerate(html[si:], si):
435
- if ch == '[': bc += 1
436
- elif ch == ']':
437
- bc -= 1
438
- if bc == 0:
439
- return json.loads(html[si:j+1])
440
- return []
441
-
442
- # Works for dataDaily, chartData, or originalData:
443
- html = http_get("https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/stock_price_history.php?t=AAPL&yb=15")
444
- daily = extract_chart_var(html, 'dataDaily')
445
-
446
- html2 = http_get("https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id=2577&url=sp500-pe-ratio-price-to-earnings-chart")
447
- pe_data = extract_chart_var(html2, 'originalData')
448
- ```
449
-
450
- ---
451
-
452
- ## URL construction guide
453
-
454
- ### Stock pages
455
-
456
- ```python
457
- STOCK_BASE = "https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
458
-
459
- # Price history OHLCV
460
- f"{STOCK_BASE}stock_price_history.php?t={ticker}" # all history
461
- f"{STOCK_BASE}stock_price_history.php?t={ticker}&yb={years}" # last N years
462
-
463
- # Market cap
464
- f"{STOCK_BASE}market_cap.php?t={ticker}&yb={years}"
465
-
466
- # Fundamentals
467
- f"{STOCK_BASE}fundamental_iframe.php?t={ticker}&type={type}&statement={stmt}&freq={freq}&sub=&yb={years}"
468
- # type/statement combos: pe-ratio/price-ratios, revenue/income-statement,
469
- # total-assets/balance-sheet, current-ratio/ratios
470
-
471
- # Metrics
472
- f"{STOCK_BASE}fundamental_metric.php?t={ticker}&chart={metric}&sub=&yb={years}"
473
- # metrics: profit-margin
474
-
475
- # Dividend yield
476
- f"{STOCK_BASE}dividend_yield.php?t={ticker}&yb={years}"
477
- ```
478
-
479
- ### Economic / index pages
480
-
481
- ```python
482
- # From numeric ID + URL slug (read from page source or page URL)
483
- f"https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id={id}&url={slug}"
484
-
485
- # Economic indicator JSON API (requires Referer header)
486
- f"https://www.macrotrends.net/economic-data/{page_id}/{freq}"
487
- ```
488
-
489
- ---
490
-
491
- ## Rate limits and anti-bot
492
-
493
- - **No rate limiting observed** at any tested volume. 10 rapid requests to the same stock iframe completed in 1.8s with no throttling, CAPTCHA, or 429 errors.
494
- - **Default UA works** (`Mozilla/5.0`) for most endpoints. The iframe PHP files never 403'd.
495
- - **Chrome UA needed** for some main HTML pages (not data endpoints): use when fetching `/stocks/charts/...` or `/2015/...` wrapper pages if you get 403. Switch to:
496
- ```python
497
- headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}
498
- ```
499
- - **Referer required** for `/economic-data/{id}/{freq}` — send the page URL as `Referer`. Without it, the request is allowed but you get a 403 on some pages.
500
- - **No cookies, sessions, or auth tokens** needed for any endpoint.
501
-
502
- ---
503
-
504
- ## Gotchas
505
-
506
- **Main page URL ≠ data page:** Some URLs redirect to different content. `/1316/us-national-debt-by-year` redirects to `/1316/us-national-unemployment-rate`. Always check the final URL with `r.url` if the returned data looks wrong. Use the final URL as the Referer.
507
-
508
- **yb parameter controls history depth:**
509
- - `yb=1` → ~250 records (last year)
510
- - `yb=15` → ~3772 records (last 15 years)
511
- - omit → full history (AAPL: 11428 records to 1980; default for most queries)
512
-
513
- **Two iframe patterns for economic pages:** Pages at `macrotrends.net/NNNN/slug` use either `chart_iframe_comp.php` (→ `originalData`) or `generateChart` + `/economic-data/` API. Check the main page HTML to detect which:
514
- ```python
515
- if 'chart_iframe_comp.php' in html: # use extract_index_chart()
516
- elif 'highchartsURL' in html: # use get_economic_data()
517
- ```
518
-
519
- **Gold data has two price columns:**
520
- ```python
521
- {'id': 'GOLDAMGBD228NLBM', 'date': '2026-04-01', 'close': '5177.19', 'close1': '5177.190'}
522
- # 'close' = inflation-adjusted price (base year adjusts over time)
523
- # 'close1' = nominal USD price (the raw market price)
524
- ```
525
-
526
- **Economic API frequency codes:** Only `D` and `M` consistently return data across most series. `A` and `Q` return `null` for most economic indicators. Always try `D` first.
527
-
528
- **chartData fields vary by metric:**
529
- - `market_cap.php` → `{'date', 'v1'}` (v1 = market cap in $B)
530
- - `fundamental_iframe.php` type=pe-ratio → `{'date', 'v1', 'v2', 'v3'}` (stock price, EPS, PE)
531
- - `fundamental_iframe.php` type=revenue → `{'date', 'v1', 'v2', 'v3'}` (TTM revenue, quarterly revenue, YoY%)
532
- - `fundamental_metric.php` chart=profit-margin → `{'date', 'v1', 'v2', 'v3'}` (gross%, operating%, net%)
533
- - `dividend_yield.php` → `{'date', 'c', 'ttm_d', 'ttm_dy'}` (price, dividend, yield%)
534
-
535
- **Bracket matching required for large arrays:** The `var dataDaily = [...]` in stock iframes is ~450KB with 3772 OHLCV records. The `re.DOTALL` greedy approach works but is slow; bracket-counting (`bc` pattern above) is O(n) and fast.
536
-
537
- **No public API for ticker lookup:** To find the company slug for a URL, check the search endpoint: `https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/ticker_search_list.php?v=YYYYMMDD` — but the stock price iframe only needs the ticker symbol (`?t=AAPL`), not the slug.
1
+ # Macrotrends — Data Extraction
2
+
3
+ `https://www.macrotrends.net` — long-term historical financial and economic charts. Three access patterns depending on page type; all work with plain `http_get`, no browser required.
4
+
5
+ All results validated against live site on 2026-04-18.
6
+
7
+ ## Do this first: pick your access pattern
8
+
9
+ | Goal | Pattern | Latency | Variable |
10
+ |------|---------|---------|----------|
11
+ | Stock OHLCV price history | Direct iframe PHP | ~190ms | `dataDaily` |
12
+ | Stock market cap (daily) | Direct iframe PHP | ~200ms | `chartData` |
13
+ | Stock fundamentals (PE, revenue, margins) | Direct iframe PHP | ~140ms | `chartData` |
14
+ | S&P 500 / composite index charts | `chart_iframe_comp.php` | ~90ms | `originalData` |
15
+ | Economic indicators (rates, yields, CPI) | `/economic-data/` JSON API | ~150ms | `data[]` array |
16
+ | Gold, commodity prices | Either path (both work) | ~150ms | `data[]` or `originalData` |
17
+
18
+ **Never use the browser for Macrotrends read-only tasks.** All endpoints are accessible via `http_get` with the default `Mozilla/5.0` UA. For pages that occasionally 403, switch to a Chrome UA (see gotchas).
19
+
20
+ ---
21
+
22
+ ## Pattern 1: Stock price history (OHLCV)
23
+
24
+ Construct the iframe URL directly — no need to fetch the main page first.
25
+
26
+ ```python
27
+ import json, re
28
+ from helpers import http_get
29
+
30
+ def get_stock_ohlcv(ticker: str, years_back: int = None) -> list[dict]:
31
+ """
32
+ Returns daily OHLCV records for any US stock.
33
+
34
+ ticker: uppercase ticker symbol, e.g. 'AAPL', 'MSFT', 'TSLA', 'NVDA'
35
+ years_back: number of years of history (1=~250 records, 15=~3772 records).
36
+ Omit (None) to get ALL available history (AAPL goes back to 1980).
37
+ """
38
+ url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/stock_price_history.php?t={ticker}"
39
+ if years_back:
40
+ url += f"&yb={years_back}"
41
+
42
+ html = http_get(url)
43
+ m = re.search(r'var\s+dataDaily\s*=\s*\[', html)
44
+ if not m:
45
+ raise ValueError(f"No dataDaily found for ticker {ticker!r}")
46
+
47
+ si = html.index('[', m.start())
48
+ bc = 0
49
+ for j, ch in enumerate(html[si:], si):
50
+ if ch == '[': bc += 1
51
+ elif ch == ']':
52
+ bc -= 1
53
+ if bc == 0: ei = j; break
54
+ return json.loads(html[si:ei+1])
55
+
56
+ # Usage
57
+ records = get_stock_ohlcv('AAPL', years_back=15)
58
+ # [{'d': '2011-04-18', 'o': '9.771', 'h': '9.9547', 'l': '9.593', 'c': '9.9433', 'v': '18.275'}, ...]
59
+
60
+ latest = records[-1]
61
+ # {'d': '2026-04-17', 'o': '266.96', 'h': '272.3', 'l': '266.72', 'c': '270.23',
62
+ # 'v': '55.211', 'ma50': '260.554', 'ma200': '251.828'}
63
+
64
+ print(f"{latest['d']}: close=${latest['c']} vol={latest['v']}M shares")
65
+ ```
66
+
67
+ ### dataDaily field reference
68
+
69
+ | Field | Meaning | Type |
70
+ |-------|---------|------|
71
+ | `d` | Date (YYYY-MM-DD) | str |
72
+ | `o` | Open price (adjusted for splits) | str/float |
73
+ | `h` | High | str/float |
74
+ | `l` | Low | str/float |
75
+ | `c` | Close | str/float |
76
+ | `v` | Volume in **millions of shares** | str/float |
77
+ | `ma50` | 50-day moving average | str/float (appears on recent records only) |
78
+ | `ma200` | 200-day moving average | str/float (appears on recent records only) |
79
+
80
+ **Note:** All price values are strings — cast with `float()`. Volume is millions: `55.211` = 55.2M shares traded.
81
+
82
+ ### Confirmed tickers (2026-04-18)
83
+
84
+ All tested with direct iframe URL, no page fetch needed:
85
+
86
+ ```python
87
+ # All work: AAPL, MSFT, TSLA, NVDA, GOOGL, AMZN, META, NFLX, etc.
88
+ # 3772 records for yb=15 (goes back to 2011-04-18)
89
+ # AAPL full history: 11428 records back to 1980-12-12
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Pattern 2: Stock fundamentals (PE ratio, revenue, market cap, margins)
95
+
96
+ Different PHP files depending on metric. Construct directly.
97
+
98
+ ### Market cap (daily, in billions USD)
99
+
100
+ ```python
101
+ import json, re
102
+ from helpers import http_get
103
+
104
+ def get_market_cap(ticker: str, years_back: int = 15) -> list[dict]:
105
+ url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/market_cap.php?t={ticker}&yb={years_back}"
106
+ html = http_get(url)
107
+ m = re.search(r'var\s+chartData\s*=\s*\[', html)
108
+ si = html.index('[', m.start())
109
+ bc = 0
110
+ for j, ch in enumerate(html[si:], si):
111
+ if ch == '[': bc += 1
112
+ elif ch == ']':
113
+ bc -= 1
114
+ if bc == 0: ei = j; break
115
+ return json.loads(html[si:ei+1])
116
+
117
+ data = get_market_cap('AAPL')
118
+ # [{'date': '2026-04-15', 'v1': 3929.35}, {'date': '2026-04-16', 'v1': 3884.67}, ...]
119
+ # v1 = market cap in billions USD
120
+ ```
121
+
122
+ ### PE ratio, revenue, current ratio (quarterly/annual fundamentals)
123
+
124
+ ```python
125
+ import json, re
126
+ from helpers import http_get
127
+
128
+ def get_fundamental(ticker: str, metric_type: str, statement: str,
129
+ freq: str = 'Q', years_back: int = 15) -> list[dict]:
130
+ """
131
+ freq: 'Q' = quarterly, 'A' = annual
132
+ """
133
+ url = (
134
+ f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
135
+ f"fundamental_iframe.php?t={ticker}&type={metric_type}&statement={statement}"
136
+ f"&freq={freq}&sub=&yb={years_back}"
137
+ )
138
+ html = http_get(url)
139
+ m = re.search(r'var\s+chartData\s*=\s*\[', html)
140
+ si = html.index('[', m.start())
141
+ bc = 0
142
+ for j, ch in enumerate(html[si:], si):
143
+ if ch == '[': bc += 1
144
+ elif ch == ']':
145
+ bc -= 1
146
+ if bc == 0: ei = j; break
147
+ return json.loads(html[si:ei+1])
148
+
149
+ # PE ratio
150
+ pe = get_fundamental('AAPL', 'pe-ratio', 'price-ratios')
151
+ # [{'date': '2025-09-30', 'v1': 254.146, 'v2': 7.46, 'v3': 34.07}, ...]
152
+ # v1 = stock price, v2 = quarterly EPS, v3 = PE ratio
153
+
154
+ # Revenue
155
+ rev = get_fundamental('AAPL', 'revenue', 'income-statement')
156
+ # [{'date': '2025-12-31', 'v1': 435.617, 'v2': 143.756, 'v3': 15.65}, ...]
157
+ # v1 = TTM revenue ($B), v2 = quarterly revenue ($B), v3 = YoY growth %
158
+
159
+ # Total assets
160
+ assets = get_fundamental('AAPL', 'total-assets', 'balance-sheet')
161
+
162
+ # Current ratio
163
+ ratio = get_fundamental('AAPL', 'current-ratio', 'ratios')
164
+ ```
165
+
166
+ ### Profit margins
167
+
168
+ ```python
169
+ def get_profit_margins(ticker: str, years_back: int = 15) -> list[dict]:
170
+ url = (
171
+ f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
172
+ f"fundamental_metric.php?t={ticker}&chart=profit-margin&sub=&yb={years_back}"
173
+ )
174
+ html = http_get(url)
175
+ m = re.search(r'var\s+chartData\s*=\s*\[', html)
176
+ si = html.index('[', m.start())
177
+ bc = 0
178
+ for j, ch in enumerate(html[si:], si):
179
+ if ch == '[': bc += 1
180
+ elif ch == ']':
181
+ bc -= 1
182
+ if bc == 0: ei = j; break
183
+ return json.loads(html[si:ei+1])
184
+
185
+ margins = get_profit_margins('AAPL')
186
+ # [{'date': '2025-12-31', 'v1': 47.33, 'v2': 32.38, 'v3': 27.04}, ...]
187
+ # v1 = gross margin %, v2 = operating margin %, v3 = net margin %
188
+ ```
189
+
190
+ ### Dividend yield
191
+
192
+ ```python
193
+ def get_dividend_yield(ticker: str, years_back: int = 15) -> list[dict]:
194
+ url = f"https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/dividend_yield.php?t={ticker}&yb={years_back}"
195
+ html = http_get(url)
196
+ m = re.search(r'var\s+chartData\s*=\s*\[', html)
197
+ si = html.index('[', m.start())
198
+ bc = 0
199
+ for j, ch in enumerate(html[si:], si):
200
+ if ch == '[': bc += 1
201
+ elif ch == ']':
202
+ bc -= 1
203
+ if bc == 0: ei = j; break
204
+ return json.loads(html[si:ei+1])
205
+
206
+ dy = get_dividend_yield('AAPL')
207
+ # [{'date': '2026-04-17', 'c': 270.23, 'ttm_d': 1.03848, 'ttm_dy': 0.3843}, ...]
208
+ # c = stock price, ttm_d = TTM dividend ($), ttm_dy = TTM yield (%)
209
+ ```
210
+
211
+ ### Stock metric URL reference
212
+
213
+ | Metric | PHP file | Extra params |
214
+ |--------|----------|-------------|
215
+ | Stock price OHLCV | `stock_price_history.php` | — |
216
+ | Market cap (daily) | `market_cap.php` | — |
217
+ | Dividend yield | `dividend_yield.php` | — |
218
+ | Stock splits (price history) | `stock_splits.php` | — |
219
+ | PE ratio | `fundamental_iframe.php` | `type=pe-ratio&statement=price-ratios` |
220
+ | Revenue | `fundamental_iframe.php` | `type=revenue&statement=income-statement` |
221
+ | Total assets | `fundamental_iframe.php` | `type=total-assets&statement=balance-sheet` |
222
+ | Current ratio | `fundamental_iframe.php` | `type=current-ratio&statement=ratios` |
223
+ | Profit margins | `fundamental_metric.php` | `chart=profit-margin` |
224
+
225
+ Base URL prefix: `https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/`
226
+
227
+ All take `?t={TICKER}&yb={N}` (or `&sub=&yb={N}` for the fundamental ones).
228
+
229
+ ---
230
+
231
+ ## Pattern 3: Index and composite charts (S&P 500, Shiller PE, etc.)
232
+
233
+ These pages embed chart data via `chart_iframe_comp.php`. The variable is `originalData`.
234
+
235
+ ```python
236
+ import json, re
237
+ from helpers import http_get
238
+
239
+ def extract_index_chart(page_id: int, url_slug: str) -> list[dict]:
240
+ """
241
+ page_id: the numeric ID from the page URL, e.g. 2577
242
+ url_slug: last segment of the page URL, e.g. 'sp500-pe-ratio-price-to-earnings-chart'
243
+ """
244
+ url = f"https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id={page_id}&url={url_slug}"
245
+ html = http_get(url)
246
+ m = re.search(r'var\s+originalData\s*=\s*\[', html)
247
+ if not m:
248
+ raise ValueError("originalData not found — this page may use a different pattern")
249
+ si = html.index('[', m.start())
250
+ bc = 0
251
+ for j, ch in enumerate(html[si:], si):
252
+ if ch == '[': bc += 1
253
+ elif ch == ']':
254
+ bc -= 1
255
+ if bc == 0: ei = j; break
256
+ return json.loads(html[si:ei+1])
257
+
258
+ # S&P 500 PE ratio (1180 monthly records, 1927-2026)
259
+ pe_data = extract_index_chart(2577, 'sp500-pe-ratio-price-to-earnings-chart')
260
+ # [{'date': '1927-12-01', 'close': '15.9099'}, ..., {'date': '2026-03-01', 'close': '27.8925'}]
261
+ # 'close' is the PE ratio value
262
+
263
+ # Gold prices (1336 monthly records, 1915-2026)
264
+ gold_data = extract_index_chart(1333, 'historical-gold-prices-100-year-chart')
265
+ # [{'id': 'GOLDAMGBD228NLBM', 'date': '1915-01-01', 'close': '629.36', 'close1': '19.250'}, ...]
266
+ # 'close' = inflation-adjusted price, 'close1' = nominal USD price
267
+
268
+ print(f"Latest S&P PE: {pe_data[-1]}") # {'date': '2026-03-01', 'close': '27.8925'}
269
+ print(f"Latest gold: {gold_data[-1]}") # {'id': ..., 'date': '2026-04-01', 'close': '5177.19', 'close1': '5177.190'}
270
+ ```
271
+
272
+ ### Detecting which pattern a page uses
273
+
274
+ ```python
275
+ def get_page_pattern(page_url: str) -> str:
276
+ html = http_get(page_url)
277
+ if 'chart_iframe_comp.php' in html:
278
+ return 'index_chart' # use extract_index_chart()
279
+ elif 'generateChart' in html and 'highchartsURL' in html:
280
+ return 'economic_api' # use get_economic_data()
281
+ elif '/production/stocks/desktop/PRODUCTION/' in html:
282
+ return 'stock_iframe' # use get_stock_ohlcv() etc.
283
+ return 'unknown'
284
+ ```
285
+
286
+ ### To get the ID and slug from a page
287
+
288
+ ```python
289
+ import re
290
+ from helpers import http_get
291
+
292
+ page_url = "https://www.macrotrends.net/2577/sp500-pe-ratio-price-to-earnings-chart"
293
+ html = http_get(page_url)
294
+
295
+ # Option A: parse from the iframe src in the HTML
296
+ m = re.search(r'chart_iframe_comp\.php\?id=(\d+)&url=([^"&]+)', html)
297
+ if m:
298
+ page_id, url_slug = int(m.group(1)), m.group(2)
299
+
300
+ # Option B: derive from the page URL (works when slug matches)
301
+ import urllib.parse
302
+ parts = page_url.rstrip('/').split('/')
303
+ page_id = int(parts[-2]) # 2577
304
+ url_slug = parts[-1] # 'sp500-pe-ratio-price-to-earnings-chart'
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Pattern 4: Economic indicator API
310
+
311
+ Pages that use `generateChart()` in their JS load data from `/economic-data/{pageID}/{freq}`.
312
+ This endpoint requires a `Referer` header matching the page URL.
313
+
314
+ ```python
315
+ import json, datetime, gzip, urllib.request
316
+ from helpers import http_get
317
+
318
+ def get_economic_data(page_id: int, referer_url: str, freq: str = 'D') -> dict:
319
+ """
320
+ page_id: numeric ID from the page URL (e.g. 2015 for Fed Funds Rate)
321
+ referer_url: the full page URL — required as Referer header
322
+ freq: 'D' = daily, 'M' = monthly (not all support both)
323
+
324
+ Returns {'data': [[ts_ms, value], ...], 'metadata': {...}}
325
+ """
326
+ url = f"https://www.macrotrends.net/economic-data/{page_id}/{freq}"
327
+ headers = {
328
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
329
+ "Accept": "application/json, */*",
330
+ "Accept-Encoding": "gzip",
331
+ "Referer": referer_url,
332
+ }
333
+ with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=20) as r:
334
+ raw = r.read()
335
+ if r.headers.get("Content-Encoding") == "gzip":
336
+ raw = gzip.decompress(raw)
337
+ result = json.loads(raw)
338
+ if result is None:
339
+ raise ValueError(f"pageID={page_id} does not support freq={freq!r}")
340
+ return result
341
+
342
+ # Fed Funds Rate (daily, 25319 records)
343
+ ffr = get_economic_data(2015, "https://www.macrotrends.net/2015/fed-funds-rate-historical-chart", freq='D')
344
+ print(ffr['metadata']['name']) # 'Fed Funds Interest Rate'
345
+ print(ffr['metadata']['label']) # '%'
346
+
347
+ # Convert timestamps to dates
348
+ for ts_ms, value in ffr['data'][-3:]:
349
+ dt = datetime.datetime.fromtimestamp(ts_ms / 1000, datetime.UTC)
350
+ print(f"{dt.strftime('%Y-%m-%d')}: {value}%")
351
+ # 2026-04-13: 3.64%
352
+ # 2026-04-14: 3.64%
353
+ # 2026-04-15: 3.64%
354
+
355
+ # 10-Year Treasury yield (daily, 16074 records)
356
+ t10 = get_economic_data(2016, "https://www.macrotrends.net/2016/10-year-treasury-bond-rate-yield-chart", freq='D')
357
+ # Last: 2026-04-15: 4.29%
358
+
359
+ # Gold prices (monthly, 1336 records, 1915-present) — template=5
360
+ gold = get_economic_data(1333, "https://www.macrotrends.net/1333/historical-gold-prices-100-year-chart", freq='M')
361
+ # metadata: {'name': 'Gold Prices', 'currency': '$', 'label': ''}
362
+
363
+ # US Unemployment Rate (monthly, 938 records)
364
+ unemp = get_economic_data(1316, "https://www.macrotrends.net/1316/us-national-unemployment-rate", freq='M')
365
+ # metadata: {'name': 'U.S. Unemployment Rate', 'label': '%'}
366
+
367
+ # Debt-to-GDP ratio (monthly, 712 records)
368
+ debt_gdp = get_economic_data(1381, "https://www.macrotrends.net/1381/debt-to-gdp-ratio-historical-chart", freq='M')
369
+ ```
370
+
371
+ ### metadata fields
372
+
373
+ ```python
374
+ {
375
+ 'name': 'Fed Funds Interest Rate', # chart title
376
+ 'tableHeaderName': 'Fed Funds Interest Rate',
377
+ 'currency': '', # '$' for dollar-denominated series
378
+ 'label': '%', # units label
379
+ 'chartType': 'line',
380
+ 'mobileChartType': 'line',
381
+ 'lineWidth': 2,
382
+ 'positiveColor': '#2caffe',
383
+ 'negativeColor': '',
384
+ 'decimals': '',
385
+ 'chartScale': 'linear',
386
+ 'seriesUnits': ''
387
+ }
388
+ ```
389
+
390
+ ### Available frequency codes
391
+
392
+ | Code | Meaning | Notes |
393
+ |------|---------|-------|
394
+ | `D` | Daily | Most series support this |
395
+ | `M` | Monthly | Returns `null` if not available |
396
+ | `Q` | Quarterly | Usually `null` — use `M` instead |
397
+ | `A` | Annual | Usually `null` — use `M` instead |
398
+ | `DEFAULT` | Default (usually monthly) | Same data as `M` for most series |
399
+ | `INDEXMONTHLY` | Monthly index close | Some commodity/index series |
400
+ | `INDEXDAILY` | Daily index | Some series |
401
+ | `DAILYEXCHANGERATE` | Daily FX rate | Currency pairs |
402
+ | `10YD` | 10-year daily | Specialized series |
403
+
404
+ Try `D` first, fall back to `M` if you get `null`.
405
+
406
+ ### Known economic page IDs
407
+
408
+ | ID | URL slug | Description |
409
+ |----|----------|-------------|
410
+ | 1316 | us-national-unemployment-rate | U.S. Unemployment Rate (monthly, back to 1948) |
411
+ | 1333 | historical-gold-prices-100-year-chart | Gold Prices (monthly, back to 1915) |
412
+ | 1381 | debt-to-gdp-ratio-historical-chart | U.S. Debt to GDP Ratio |
413
+ | 2015 | fed-funds-rate-historical-chart | Fed Funds Interest Rate (daily, back to 1954) |
414
+ | 2016 | 10-year-treasury-bond-rate-yield-chart | 10-Year Treasury Yield (daily, back to 1962) |
415
+ | 2577 | sp500-pe-ratio-price-to-earnings-chart | S&P 500 PE Ratio (uses `chart_iframe_comp.php`) |
416
+
417
+ ---
418
+
419
+ ## Generic extraction helper
420
+
421
+ One function that handles all three embedded-JS patterns:
422
+
423
+ ```python
424
+ import json, re
425
+ from helpers import http_get
426
+
427
+ def extract_chart_var(html: str, var_name: str) -> list:
428
+ """Extract a JS array variable from Macrotrends iframe HTML."""
429
+ m = re.search(rf'var\s+{re.escape(var_name)}\s*=\s*\[', html)
430
+ if not m:
431
+ return []
432
+ si = html.index('[', m.start())
433
+ bc = 0
434
+ for j, ch in enumerate(html[si:], si):
435
+ if ch == '[': bc += 1
436
+ elif ch == ']':
437
+ bc -= 1
438
+ if bc == 0:
439
+ return json.loads(html[si:j+1])
440
+ return []
441
+
442
+ # Works for dataDaily, chartData, or originalData:
443
+ html = http_get("https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/stock_price_history.php?t=AAPL&yb=15")
444
+ daily = extract_chart_var(html, 'dataDaily')
445
+
446
+ html2 = http_get("https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id=2577&url=sp500-pe-ratio-price-to-earnings-chart")
447
+ pe_data = extract_chart_var(html2, 'originalData')
448
+ ```
449
+
450
+ ---
451
+
452
+ ## URL construction guide
453
+
454
+ ### Stock pages
455
+
456
+ ```python
457
+ STOCK_BASE = "https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/"
458
+
459
+ # Price history OHLCV
460
+ f"{STOCK_BASE}stock_price_history.php?t={ticker}" # all history
461
+ f"{STOCK_BASE}stock_price_history.php?t={ticker}&yb={years}" # last N years
462
+
463
+ # Market cap
464
+ f"{STOCK_BASE}market_cap.php?t={ticker}&yb={years}"
465
+
466
+ # Fundamentals
467
+ f"{STOCK_BASE}fundamental_iframe.php?t={ticker}&type={type}&statement={stmt}&freq={freq}&sub=&yb={years}"
468
+ # type/statement combos: pe-ratio/price-ratios, revenue/income-statement,
469
+ # total-assets/balance-sheet, current-ratio/ratios
470
+
471
+ # Metrics
472
+ f"{STOCK_BASE}fundamental_metric.php?t={ticker}&chart={metric}&sub=&yb={years}"
473
+ # metrics: profit-margin
474
+
475
+ # Dividend yield
476
+ f"{STOCK_BASE}dividend_yield.php?t={ticker}&yb={years}"
477
+ ```
478
+
479
+ ### Economic / index pages
480
+
481
+ ```python
482
+ # From numeric ID + URL slug (read from page source or page URL)
483
+ f"https://www.macrotrends.net/assets/php/chart_iframe_comp.php?id={id}&url={slug}"
484
+
485
+ # Economic indicator JSON API (requires Referer header)
486
+ f"https://www.macrotrends.net/economic-data/{page_id}/{freq}"
487
+ ```
488
+
489
+ ---
490
+
491
+ ## Rate limits and anti-bot
492
+
493
+ - **No rate limiting observed** at any tested volume. 10 rapid requests to the same stock iframe completed in 1.8s with no throttling, CAPTCHA, or 429 errors.
494
+ - **Default UA works** (`Mozilla/5.0`) for most endpoints. The iframe PHP files never 403'd.
495
+ - **Chrome UA needed** for some main HTML pages (not data endpoints): use when fetching `/stocks/charts/...` or `/2015/...` wrapper pages if you get 403. Switch to:
496
+ ```python
497
+ headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}
498
+ ```
499
+ - **Referer required** for `/economic-data/{id}/{freq}` — send the page URL as `Referer`. Without it, the request is allowed but you get a 403 on some pages.
500
+ - **No cookies, sessions, or auth tokens** needed for any endpoint.
501
+
502
+ ---
503
+
504
+ ## Gotchas
505
+
506
+ **Main page URL ≠ data page:** Some URLs redirect to different content. `/1316/us-national-debt-by-year` redirects to `/1316/us-national-unemployment-rate`. Always check the final URL with `r.url` if the returned data looks wrong. Use the final URL as the Referer.
507
+
508
+ **yb parameter controls history depth:**
509
+ - `yb=1` → ~250 records (last year)
510
+ - `yb=15` → ~3772 records (last 15 years)
511
+ - omit → full history (AAPL: 11428 records to 1980; default for most queries)
512
+
513
+ **Two iframe patterns for economic pages:** Pages at `macrotrends.net/NNNN/slug` use either `chart_iframe_comp.php` (→ `originalData`) or `generateChart` + `/economic-data/` API. Check the main page HTML to detect which:
514
+ ```python
515
+ if 'chart_iframe_comp.php' in html: # use extract_index_chart()
516
+ elif 'highchartsURL' in html: # use get_economic_data()
517
+ ```
518
+
519
+ **Gold data has two price columns:**
520
+ ```python
521
+ {'id': 'GOLDAMGBD228NLBM', 'date': '2026-04-01', 'close': '5177.19', 'close1': '5177.190'}
522
+ # 'close' = inflation-adjusted price (base year adjusts over time)
523
+ # 'close1' = nominal USD price (the raw market price)
524
+ ```
525
+
526
+ **Economic API frequency codes:** Only `D` and `M` consistently return data across most series. `A` and `Q` return `null` for most economic indicators. Always try `D` first.
527
+
528
+ **chartData fields vary by metric:**
529
+ - `market_cap.php` → `{'date', 'v1'}` (v1 = market cap in $B)
530
+ - `fundamental_iframe.php` type=pe-ratio → `{'date', 'v1', 'v2', 'v3'}` (stock price, EPS, PE)
531
+ - `fundamental_iframe.php` type=revenue → `{'date', 'v1', 'v2', 'v3'}` (TTM revenue, quarterly revenue, YoY%)
532
+ - `fundamental_metric.php` chart=profit-margin → `{'date', 'v1', 'v2', 'v3'}` (gross%, operating%, net%)
533
+ - `dividend_yield.php` → `{'date', 'c', 'ttm_d', 'ttm_dy'}` (price, dividend, yield%)
534
+
535
+ **Bracket matching required for large arrays:** The `var dataDaily = [...]` in stock iframes is ~450KB with 3772 OHLCV records. The `re.DOTALL` greedy approach works but is slow; bracket-counting (`bc` pattern above) is O(n) and fast.
536
+
537
+ **No public API for ticker lookup:** To find the company slug for a URL, check the search endpoint: `https://www.macrotrends.net/production/stocks/desktop/PRODUCTION/ticker_search_list.php?v=YYYYMMDD` — but the stock price iframe only needs the ticker symbol (`?t=AAPL`), not the slug.