@pencil-agent/nano-pencil 2.0.1 → 2.0.3

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 (188) 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/model/custom-providers.js +1 -1
  7. package/dist/core/model-registry.js +5 -5
  8. package/dist/extensions/builtin/AGENT.md +115 -115
  9. package/dist/extensions/builtin/browser/AGENT.md +17 -17
  10. package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
  11. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
  12. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
  13. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
  14. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
  15. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
  16. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
  17. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
  18. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
  19. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
  20. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
  21. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
  22. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
  23. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
  24. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
  25. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
  26. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
  27. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
  28. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
  29. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
  30. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
  31. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
  32. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
  33. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
  34. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
  35. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
  36. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
  37. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
  38. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
  39. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
  40. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
  41. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
  42. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
  43. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
  44. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
  45. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
  46. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
  47. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
  48. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
  49. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
  50. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
  51. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
  52. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
  53. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
  54. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
  55. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
  56. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
  57. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
  58. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
  59. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
  60. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
  61. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
  62. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
  63. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
  64. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
  65. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
  66. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
  67. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
  68. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
  69. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
  70. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
  71. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
  72. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
  73. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
  74. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
  75. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
  76. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
  77. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
  78. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
  79. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
  80. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
  81. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
  82. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
  83. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
  84. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
  85. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
  86. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
  87. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
  88. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
  89. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
  90. package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
  91. package/dist/extensions/builtin/browser/browser.md +73 -73
  92. package/dist/extensions/builtin/browser/install.md +142 -142
  93. package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
  94. package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
  95. package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
  96. package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
  97. package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
  98. package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
  99. package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
  100. package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
  101. package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
  102. package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
  103. package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
  104. package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
  105. package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
  106. package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
  107. package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
  108. package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
  109. package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
  110. package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
  111. package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
  112. package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
  113. package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
  114. package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
  115. package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
  116. package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
  117. package/dist/extensions/builtin/debug/index.js +9 -9
  118. package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
  119. package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
  120. package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
  121. package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
  122. package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
  123. package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
  124. package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
  125. package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
  126. package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
  127. package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
  128. package/dist/extensions/builtin/goal/README.md +67 -67
  129. package/dist/extensions/builtin/goal/index.js +6 -6
  130. package/dist/extensions/builtin/grub/README.md +112 -112
  131. package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
  132. package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
  133. package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
  134. package/dist/extensions/builtin/link-world/linkworld.md +313 -313
  135. package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
  136. package/dist/extensions/builtin/loop/README.md +92 -92
  137. package/dist/extensions/builtin/mcp/figma-design.md +68 -68
  138. package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
  139. package/dist/extensions/builtin/recap/AGENT.md +15 -15
  140. package/dist/extensions/builtin/sal/README.md +72 -72
  141. package/dist/extensions/builtin/security-audit/README.md +289 -289
  142. package/dist/extensions/builtin/team/AGENT.md +112 -112
  143. package/dist/extensions/builtin/team/TESTING.md +299 -299
  144. package/dist/extensions/builtin/token-save/README.md +56 -56
  145. package/dist/extensions/optional/AGENT.md +10 -10
  146. package/dist/modes/interactive/controllers/input-submit-controller.js +2 -2
  147. package/dist/modes/interactive/controllers/stream-render-controller.js +2 -2
  148. package/dist/modes/interactive/interactive-mode.js +19 -19
  149. package/dist/modes/interactive/theme/dark.json +85 -85
  150. package/dist/modes/interactive/theme/light.json +84 -84
  151. package/dist/modes/interactive/theme/theme-schema.json +335 -335
  152. package/dist/modes/interactive/theme/warm.json +81 -81
  153. package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
  154. package/dist/node_modules/@pencil-agent/ai/dist/models.generated.js +1 -1
  155. 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 +851 -0
  156. package/docs/SDK-TESTING.md +364 -0
  157. package/docs/codex-goal-command-impl.md +1055 -1055
  158. package/docs/codex-goal-vs-grub.md +500 -500
  159. package/docs/custom-provider.md +27 -27
  160. package/docs/extensions.md +27 -27
  161. package/docs/keybindings.md +27 -27
  162. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
  163. package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
  164. package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
  165. 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
  166. 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
  167. package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
  168. package/docs/loop-usage-examples.md +214 -214
  169. package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
  170. package/docs/models.md +27 -27
  171. package/docs/packages.md +27 -27
  172. package/docs/pi-design-philosophy.md +457 -457
  173. package/docs/planmode.md +1987 -1987
  174. package/docs/prompt-templates.md +27 -27
  175. package/docs/providers.md +27 -27
  176. package/docs/sdk.md +27 -27
  177. package/docs/skills.md +27 -27
  178. package/docs/startup-performance-optimization.md +301 -0
  179. package/docs/themes.md +27 -27
  180. package/docs/tui.md +27 -27
  181. package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
  182. package/package.json +190 -190
  183. package/docs/cc-agent-design.md +0 -1297
  184. package/docs/cc-tui-design.md +0 -1333
  185. package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +0 -170
  186. package/docs/scan-report.md +0 -3820
  187. package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
  188. 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 +0 -261
@@ -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.