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,508 @@
1
+ """Market data MCP tools for stocks, funds, and other instruments."""
2
+
3
+ from fastmcp import Context
4
+
5
+ from .. import mcp
6
+ from ..client import AvanzaClient
7
+ from ..services import MarketDataService
8
+
9
+
10
+ @mcp.tool()
11
+ async def get_stock_info(
12
+ ctx: Context,
13
+ instrument_id: str,
14
+ ) -> dict:
15
+ """Get detailed information about a specific stock.
16
+
17
+ Provides comprehensive stock data including current price, trading volume,
18
+ market capitalization, valuation metrics (P/E, P/B ratios), dividend yield,
19
+ and company description.
20
+
21
+ Use search_instruments() first to find the instrument_id for a stock.
22
+
23
+ Args:
24
+ ctx: MCP context for logging
25
+ instrument_id: Avanza instrument ID from search results
26
+
27
+ Returns:
28
+ Detailed stock information including:
29
+ - quote: Current price, change, volume, trading data
30
+ - company: Description, CEO, sector, market cap
31
+ - key_ratios: P/E ratio, dividend yield, volatility, beta
32
+ - Trading metadata: market, tradeable status
33
+
34
+ Examples:
35
+ Get info for Volvo B (ID from search):
36
+ >>> get_stock_info(instrument_id="5479")
37
+ """
38
+ ctx.info(f"Fetching stock info for ID: {instrument_id}")
39
+
40
+ try:
41
+ async with AvanzaClient() as client:
42
+ service = MarketDataService(client)
43
+ stock_info = await service.get_stock_info(instrument_id)
44
+
45
+ ctx.info(f"Retrieved info for: {stock_info.name}")
46
+ return stock_info.model_dump(by_alias=True, exclude_none=True)
47
+
48
+ except Exception as e:
49
+ ctx.error(f"Failed to fetch stock info: {str(e)}")
50
+ raise
51
+
52
+
53
+ @mcp.tool()
54
+ async def get_fund_info(
55
+ ctx: Context,
56
+ instrument_id: str,
57
+ ) -> dict:
58
+ """Get detailed information about a mutual fund.
59
+
60
+ Provides comprehensive fund data including NAV (Net Asset Value), performance
61
+ over various time periods, risk metrics, fees, and fund characteristics.
62
+
63
+ Use search_instruments() with instrument_type="fund" to find fund IDs.
64
+
65
+ Args:
66
+ ctx: MCP context for logging
67
+ instrument_id: Avanza fund ID from search results
68
+
69
+ Returns:
70
+ Detailed fund information including:
71
+ - Basic info: name, ISIN, description, NAV
72
+ - Performance: returns over 1w, 1m, 3m, YTD, 1y, 3y, 5y, 10y
73
+ - Risk: risk level (1-7), standard deviation, Sharpe ratio
74
+ - Fees: ongoing charges, entry/exit fees
75
+ - Characteristics: fund company, type, category, AUM
76
+
77
+ Examples:
78
+ Get info for a specific fund:
79
+ >>> get_fund_info(instrument_id="12345")
80
+ """
81
+ ctx.info(f"Fetching fund info for ID: {instrument_id}")
82
+
83
+ try:
84
+ async with AvanzaClient() as client:
85
+ service = MarketDataService(client)
86
+ fund_info = await service.get_fund_info(instrument_id)
87
+
88
+ ctx.info(f"Retrieved info for: {fund_info.name}")
89
+ return fund_info.model_dump(by_alias=True, exclude_none=True)
90
+
91
+ except Exception as e:
92
+ ctx.error(f"Failed to fetch fund info: {str(e)}")
93
+ raise
94
+
95
+
96
+ @mcp.tool()
97
+ async def get_stock_chart(
98
+ ctx: Context,
99
+ instrument_id: str,
100
+ time_period: str = "one_year",
101
+ ) -> dict:
102
+ """Get historical price chart data (OHLC) for a stock.
103
+
104
+ Retrieves time series price data with Open, High, Low, Close values and volume.
105
+ Perfect for charting and technical analysis.
106
+
107
+ Args:
108
+ ctx: MCP context for logging
109
+ instrument_id: Avanza instrument ID
110
+ time_period: Time period for chart data. Options:
111
+ - "today": Intraday data for today
112
+ - "one_week": Past week
113
+ - "one_month": Past month
114
+ - "three_months": Past 3 months
115
+ - "this_year": Year to date
116
+ - "one_year": Past year (default)
117
+ - "three_years": Past 3 years
118
+ - "five_years": Past 5 years
119
+
120
+ Returns:
121
+ Chart data with OHLC values:
122
+ - ohlc: Array of data points with timestamp, open, high, low, close, volume
123
+ - metadata: Chart metadata
124
+ - from/to: Time range
125
+ - previousClosingPrice: Previous closing price
126
+
127
+ Examples:
128
+ Get 1 year daily data:
129
+ >>> get_stock_chart(instrument_id="5269", time_period="one_year")
130
+
131
+ Get past week data:
132
+ >>> get_stock_chart(instrument_id="5269", time_period="one_week")
133
+ """
134
+ ctx.info(f"Fetching chart data for ID: {instrument_id} (time_period={time_period})")
135
+
136
+ try:
137
+ async with AvanzaClient() as client:
138
+ service = MarketDataService(client)
139
+ chart_data = await service.get_chart_data(
140
+ instrument_id=instrument_id,
141
+ time_period=time_period,
142
+ )
143
+
144
+ data_points = len(chart_data.ohlc)
145
+ ctx.info(f"Retrieved {data_points} OHLC data points")
146
+ return chart_data.model_dump(by_alias=True, exclude_none=True)
147
+
148
+ except Exception as e:
149
+ ctx.error(f"Failed to fetch chart data: {str(e)}")
150
+ raise
151
+
152
+
153
+ @mcp.tool()
154
+ async def get_orderbook(
155
+ ctx: Context,
156
+ instrument_id: str,
157
+ ) -> dict:
158
+ """Get real-time order book depth for an instrument.
159
+
160
+ Shows current buy and sell orders with prices and volumes at each level.
161
+ Useful for understanding market depth, liquidity, and bid-ask spread.
162
+
163
+ Args:
164
+ ctx: MCP context for logging
165
+ instrument_id: Avanza instrument ID
166
+
167
+ Returns:
168
+ Order book depth data with:
169
+ - receivedTime: Timestamp of order book data
170
+ - levels: Array of price levels, each containing:
171
+ - buySide: Buy order with price, volume, priceString
172
+ - sellSide: Sell order with price, volume, priceString
173
+
174
+ Examples:
175
+ Get current order book:
176
+ >>> get_orderbook(instrument_id="5269")
177
+ """
178
+ ctx.info(f"Fetching order book for ID: {instrument_id}")
179
+
180
+ try:
181
+ async with AvanzaClient() as client:
182
+ service = MarketDataService(client)
183
+ orderbook = await service.get_order_depth(instrument_id)
184
+
185
+ levels_count = len(orderbook.levels)
186
+ ctx.info(f"Retrieved order book with {levels_count} levels")
187
+ return orderbook.model_dump(by_alias=True, exclude_none=True)
188
+
189
+ except Exception as e:
190
+ ctx.error(f"Failed to fetch order book: {str(e)}")
191
+ raise
192
+
193
+
194
+ @mcp.tool()
195
+ async def get_stock_analysis(
196
+ ctx: Context,
197
+ instrument_id: str,
198
+ ) -> dict:
199
+ """Get detailed stock analysis with key financial ratios.
200
+
201
+ Provides comprehensive financial analysis including key ratios grouped by:
202
+ - Annual data (stockKeyRatiosByYear)
203
+ - Quarterly data (stockKeyRatiosByQuarter)
204
+ - Quarter-over-quarter comparisons (stockKeyRatiosByQuarterQuarter)
205
+ - Trailing twelve months (TTM) data (stockKeyRatiosByQuarterTTM)
206
+ - Company-level annual ratios (companyKeyRatiosByYear)
207
+
208
+ Use search_instruments() first to find the instrument_id for a stock.
209
+
210
+ Args:
211
+ ctx: MCP context for logging
212
+ instrument_id: Avanza instrument ID from search results
213
+
214
+ Returns:
215
+ Comprehensive analysis data with:
216
+ - stockKeyRatiosByYear: Annual financial ratios
217
+ - stockKeyRatiosByQuarter: Quarterly financial ratios
218
+ - stockKeyRatiosByQuarterTTM: TTM financial ratios
219
+ - stockKeyRatiosByQuarterQuarter: Q-o-Q comparisons
220
+ - companyKeyRatiosByYear: Company-level annual metrics
221
+ - And more detailed financial analysis data
222
+
223
+ Examples:
224
+ Get analysis for Volvo B (ID from search):
225
+ >>> get_stock_analysis(instrument_id="5479")
226
+ """
227
+ ctx.info(f"Fetching stock analysis for ID: {instrument_id}")
228
+
229
+ try:
230
+ async with AvanzaClient() as client:
231
+ service = MarketDataService(client)
232
+ analysis = await service.get_stock_analysis(instrument_id)
233
+
234
+ ctx.info(f"Retrieved analysis data")
235
+ return analysis
236
+
237
+ except Exception as e:
238
+ ctx.error(f"Failed to fetch stock analysis: {str(e)}")
239
+ raise
240
+
241
+
242
+ @mcp.tool()
243
+ async def get_stock_quote(
244
+ ctx: Context,
245
+ instrument_id: str,
246
+ ) -> dict:
247
+ """Get real-time stock quote with current pricing and trading data.
248
+
249
+ Provides immediate price information including bid/ask spread, last trade,
250
+ trading volumes, and real-time status. Lighter weight than get_stock_info.
251
+
252
+ Args:
253
+ ctx: MCP context for logging
254
+ instrument_id: Avanza instrument ID from search results
255
+
256
+ Returns:
257
+ Real-time quote data with:
258
+ - buy: Current buy (bid) price
259
+ - sell: Current sell (ask) price
260
+ - last: Last traded price
261
+ - highest: Highest price today
262
+ - lowest: Lowest price today
263
+ - change: Price change
264
+ - changePercent: Percentage change
265
+ - totalValueTraded: Total value traded
266
+ - totalVolumeTraded: Total volume traded
267
+ - volumeWeightedAveragePrice: VWAP
268
+ - isRealTime: Whether data is real-time
269
+
270
+ Examples:
271
+ Get current quote for a stock:
272
+ >>> get_stock_quote(instrument_id="5269")
273
+ """
274
+ ctx.info(f"Fetching stock quote for ID: {instrument_id}")
275
+
276
+ try:
277
+ async with AvanzaClient() as client:
278
+ service = MarketDataService(client)
279
+ quote = await service.get_stock_quote(instrument_id)
280
+
281
+ ctx.info(f"Retrieved quote: last={quote.last}, change={quote.changePercent}%")
282
+ return quote.model_dump(by_alias=True, exclude_none=True)
283
+
284
+ except Exception as e:
285
+ ctx.error(f"Failed to fetch stock quote: {str(e)}")
286
+ raise
287
+
288
+
289
+ @mcp.tool()
290
+ async def get_marketplace_info(
291
+ ctx: Context,
292
+ instrument_id: str,
293
+ ) -> dict:
294
+ """Get marketplace status and trading hours for an instrument.
295
+
296
+ Provides information about whether the market is open, time remaining until close,
297
+ and today's opening/closing times.
298
+
299
+ Args:
300
+ ctx: MCP context for logging
301
+ instrument_id: Avanza instrument ID from search results
302
+
303
+ Returns:
304
+ Marketplace information with:
305
+ - marketOpen: Boolean indicating if market is currently open
306
+ - timeLeftMs: Milliseconds remaining until market close
307
+ - openingTime: Today's market opening time
308
+ - todayClosingTime: Today's market closing time
309
+ - normalClosingTime: Standard market closing time
310
+
311
+ Examples:
312
+ Check if market is open:
313
+ >>> get_marketplace_info(instrument_id="5269")
314
+ """
315
+ ctx.info(f"Fetching marketplace info for ID: {instrument_id}")
316
+
317
+ try:
318
+ async with AvanzaClient() as client:
319
+ service = MarketDataService(client)
320
+ marketplace = await service.get_marketplace_info(instrument_id)
321
+
322
+ status = "open" if marketplace.marketOpen else "closed"
323
+ ctx.info(f"Market is {status}")
324
+ return marketplace.model_dump(by_alias=True, exclude_none=True)
325
+
326
+ except Exception as e:
327
+ ctx.error(f"Failed to fetch marketplace info: {str(e)}")
328
+ raise
329
+
330
+
331
+ @mcp.tool()
332
+ async def get_recent_trades(
333
+ ctx: Context,
334
+ instrument_id: str,
335
+ ) -> dict:
336
+ """Get recent trades for an instrument.
337
+
338
+ Returns a list of the most recent trades with price, volume, timestamp,
339
+ and trade metadata. Useful for understanding recent trading activity.
340
+
341
+ Args:
342
+ ctx: MCP context for logging
343
+ instrument_id: Avanza instrument ID from search results
344
+
345
+ Returns:
346
+ List of recent trades, each containing:
347
+ - buyer: Buyer information
348
+ - seller: Seller information
349
+ - dealTime: Trade timestamp
350
+ - price: Trade price
351
+ - volume: Trade volume
352
+ - matchedOnMarket: Market where trade was matched
353
+
354
+ Examples:
355
+ Get recent trades:
356
+ >>> get_recent_trades(instrument_id="5269")
357
+ """
358
+ ctx.info(f"Fetching recent trades for ID: {instrument_id}")
359
+
360
+ try:
361
+ async with AvanzaClient() as client:
362
+ service = MarketDataService(client)
363
+ trades = await service.get_trades(instrument_id)
364
+
365
+ ctx.info(f"Retrieved {len(trades)} recent trades")
366
+ return {
367
+ "trades": [trade.model_dump(by_alias=True, exclude_none=True) for trade in trades]
368
+ }
369
+
370
+ except Exception as e:
371
+ ctx.error(f"Failed to fetch recent trades: {str(e)}")
372
+ raise
373
+
374
+
375
+ @mcp.tool()
376
+ async def get_broker_trade_summary(
377
+ ctx: Context,
378
+ instrument_id: str,
379
+ ) -> dict:
380
+ """Get broker trade summary showing buy/sell activity.
381
+
382
+ Returns aggregated broker trading data showing total buy and sell volumes.
383
+ Useful for understanding institutional trading activity.
384
+
385
+ Args:
386
+ ctx: MCP context for logging
387
+ instrument_id: Avanza instrument ID from search results
388
+
389
+ Returns:
390
+ List of broker trade summaries with:
391
+ - brokerCode: Broker identifier
392
+ - buyVolume: Total volume bought
393
+ - sellVolume: Total volume sold
394
+ - netVolume: Net volume (buy - sell)
395
+
396
+ Examples:
397
+ Get broker trading activity:
398
+ >>> get_broker_trade_summary(instrument_id="5269")
399
+ """
400
+ ctx.info(f"Fetching broker trade summary for ID: {instrument_id}")
401
+
402
+ try:
403
+ async with AvanzaClient() as client:
404
+ service = MarketDataService(client)
405
+ summaries = await service.get_broker_trades(instrument_id)
406
+
407
+ ctx.info(f"Retrieved {len(summaries)} broker trade summaries")
408
+ return {
409
+ "summaries": [
410
+ summary.model_dump(by_alias=True, exclude_none=True) for summary in summaries
411
+ ]
412
+ }
413
+
414
+ except Exception as e:
415
+ ctx.error(f"Failed to fetch broker trade summary: {str(e)}")
416
+ raise
417
+
418
+
419
+ @mcp.tool()
420
+ async def get_dividends(
421
+ ctx: Context,
422
+ instrument_id: str,
423
+ ) -> dict:
424
+ """Get historical dividend data for a stock.
425
+
426
+ Returns dividend history by year including amounts, dates, and yields.
427
+ Useful for income investors and dividend analysis.
428
+
429
+ Args:
430
+ ctx: MCP context for logging
431
+ instrument_id: Avanza instrument ID from search results
432
+
433
+ Returns:
434
+ Dividend data with:
435
+ - dividendsByYear: Array of yearly dividend data including:
436
+ - year: Year of dividend
437
+ - dividend: Dividend amount per share
438
+ - exDate: Ex-dividend date
439
+ - paymentDate: Payment date
440
+ - yield: Dividend yield percentage
441
+ - currency: Dividend currency
442
+
443
+ Examples:
444
+ Get dividend history for a stock:
445
+ >>> get_dividends(instrument_id="5479")
446
+ """
447
+ ctx.info(f"Fetching dividend data for ID: {instrument_id}")
448
+
449
+ try:
450
+ async with AvanzaClient() as client:
451
+ service = MarketDataService(client)
452
+ dividends = await service.get_dividends(instrument_id)
453
+
454
+ years = len(dividends.get("dividendsByYear", []))
455
+ ctx.info(f"Retrieved {years} years of dividend data")
456
+ return dividends
457
+
458
+ except Exception as e:
459
+ ctx.error(f"Failed to fetch dividend data: {str(e)}")
460
+ raise
461
+
462
+
463
+ @mcp.tool()
464
+ async def get_company_financials(
465
+ ctx: Context,
466
+ instrument_id: str,
467
+ ) -> dict:
468
+ """Get company financial statements and metrics.
469
+
470
+ Returns comprehensive financial data including revenue, profits, margins,
471
+ and other key metrics by year and quarter.
472
+
473
+ Args:
474
+ ctx: MCP context for logging
475
+ instrument_id: Avanza instrument ID from search results
476
+
477
+ Returns:
478
+ Financial data with:
479
+ - companyFinancialsByYear: Annual financial data
480
+ - companyFinancialsByQuarter: Quarterly financial data
481
+ - companyFinancialsByQuarterTTM: Trailing twelve months data
482
+ Each containing metrics like:
483
+ - revenue: Total revenue
484
+ - operatingProfit: Operating profit
485
+ - netProfit: Net profit
486
+ - grossMargin: Gross profit margin
487
+ - operatingMargin: Operating margin
488
+ - netMargin: Net profit margin
489
+
490
+ Examples:
491
+ Get financials for a company:
492
+ >>> get_company_financials(instrument_id="5479")
493
+ """
494
+ ctx.info(f"Fetching company financials for ID: {instrument_id}")
495
+
496
+ try:
497
+ async with AvanzaClient() as client:
498
+ service = MarketDataService(client)
499
+ financials = await service.get_company_financials(instrument_id)
500
+
501
+ years = len(financials.get("companyFinancialsByYear", []))
502
+ quarters = len(financials.get("companyFinancialsByQuarter", []))
503
+ ctx.info(f"Retrieved financials: {years} years, {quarters} quarters")
504
+ return financials
505
+
506
+ except Exception as e:
507
+ ctx.error(f"Failed to fetch company financials: {str(e)}")
508
+ raise
@@ -0,0 +1,119 @@
1
+ """Search-related MCP tools."""
2
+
3
+ from typing import Literal
4
+
5
+ from fastmcp import Context
6
+
7
+ from .. import mcp
8
+ from ..client import AvanzaClient
9
+ from ..services import SearchService
10
+
11
+
12
+ @mcp.tool()
13
+ async def search_instruments(
14
+ ctx: Context,
15
+ query: str,
16
+ instrument_type: Literal[
17
+ "stock", "fund", "etf", "certificate", "warrant", "all"
18
+ ] = "all",
19
+ limit: int = 10,
20
+ ) -> dict:
21
+ """Search for financial instruments on Avanza.
22
+
23
+ Searches across stocks, funds, ETFs, certificates, and warrants.
24
+ Returns detailed search results including price info, sectors, and metadata.
25
+
26
+ Args:
27
+ ctx: MCP context for logging
28
+ query: Search term (company name, ticker symbol, or ISIN)
29
+ instrument_type: Type of instrument to search for. Options:
30
+ - "stock": Stocks only
31
+ - "fund": Mutual funds only
32
+ - "etf": ETFs only
33
+ - "certificate": Certificates only
34
+ - "warrant": Warrants only
35
+ - "all": All instrument types (default)
36
+ limit: Maximum number of results to return (1-50, default: 10)
37
+
38
+ Returns:
39
+ Search response with:
40
+ - totalNumberOfHits: Total matching results
41
+ - hits: Array of search results with:
42
+ - orderBookId: Unique ID
43
+ - type: Instrument type (STOCK, FUND, etc.)
44
+ - title: Name
45
+ - price: Price information
46
+ - marketPlaceName: Exchange/market
47
+ - And more details...
48
+ - facets: Type breakdowns with counts
49
+ - searchQuery: The query that was executed
50
+
51
+ Examples:
52
+ Search for Volvo stock:
53
+ >>> search_instruments(query="Volvo", instrument_type="stock", limit=5)
54
+
55
+ Search for any instrument matching "Global":
56
+ >>> search_instruments(query="Global", instrument_type="all", limit=10)
57
+ """
58
+ ctx.info(f"Searching for '{query}' (type: {instrument_type}, limit: {limit})")
59
+
60
+ try:
61
+ # Validate limit
62
+ limit = max(1, min(limit, 50))
63
+
64
+ async with AvanzaClient() as client:
65
+ service = SearchService(client)
66
+ response = await service.search(
67
+ query=query,
68
+ instrument_type=instrument_type if instrument_type != "all" else None,
69
+ limit=limit,
70
+ )
71
+
72
+ ctx.info(f"Found {response.totalNumberOfHits} total hits, returning {len(response.hits)} results")
73
+
74
+ # Return the full response as dict
75
+ return response.model_dump(by_alias=True)
76
+
77
+ except Exception as e:
78
+ ctx.error(f"Search failed: {str(e)}")
79
+ raise
80
+
81
+
82
+ @mcp.tool()
83
+ async def get_instrument_by_order_book_id(
84
+ ctx: Context,
85
+ order_book_id: str,
86
+ ) -> dict | None:
87
+ """Look up a financial instrument by its order book ID.
88
+
89
+ The order book ID is the unique identifier returned from search results.
90
+
91
+ Args:
92
+ ctx: MCP context for logging
93
+ order_book_id: Order book ID to search for
94
+
95
+ Returns:
96
+ First matching search hit if found, None otherwise
97
+
98
+ Examples:
99
+ Look up by order book ID:
100
+ >>> get_instrument_by_order_book_id(order_book_id="878733")
101
+ """
102
+ ctx.info(f"Looking up instrument with order book ID: {order_book_id}")
103
+
104
+ try:
105
+ async with AvanzaClient() as client:
106
+ service = SearchService(client)
107
+ response = await service.search(query=order_book_id, limit=1)
108
+
109
+ if response.hits:
110
+ hit = response.hits[0]
111
+ ctx.info(f"Found: {hit.title}")
112
+ return hit.model_dump()
113
+ else:
114
+ ctx.info("No instrument found with that order book ID")
115
+ return None
116
+
117
+ except Exception as e:
118
+ ctx.error(f"Lookup failed: {str(e)}")
119
+ raise