avanza-mcp 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,116 @@
1
+ """Analysis prompt templates for common workflows."""
2
+
3
+ from .. import mcp
4
+
5
+
6
+ @mcp.prompt()
7
+ def analyze_stock(stock_symbol: str) -> str:
8
+ """Comprehensive stock analysis workflow.
9
+
10
+ Guides the LLM through analyzing a stock including company information,
11
+ price performance, recent news, and valuation metrics.
12
+
13
+ Args:
14
+ stock_symbol: Stock ticker symbol or name to analyze
15
+
16
+ Returns:
17
+ Prompt text for stock analysis
18
+ """
19
+ return f"""Please perform a comprehensive analysis of {stock_symbol}:
20
+
21
+ 1. **Find the stock**: Use search_instruments() to find the stock and get its instrument_id
22
+ 2. **Get detailed information**: Use get_stock_info() to retrieve:
23
+ - Current price and recent performance
24
+ - Company description and sector
25
+ - Key financial ratios (P/E, dividend yield, etc.)
26
+ 3. **Analyze price trends**: Use get_stock_chart() to get historical data (1 year, daily)
27
+ 4. **Check recent news**: Use get_news() to see recent company announcements
28
+ 5. **Assess the orderbook**: Use get_orderbook() to check current liquidity
29
+
30
+ Based on this data, provide:
31
+ - **Current valuation assessment** (is it overvalued/undervalued?)
32
+ - **Price trend analysis** (what's the recent momentum?)
33
+ - **Key risks and opportunities**
34
+ - **Overall investment perspective**
35
+
36
+ Focus on factual analysis based on the available data.
37
+ """
38
+
39
+
40
+ @mcp.prompt()
41
+ def compare_funds(fund_names: str) -> str:
42
+ """Compare multiple funds across key metrics.
43
+
44
+ Analyzes and compares funds on performance, fees, risk metrics,
45
+ and characteristics.
46
+
47
+ Args:
48
+ fund_names: Comma-separated list of fund names to compare
49
+
50
+ Returns:
51
+ Prompt text for fund comparison
52
+ """
53
+ funds = [f.strip() for f in fund_names.split(",")]
54
+ funds_list = "\n".join([f"- {fund}" for fund in funds])
55
+
56
+ return f"""Compare the following funds:
57
+ {funds_list}
58
+
59
+ For each fund:
60
+ 1. **Find the fund**: Use search_instruments() with instrument_type="fund"
61
+ 2. **Get fund details**: Use get_fund_info() to retrieve:
62
+ - Current NAV and recent performance
63
+ - Performance over multiple periods (YTD, 1Y, 3Y, 5Y)
64
+ - Risk rating and metrics
65
+ - Fees (ongoing charges, entry/exit fees)
66
+ - Fund characteristics (type, category, AUM)
67
+
68
+ Then create a comparison showing:
69
+
70
+ | Metric | {' | '.join(funds)} |
71
+ |--------|{'-|' * len(funds)}
72
+ | NAV | ... | ... |
73
+ | YTD Return | ... | ... |
74
+ | 1Y Return | ... | ... |
75
+ | 3Y Return | ... | ... |
76
+ | 5Y Return | ... | ... |
77
+ | Risk Level (1-7) | ... | ... |
78
+ | Ongoing Charges | ... | ... |
79
+ | Fund Size (AUM) | ... | ... |
80
+
81
+ Conclude with:
82
+ - Which fund has the best risk-adjusted returns?
83
+ - Which has the lowest fees?
84
+ - Recommendation based on the comparison
85
+ """
86
+
87
+
88
+ @mcp.prompt()
89
+ def screen_dividend_stocks(min_yield: float = 3.0) -> str:
90
+ """Screen for dividend-paying stocks.
91
+
92
+ Helps find stocks with attractive dividend yields.
93
+
94
+ Args:
95
+ min_yield: Minimum dividend yield percentage (default: 3.0)
96
+
97
+ Returns:
98
+ Prompt text for dividend screening
99
+ """
100
+ return f"""Help me find dividend stocks with yields above {min_yield}%.
101
+
102
+ Process:
103
+ 1. Search for major Swedish stocks (you might start with well-known companies)
104
+ 2. For each stock, use get_stock_info() to check the dividend yield
105
+ 3. Filter for stocks with dividend_yield >= {min_yield}%
106
+
107
+ Present results as a table:
108
+
109
+ | Stock | Ticker | Price | Dividend Yield | P/E Ratio | Market Cap |
110
+ |-------|--------|-------|----------------|-----------|------------|
111
+ | ... | ... | ... | ...% | ... | ... |
112
+
113
+ Sort by dividend yield (highest first) and include:
114
+ - Brief assessment of sustainability (based on P/E and other metrics)
115
+ - Any notable risks or observations
116
+ """
@@ -0,0 +1,6 @@
1
+ """MCP resources for Avanza API."""
2
+
3
+ # Import to register resources via decorators
4
+ from . import instruments # noqa: F401
5
+
6
+ __all__ = ["instruments"]
@@ -0,0 +1,127 @@
1
+ """URI-based instrument resources."""
2
+
3
+ from .. import mcp
4
+ from ..client import AvanzaClient
5
+ from ..services import MarketDataService
6
+
7
+
8
+ def format_stock_markdown(stock_data: dict) -> str:
9
+ """Format stock info as markdown.
10
+
11
+ Args:
12
+ stock_data: Stock information dictionary
13
+
14
+ Returns:
15
+ Formatted markdown string
16
+ """
17
+ quote = stock_data.get("quote", {})
18
+ company = stock_data.get("company", {})
19
+ listing = stock_data.get("listing", {})
20
+ key_ratios = stock_data.get("key_ratios") or stock_data.get("keyIndicators", {})
21
+
22
+ name = stock_data.get("name", "Unknown")
23
+ price = quote.get("last", "N/A")
24
+ change = quote.get("change", 0)
25
+ change_pct = quote.get("changePercent", 0)
26
+ currency = listing.get("currency", "SEK")
27
+
28
+ md = f"# {name}\n\n"
29
+ md += f"**Price:** {price} {currency}\n"
30
+ md += f"**Change:** {change:+.2f} ({change_pct:+.2f}%)\n\n"
31
+
32
+ if company:
33
+ if desc := company.get("description"):
34
+ md += f"## Company\n{desc}\n\n"
35
+ if market_cap := company.get("marketCapital"):
36
+ if isinstance(market_cap, dict):
37
+ cap_value = market_cap.get("value", 0)
38
+ cap_currency = market_cap.get("currency", currency)
39
+ md += f"**Market Cap:** {cap_value:,.0f} {cap_currency}\n"
40
+ else:
41
+ md += f"**Market Cap:** {market_cap:,.0f} {currency}\n"
42
+
43
+ if key_ratios:
44
+ md += "\n## Key Ratios\n"
45
+ if pe := key_ratios.get("priceEarningsRatio"):
46
+ md += f"- **P/E Ratio:** {pe:.2f}\n"
47
+ if div_yield := key_ratios.get("directYield"):
48
+ md += f"- **Dividend Yield:** {div_yield:.2f}%\n"
49
+
50
+ return md
51
+
52
+
53
+ def format_fund_markdown(fund_data: dict) -> str:
54
+ """Format fund info as markdown.
55
+
56
+ Args:
57
+ fund_data: Fund information dictionary
58
+
59
+ Returns:
60
+ Formatted markdown string
61
+ """
62
+ name = fund_data.get("name", "Unknown")
63
+ nav = fund_data.get("nav", "N/A")
64
+ currency = fund_data.get("currency", "SEK")
65
+
66
+ md = f"# {name}\n\n"
67
+ md += f"**NAV:** {nav} {currency}\n\n"
68
+
69
+ if desc := fund_data.get("description"):
70
+ md += f"{desc}\n\n"
71
+
72
+ if development := fund_data.get("development"):
73
+ md += "## Performance\n"
74
+ if ytd := development.get("thisYear"):
75
+ md += f"- **YTD:** {ytd:+.2f}%\n"
76
+ if one_year := development.get("oneYear"):
77
+ md += f"- **1 Year:** {one_year:+.2f}%\n"
78
+ if three_years := development.get("threeYears"):
79
+ md += f"- **3 Years:** {three_years:+.2f}%\n"
80
+
81
+ if risk := fund_data.get("risk"):
82
+ md += f"\n**Risk Level:** {risk}/7\n"
83
+
84
+ if fee := fund_data.get("fee", {}).get("ongoingCharges"):
85
+ md += f"**Ongoing Charges:** {fee:.2f}%\n"
86
+
87
+ return md
88
+
89
+
90
+ @mcp.resource("avanza://stock/{instrument_id}")
91
+ async def get_stock_resource(instrument_id: str) -> str:
92
+ """Get stock information as a markdown resource.
93
+
94
+ URI: avanza://stock/{instrument_id}
95
+
96
+ Args:
97
+ instrument_id: Avanza stock ID
98
+
99
+ Returns:
100
+ Formatted markdown with stock information
101
+ """
102
+ async with AvanzaClient() as client:
103
+ service = MarketDataService(client)
104
+ stock_info = await service.get_stock_info(instrument_id)
105
+
106
+ stock_data = stock_info.model_dump(by_alias=True, exclude_none=True)
107
+ return format_stock_markdown(stock_data)
108
+
109
+
110
+ @mcp.resource("avanza://fund/{instrument_id}")
111
+ async def get_fund_resource(instrument_id: str) -> str:
112
+ """Get fund information as a markdown resource.
113
+
114
+ URI: avanza://fund/{instrument_id}
115
+
116
+ Args:
117
+ instrument_id: Avanza fund ID
118
+
119
+ Returns:
120
+ Formatted markdown with fund information
121
+ """
122
+ async with AvanzaClient() as client:
123
+ service = MarketDataService(client)
124
+ fund_info = await service.get_fund_info(instrument_id)
125
+
126
+ fund_data = fund_info.model_dump(by_alias=True, exclude_none=True)
127
+ return format_fund_markdown(fund_data)
@@ -0,0 +1,6 @@
1
+ """Business logic services for Avanza API."""
2
+
3
+ from .market_data_service import MarketDataService
4
+ from .search_service import SearchService
5
+
6
+ __all__ = ["SearchService", "MarketDataService"]
@@ -0,0 +1,298 @@
1
+ """Market data service for retrieving stock and fund information."""
2
+
3
+ from typing import Any
4
+
5
+ from ..client.base import AvanzaClient
6
+ from ..client.endpoints import PublicEndpoint
7
+ from ..models.fund import (
8
+ FundChart,
9
+ FundChartPeriod,
10
+ FundDescription,
11
+ FundInfo,
12
+ FundSustainability,
13
+ )
14
+ from ..models.stock import (
15
+ BrokerTradeSummary,
16
+ MarketplaceInfo,
17
+ OrderDepth,
18
+ Quote,
19
+ StockChart,
20
+ StockInfo,
21
+ Trade,
22
+ )
23
+
24
+
25
+ class MarketDataService:
26
+ """Service for retrieving market data."""
27
+
28
+ def __init__(self, client: AvanzaClient) -> None:
29
+ """Initialize market data service.
30
+
31
+ Args:
32
+ client: Avanza HTTP client
33
+ """
34
+ self._client = client
35
+
36
+ async def get_stock_info(self, instrument_id: str) -> StockInfo:
37
+ """Fetch detailed stock information.
38
+
39
+ Args:
40
+ instrument_id: Avanza instrument ID
41
+
42
+ Returns:
43
+ Detailed stock information
44
+
45
+ Raises:
46
+ AvanzaError: If request fails
47
+ """
48
+ endpoint = PublicEndpoint.STOCK_INFO.format(id=instrument_id)
49
+ raw_data = await self._client.get(endpoint)
50
+ return StockInfo.model_validate(raw_data)
51
+
52
+ async def get_fund_info(self, instrument_id: str) -> FundInfo:
53
+ """Fetch detailed fund information.
54
+
55
+ Args:
56
+ instrument_id: Avanza fund ID
57
+
58
+ Returns:
59
+ Detailed fund information
60
+
61
+ Raises:
62
+ AvanzaError: If request fails
63
+ """
64
+ endpoint = PublicEndpoint.FUND_INFO.format(id=instrument_id)
65
+ raw_data = await self._client.get(endpoint)
66
+ return FundInfo.model_validate(raw_data)
67
+
68
+ async def get_order_depth(self, instrument_id: str) -> OrderDepth:
69
+ """Fetch real-time order book depth data.
70
+
71
+ Args:
72
+ instrument_id: Avanza instrument ID
73
+
74
+ Returns:
75
+ Order book depth with buy and sell levels
76
+
77
+ Raises:
78
+ AvanzaError: If request fails
79
+ """
80
+ endpoint = PublicEndpoint.STOCK_ORDERDEPTH.format(id=instrument_id)
81
+ raw_data = await self._client.get(endpoint)
82
+ return OrderDepth.model_validate(raw_data)
83
+
84
+ async def get_chart_data(
85
+ self,
86
+ instrument_id: str,
87
+ time_period: str = "one_year",
88
+ ) -> StockChart:
89
+ """Fetch historical chart data with OHLC values.
90
+
91
+ Args:
92
+ instrument_id: Avanza instrument ID
93
+ time_period: Time period - one_week, one_month, three_months, one_year, etc.
94
+
95
+ Returns:
96
+ Chart data with OHLC values
97
+
98
+ Raises:
99
+ AvanzaError: If request fails
100
+ """
101
+ endpoint = PublicEndpoint.STOCK_CHART.format(id=instrument_id)
102
+ params = {"timePeriod": time_period}
103
+ raw_data = await self._client.get(endpoint, params=params)
104
+ return StockChart.model_validate(raw_data)
105
+
106
+ async def get_marketplace_info(self, instrument_id: str) -> MarketplaceInfo:
107
+ """Fetch marketplace status and trading hours.
108
+
109
+ Args:
110
+ instrument_id: Avanza instrument ID
111
+
112
+ Returns:
113
+ Marketplace information including open/close times
114
+
115
+ Raises:
116
+ AvanzaError: If request fails
117
+ """
118
+ endpoint = PublicEndpoint.STOCK_MARKETPLACE.format(id=instrument_id)
119
+ raw_data = await self._client.get(endpoint)
120
+ return MarketplaceInfo.model_validate(raw_data)
121
+
122
+ async def get_trades(self, instrument_id: str) -> list[Trade]:
123
+ """Fetch recent trades for an instrument.
124
+
125
+ Args:
126
+ instrument_id: Avanza instrument ID
127
+
128
+ Returns:
129
+ List of recent trades
130
+
131
+ Raises:
132
+ AvanzaError: If request fails
133
+ """
134
+ endpoint = PublicEndpoint.STOCK_TRADES.format(id=instrument_id)
135
+ raw_data = await self._client.get(endpoint)
136
+ return [Trade.model_validate(trade) for trade in raw_data]
137
+
138
+ async def get_broker_trades(self, instrument_id: str) -> list[BrokerTradeSummary]:
139
+ """Fetch broker trade summaries.
140
+
141
+ Args:
142
+ instrument_id: Avanza instrument ID
143
+
144
+ Returns:
145
+ List of broker trade summaries with buy/sell volumes
146
+
147
+ Raises:
148
+ AvanzaError: If request fails
149
+ """
150
+ endpoint = PublicEndpoint.STOCK_BROKER_TRADES.format(id=instrument_id)
151
+ raw_data = await self._client.get(endpoint)
152
+ return [BrokerTradeSummary.model_validate(trade) for trade in raw_data]
153
+
154
+ async def get_stock_analysis(self, instrument_id: str) -> dict[str, Any]:
155
+ """Fetch stock analysis with key ratios by year and quarter.
156
+
157
+ Args:
158
+ instrument_id: Avanza instrument ID
159
+
160
+ Returns:
161
+ Stock analysis data with key ratios grouped by time periods
162
+
163
+ Raises:
164
+ AvanzaError: If request fails
165
+ """
166
+ endpoint = PublicEndpoint.STOCK_ANALYSIS.format(id=instrument_id)
167
+ return await self._client.get(endpoint)
168
+
169
+ async def get_dividends(self, instrument_id: str) -> dict[str, Any]:
170
+ """Fetch dividend history from stock analysis data.
171
+
172
+ Args:
173
+ instrument_id: Avanza instrument ID
174
+
175
+ Returns:
176
+ Dividend data by year including:
177
+ - dividend: Dividend amount per share
178
+ - exDate: Ex-dividend date
179
+ - paymentDate: Payment date
180
+ - yield: Dividend yield percentage
181
+
182
+ Raises:
183
+ AvanzaError: If request fails
184
+ """
185
+ endpoint = PublicEndpoint.STOCK_ANALYSIS.format(id=instrument_id)
186
+ analysis = await self._client.get(endpoint)
187
+ return {
188
+ "dividendsByYear": analysis.get("dividendsByYear", []),
189
+ }
190
+
191
+ async def get_company_financials(self, instrument_id: str) -> dict[str, Any]:
192
+ """Fetch company financial data from stock analysis.
193
+
194
+ Args:
195
+ instrument_id: Avanza instrument ID
196
+
197
+ Returns:
198
+ Company financials by year and quarter including revenue,
199
+ profit margins, earnings, and other financial metrics
200
+
201
+ Raises:
202
+ AvanzaError: If request fails
203
+ """
204
+ endpoint = PublicEndpoint.STOCK_ANALYSIS.format(id=instrument_id)
205
+ analysis = await self._client.get(endpoint)
206
+ return {
207
+ "companyFinancialsByYear": analysis.get("companyFinancialsByYear", []),
208
+ "companyFinancialsByQuarter": analysis.get("companyFinancialsByQuarter", []),
209
+ "companyFinancialsByQuarterTTM": analysis.get(
210
+ "companyFinancialsByQuarterTTM", []
211
+ ),
212
+ }
213
+
214
+ async def get_stock_quote(self, instrument_id: str) -> Quote:
215
+ """Fetch real-time stock quote with current pricing.
216
+
217
+ Args:
218
+ instrument_id: Avanza instrument ID
219
+
220
+ Returns:
221
+ Real-time quote with buy, sell, last price, and trading volumes
222
+
223
+ Raises:
224
+ AvanzaError: If request fails
225
+ """
226
+ endpoint = PublicEndpoint.STOCK_QUOTE.format(id=instrument_id)
227
+ raw_data = await self._client.get(endpoint)
228
+ return Quote.model_validate(raw_data)
229
+
230
+ async def get_fund_sustainability(self, instrument_id: str) -> FundSustainability:
231
+ """Fetch fund sustainability and ESG metrics.
232
+
233
+ Args:
234
+ instrument_id: Avanza fund ID
235
+
236
+ Returns:
237
+ Sustainability metrics including ESG scores and environmental data
238
+
239
+ Raises:
240
+ AvanzaError: If request fails
241
+ """
242
+ endpoint = PublicEndpoint.FUND_SUSTAINABILITY.format(id=instrument_id)
243
+ raw_data = await self._client.get(endpoint)
244
+ return FundSustainability.model_validate(raw_data)
245
+
246
+ async def get_fund_chart(
247
+ self, instrument_id: str, time_period: str = "three_years"
248
+ ) -> FundChart:
249
+ """Fetch fund chart data for a specific time period.
250
+
251
+ Args:
252
+ instrument_id: Avanza fund ID
253
+ time_period: Time period (e.g., three_years, five_years, etc.)
254
+
255
+ Returns:
256
+ Fund chart with historical performance data
257
+
258
+ Raises:
259
+ AvanzaError: If request fails
260
+ """
261
+ endpoint = PublicEndpoint.FUND_CHART.format(
262
+ id=instrument_id, time_period=time_period
263
+ )
264
+ raw_data = await self._client.get(endpoint)
265
+ return FundChart.model_validate(raw_data)
266
+
267
+ async def get_fund_chart_periods(self, instrument_id: str) -> list[FundChartPeriod]:
268
+ """Fetch available fund chart periods with performance data.
269
+
270
+ Args:
271
+ instrument_id: Avanza fund ID
272
+
273
+ Returns:
274
+ List of time periods with performance changes
275
+
276
+ Raises:
277
+ AvanzaError: If request fails
278
+ """
279
+ endpoint = PublicEndpoint.FUND_CHART_PERIODS.format(id=instrument_id)
280
+ raw_data = await self._client.get(endpoint)
281
+ return [FundChartPeriod.model_validate(period) for period in raw_data]
282
+
283
+ async def get_fund_description(self, instrument_id: str) -> FundDescription:
284
+ """Fetch fund description and category information.
285
+
286
+ Args:
287
+ instrument_id: Avanza fund ID
288
+
289
+ Returns:
290
+ Fund description with detailed category information
291
+
292
+ Raises:
293
+ AvanzaError: If request fails
294
+ """
295
+ endpoint = PublicEndpoint.FUND_DESCRIPTION.format(id=instrument_id)
296
+ raw_data = await self._client.get(endpoint)
297
+ return FundDescription.model_validate(raw_data)
298
+
@@ -0,0 +1,64 @@
1
+ """Search service for finding financial instruments."""
2
+
3
+ from typing import Any
4
+
5
+ from ..client.base import AvanzaClient
6
+ from ..client.endpoints import PublicEndpoint
7
+ from ..models.common import InstrumentType
8
+ from ..models.search import SearchResponse
9
+
10
+
11
+ class SearchService:
12
+ """Service for searching financial instruments."""
13
+
14
+ def __init__(self, client: AvanzaClient) -> None:
15
+ """Initialize search service.
16
+
17
+ Args:
18
+ client: Avanza HTTP client
19
+ """
20
+ self._client = client
21
+
22
+ async def search(
23
+ self,
24
+ query: str,
25
+ instrument_type: str | InstrumentType | None = None,
26
+ limit: int = 10,
27
+ ) -> SearchResponse:
28
+ """Search for financial instruments.
29
+
30
+ Args:
31
+ query: Search term (company name, ticker, ISIN)
32
+ instrument_type: Optional type filter (stock, fund, etc.)
33
+ limit: Maximum number of results
34
+
35
+ Returns:
36
+ Complete search response with hits, facets, and pagination
37
+ """
38
+ # Build search payload
39
+ payload: dict[str, Any] = {
40
+ "query": query,
41
+ "limit": limit,
42
+ }
43
+
44
+ # Add instrument type filter if specified
45
+ if instrument_type and instrument_type != "all":
46
+ if isinstance(instrument_type, str):
47
+ # Convert string to InstrumentType enum
48
+ try:
49
+ type_value = InstrumentType[instrument_type.upper()].value
50
+ except KeyError:
51
+ type_value = instrument_type.upper()
52
+ else:
53
+ type_value = instrument_type.value
54
+
55
+ payload["instrumentType"] = type_value
56
+
57
+ # Make search request
58
+ response = await self._client.post(
59
+ PublicEndpoint.SEARCH.value,
60
+ json=payload,
61
+ )
62
+
63
+ # Parse and return typed response
64
+ return SearchResponse.model_validate(response)
@@ -0,0 +1,8 @@
1
+ """MCP tools for Avanza API."""
2
+
3
+ # Import to register tools via decorators
4
+ from . import funds # noqa: F401
5
+ from . import market_data # noqa: F401
6
+ from . import search # noqa: F401
7
+
8
+ __all__ = ["search", "market_data", "funds"]