avanza-mcp 1.1.0__py3-none-any.whl → 1.2.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.
@@ -4,6 +4,32 @@ from typing import Any
4
4
 
5
5
  from ..client.base import AvanzaClient
6
6
  from ..client.endpoints import PublicEndpoint
7
+ from ..models.certificate import (
8
+ CertificateDetails,
9
+ CertificateFilterRequest,
10
+ CertificateFilterResponse,
11
+ CertificateInfo,
12
+ )
13
+ from ..models.chart import ChartData
14
+ from ..models.etf import (
15
+ ETFDetails,
16
+ ETFFilterRequest,
17
+ ETFFilterResponse,
18
+ ETFInfo,
19
+ )
20
+ from ..models.future_forward import (
21
+ FutureForwardDetails,
22
+ FutureForwardInfo,
23
+ FutureForwardMatrixRequest,
24
+ FutureForwardMatrixResponse,
25
+ )
26
+ from ..models.instrument_data import NumberOfOwners, ShortSellingData
27
+ from ..models.warrant import (
28
+ WarrantDetails,
29
+ WarrantFilterRequest,
30
+ WarrantFilterResponse,
31
+ WarrantInfo,
32
+ )
7
33
  from ..models.fund import (
8
34
  FundChart,
9
35
  FundChartPeriod,
@@ -296,3 +322,290 @@ class MarketDataService:
296
322
  raw_data = await self._client.get(endpoint)
297
323
  return FundDescription.model_validate(raw_data)
298
324
 
325
+ # === Certificates ===
326
+
327
+ async def filter_certificates(
328
+ self, filter_request: CertificateFilterRequest
329
+ ) -> CertificateFilterResponse:
330
+ """Filter and list certificates with pagination.
331
+
332
+ Args:
333
+ filter_request: Filter criteria and pagination parameters
334
+
335
+ Returns:
336
+ Filtered list of certificates
337
+
338
+ Raises:
339
+ AvanzaError: If request fails
340
+ """
341
+ endpoint = PublicEndpoint.CERTIFICATE_FILTER.value
342
+ raw_data = await self._client.post(
343
+ endpoint, json=filter_request.model_dump(by_alias=True, exclude_none=True)
344
+ )
345
+ return CertificateFilterResponse.model_validate(raw_data)
346
+
347
+ async def get_certificate_info(self, instrument_id: str) -> CertificateInfo:
348
+ """Fetch detailed certificate information.
349
+
350
+ Args:
351
+ instrument_id: Avanza certificate ID
352
+
353
+ Returns:
354
+ Detailed certificate information
355
+
356
+ Raises:
357
+ AvanzaError: If request fails
358
+ """
359
+ endpoint = PublicEndpoint.CERTIFICATE_INFO.format(id=instrument_id)
360
+ raw_data = await self._client.get(endpoint)
361
+ return CertificateInfo.model_validate(raw_data)
362
+
363
+ async def get_certificate_details(self, instrument_id: str) -> CertificateDetails:
364
+ """Fetch extended certificate details.
365
+
366
+ Args:
367
+ instrument_id: Avanza certificate ID
368
+
369
+ Returns:
370
+ Extended certificate details
371
+
372
+ Raises:
373
+ AvanzaError: If request fails
374
+ """
375
+ endpoint = PublicEndpoint.CERTIFICATE_DETAILS.format(id=instrument_id)
376
+ raw_data = await self._client.get(endpoint)
377
+ return CertificateDetails.model_validate(raw_data)
378
+
379
+ # === Warrants ===
380
+
381
+ async def filter_warrants(
382
+ self, filter_request: WarrantFilterRequest
383
+ ) -> WarrantFilterResponse:
384
+ """Filter and list warrants with pagination.
385
+
386
+ Args:
387
+ filter_request: Filter criteria and pagination parameters
388
+
389
+ Returns:
390
+ Filtered list of warrants
391
+
392
+ Raises:
393
+ AvanzaError: If request fails
394
+ """
395
+ endpoint = PublicEndpoint.WARRANT_FILTER.value
396
+ raw_data = await self._client.post(
397
+ endpoint, json=filter_request.model_dump(by_alias=True, exclude_none=True)
398
+ )
399
+ return WarrantFilterResponse.model_validate(raw_data)
400
+
401
+ async def get_warrant_info(self, instrument_id: str) -> WarrantInfo:
402
+ """Fetch detailed warrant information.
403
+
404
+ Args:
405
+ instrument_id: Avanza warrant ID
406
+
407
+ Returns:
408
+ Detailed warrant information
409
+
410
+ Raises:
411
+ AvanzaError: If request fails
412
+ """
413
+ endpoint = PublicEndpoint.WARRANT_INFO.format(id=instrument_id)
414
+ raw_data = await self._client.get(endpoint)
415
+ return WarrantInfo.model_validate(raw_data)
416
+
417
+ async def get_warrant_details(self, instrument_id: str) -> WarrantDetails:
418
+ """Fetch extended warrant details.
419
+
420
+ Args:
421
+ instrument_id: Avanza warrant ID
422
+
423
+ Returns:
424
+ Extended warrant details
425
+
426
+ Raises:
427
+ AvanzaError: If request fails
428
+ """
429
+ endpoint = PublicEndpoint.WARRANT_DETAILS.format(id=instrument_id)
430
+ raw_data = await self._client.get(endpoint)
431
+ return WarrantDetails.model_validate(raw_data)
432
+
433
+ # === ETFs ===
434
+
435
+ async def filter_etfs(self, filter_request: ETFFilterRequest) -> ETFFilterResponse:
436
+ """Filter and list ETFs with pagination.
437
+
438
+ Args:
439
+ filter_request: Filter criteria and pagination parameters
440
+
441
+ Returns:
442
+ Filtered list of ETFs
443
+
444
+ Raises:
445
+ AvanzaError: If request fails
446
+ """
447
+ endpoint = PublicEndpoint.ETF_FILTER.value
448
+ raw_data = await self._client.post(
449
+ endpoint, json=filter_request.model_dump(by_alias=True, exclude_none=True)
450
+ )
451
+ return ETFFilterResponse.model_validate(raw_data)
452
+
453
+ async def get_etf_info(self, instrument_id: str) -> ETFInfo:
454
+ """Fetch detailed ETF information.
455
+
456
+ Args:
457
+ instrument_id: Avanza ETF ID
458
+
459
+ Returns:
460
+ Detailed ETF information
461
+
462
+ Raises:
463
+ AvanzaError: If request fails
464
+ """
465
+ endpoint = PublicEndpoint.ETF_INFO.format(id=instrument_id)
466
+ raw_data = await self._client.get(endpoint)
467
+ return ETFInfo.model_validate(raw_data)
468
+
469
+ async def get_etf_details(self, instrument_id: str) -> ETFDetails:
470
+ """Fetch extended ETF details.
471
+
472
+ Args:
473
+ instrument_id: Avanza ETF ID
474
+
475
+ Returns:
476
+ Extended ETF details
477
+
478
+ Raises:
479
+ AvanzaError: If request fails
480
+ """
481
+ endpoint = PublicEndpoint.ETF_DETAILS.format(id=instrument_id)
482
+ raw_data = await self._client.get(endpoint)
483
+ return ETFDetails.model_validate(raw_data)
484
+
485
+ # === Futures/Forwards ===
486
+
487
+ async def list_futures_forwards(
488
+ self, request: FutureForwardMatrixRequest
489
+ ) -> FutureForwardMatrixResponse:
490
+ """List futures and forwards using matrix endpoint.
491
+
492
+ Args:
493
+ request: Matrix list request parameters
494
+
495
+ Returns:
496
+ Matrix response with futures/forwards
497
+
498
+ Raises:
499
+ AvanzaError: If request fails
500
+ """
501
+ endpoint = PublicEndpoint.FUTURE_FORWARD_MATRIX.value
502
+ raw_data = await self._client.post(
503
+ endpoint, json=request.model_dump(by_alias=True, exclude_none=True)
504
+ )
505
+ return FutureForwardMatrixResponse.model_validate(raw_data)
506
+
507
+ async def get_future_forward_info(self, instrument_id: str) -> FutureForwardInfo:
508
+ """Fetch detailed future/forward information.
509
+
510
+ Args:
511
+ instrument_id: Avanza future/forward ID
512
+
513
+ Returns:
514
+ Detailed future/forward information
515
+
516
+ Raises:
517
+ AvanzaError: If request fails
518
+ """
519
+ endpoint = PublicEndpoint.FUTURE_FORWARD_INFO.format(id=instrument_id)
520
+ raw_data = await self._client.get(endpoint)
521
+ return FutureForwardInfo.model_validate(raw_data)
522
+
523
+ async def get_future_forward_details(
524
+ self, instrument_id: str
525
+ ) -> FutureForwardDetails:
526
+ """Fetch extended future/forward details.
527
+
528
+ Args:
529
+ instrument_id: Avanza future/forward ID
530
+
531
+ Returns:
532
+ Extended future/forward details
533
+
534
+ Raises:
535
+ AvanzaError: If request fails
536
+ """
537
+ endpoint = PublicEndpoint.FUTURE_FORWARD_DETAILS.format(id=instrument_id)
538
+ raw_data = await self._client.get(endpoint)
539
+ return FutureForwardDetails.model_validate(raw_data)
540
+
541
+ async def get_future_forward_filter_options(self) -> dict[str, Any]:
542
+ """Get available filter options for futures/forwards.
543
+
544
+ Returns:
545
+ Available filter options including underlying instruments, dates, etc.
546
+
547
+ Raises:
548
+ AvanzaError: If request fails
549
+ """
550
+ endpoint = PublicEndpoint.FUTURE_FORWARD_FILTER_OPTIONS.value
551
+ raw_data = await self._client.get(endpoint)
552
+ return raw_data
553
+
554
+ # === Additional Features ===
555
+
556
+ async def get_number_of_owners(self, instrument_id: str) -> NumberOfOwners:
557
+ """Get number of owners for an instrument.
558
+
559
+ Args:
560
+ instrument_id: Avanza instrument ID
561
+
562
+ Returns:
563
+ Number of owners data
564
+
565
+ Raises:
566
+ AvanzaError: If request fails
567
+ """
568
+ endpoint = PublicEndpoint.NUMBER_OF_OWNERS.format(id=instrument_id)
569
+ raw_data = await self._client.get(endpoint)
570
+ return NumberOfOwners.model_validate(raw_data)
571
+
572
+ async def get_short_selling(self, instrument_id: str) -> ShortSellingData:
573
+ """Get short selling data for an instrument.
574
+
575
+ Args:
576
+ instrument_id: Avanza instrument ID
577
+
578
+ Returns:
579
+ Short selling data
580
+
581
+ Raises:
582
+ AvanzaError: If request fails
583
+ """
584
+ endpoint = PublicEndpoint.SHORT_SELLING.format(id=instrument_id)
585
+ raw_data = await self._client.get(endpoint)
586
+ return ShortSellingData.model_validate(raw_data)
587
+
588
+ async def get_marketmaker_chart(
589
+ self, instrument_id: str, time_period: str = "today"
590
+ ) -> ChartData:
591
+ """Get price chart data for traded products (certificates, warrants, ETFs).
592
+
593
+ This endpoint provides OHLC (Open-High-Low-Close) candlestick data
594
+ with market maker information for traded products.
595
+
596
+ Args:
597
+ instrument_id: Avanza instrument ID
598
+ time_period: Time period for chart data (default: "today")
599
+ Available periods: today, one_week, one_month, three_months,
600
+ six_months, one_year, three_years, five_years, etc.
601
+
602
+ Returns:
603
+ Chart data with OHLC candlesticks and metadata
604
+
605
+ Raises:
606
+ AvanzaError: If request fails
607
+ """
608
+ endpoint = PublicEndpoint.MARKETMAKER_CHART.format(id=instrument_id)
609
+ raw_data = await self._client.get(endpoint, params={"timePeriod": time_period})
610
+ return ChartData.model_validate(raw_data)
611
+
@@ -1,8 +1,22 @@
1
1
  """MCP tools for Avanza API."""
2
2
 
3
3
  # Import to register tools via decorators
4
+ from . import certificates # noqa: F401
5
+ from . import etfs # noqa: F401
4
6
  from . import funds # noqa: F401
7
+ from . import futures_forwards # noqa: F401
8
+ from . import instrument_data # noqa: F401
5
9
  from . import market_data # noqa: F401
6
10
  from . import search # noqa: F401
11
+ from . import warrants # noqa: F401
7
12
 
8
- __all__ = ["search", "market_data", "funds"]
13
+ __all__ = [
14
+ "certificates",
15
+ "etfs",
16
+ "funds",
17
+ "futures_forwards",
18
+ "instrument_data",
19
+ "market_data",
20
+ "search",
21
+ "warrants",
22
+ ]
@@ -0,0 +1,156 @@
1
+ """Certificate MCP tools."""
2
+
3
+ from fastmcp import Context
4
+
5
+ from .. import mcp
6
+ from ..client import AvanzaClient
7
+ from ..models.certificate import CertificateFilter, CertificateFilterRequest
8
+ from ..models.filter import SortBy
9
+ from ..services import MarketDataService
10
+
11
+
12
+ @mcp.tool()
13
+ async def filter_certificates(
14
+ ctx: Context,
15
+ offset: int = 0,
16
+ limit: int = 20,
17
+ directions: list[str] | None = None,
18
+ leverages: list[float] | None = None,
19
+ issuers: list[str] | None = None,
20
+ categories: list[str] | None = None,
21
+ exposures: list[str] | None = None,
22
+ underlying_instruments: list[str] | None = None,
23
+ sort_field: str = "name",
24
+ sort_order: str = "asc",
25
+ ) -> dict:
26
+ """Filter and list certificates with optional filtering and pagination.
27
+
28
+ Search through available certificates with optional filters for direction
29
+ (long/short), leverage, issuer, category, exposure, and underlying instrument.
30
+ Supports pagination for large result sets.
31
+
32
+ Args:
33
+ ctx: MCP context for logging
34
+ offset: Number of results to skip (default: 0)
35
+ limit: Maximum number of results to return (default: 20, max: 100)
36
+ directions: Filter by direction, e.g., ["long"], ["short"]
37
+ leverages: Filter by leverage values, e.g., [1.0, 2.0]
38
+ issuers: Filter by issuer names, e.g., ["Valour", "21Shares"]
39
+ categories: Filter by categories
40
+ exposures: Filter by exposures
41
+ underlying_instruments: Filter by underlying instrument IDs
42
+ sort_field: Field to sort by (default: "name")
43
+ sort_order: Sort order "asc" or "desc" (default: "asc")
44
+
45
+ Returns:
46
+ Filtered list of certificates with:
47
+ - certificates: Array of certificate objects with orderbookId, name, price, etc.
48
+ - pagination: Current offset and limit
49
+ - totalNumberOfOrderbooks: Total matching certificates
50
+
51
+ Examples:
52
+ List first 20 certificates:
53
+ >>> filter_certificates()
54
+
55
+ Find long certificates with 1x leverage:
56
+ >>> filter_certificates(directions=["long"], leverages=[1.0])
57
+
58
+ Get next page of results:
59
+ >>> filter_certificates(offset=20, limit=20)
60
+ """
61
+ ctx.info(f"Filtering certificates: offset={offset}, limit={limit}")
62
+
63
+ try:
64
+ filter_req = CertificateFilterRequest(
65
+ filter=CertificateFilter(
66
+ directions=directions or [],
67
+ leverages=leverages or [],
68
+ issuers=issuers or [],
69
+ categories=categories or [],
70
+ exposures=exposures or [],
71
+ underlyingInstruments=underlying_instruments or [],
72
+ ),
73
+ offset=offset,
74
+ limit=min(limit, 100),
75
+ sortBy=SortBy(field=sort_field, order=sort_order),
76
+ )
77
+
78
+ async with AvanzaClient() as client:
79
+ service = MarketDataService(client)
80
+ result = await service.filter_certificates(filter_req)
81
+
82
+ ctx.info(f"Retrieved {len(result.certificates)} certificates")
83
+ return result.model_dump(by_alias=True, exclude_none=True)
84
+
85
+ except Exception as e:
86
+ ctx.error(f"Failed to filter certificates: {str(e)}")
87
+ raise
88
+
89
+
90
+ @mcp.tool()
91
+ async def get_certificate_info(ctx: Context, instrument_id: str) -> dict:
92
+ """Get detailed information about a specific certificate.
93
+
94
+ Provides comprehensive certificate data including current price, leverage,
95
+ underlying instrument, issuer, and historical performance.
96
+
97
+ Args:
98
+ ctx: MCP context for logging
99
+ instrument_id: Avanza certificate ID (orderbookId from filter results)
100
+
101
+ Returns:
102
+ Detailed certificate information including:
103
+ - Basic info: name, ISIN, issuer
104
+ - Pricing: current quote, buy/sell prices
105
+ - Characteristics: leverage, direction, underlying instrument
106
+ - Historical data: price history over various periods
107
+
108
+ Examples:
109
+ Get info for a certificate:
110
+ >>> get_certificate_info(instrument_id="1756318")
111
+ """
112
+ ctx.info(f"Fetching certificate info for ID: {instrument_id}")
113
+
114
+ try:
115
+ async with AvanzaClient() as client:
116
+ service = MarketDataService(client)
117
+ certificate = await service.get_certificate_info(instrument_id)
118
+
119
+ ctx.info(f"Retrieved info for: {certificate.name}")
120
+ return certificate.model_dump(by_alias=True, exclude_none=True)
121
+
122
+ except Exception as e:
123
+ ctx.error(f"Failed to fetch certificate info: {str(e)}")
124
+ raise
125
+
126
+
127
+ @mcp.tool()
128
+ async def get_certificate_details(ctx: Context, instrument_id: str) -> dict:
129
+ """Get extended details about a specific certificate.
130
+
131
+ Provides additional detailed information beyond basic certificate info.
132
+
133
+ Args:
134
+ ctx: MCP context for logging
135
+ instrument_id: Avanza certificate ID
136
+
137
+ Returns:
138
+ Extended certificate details
139
+
140
+ Examples:
141
+ Get detailed info:
142
+ >>> get_certificate_details(instrument_id="1756318")
143
+ """
144
+ ctx.info(f"Fetching certificate details for ID: {instrument_id}")
145
+
146
+ try:
147
+ async with AvanzaClient() as client:
148
+ service = MarketDataService(client)
149
+ details = await service.get_certificate_details(instrument_id)
150
+
151
+ ctx.info("Retrieved certificate details")
152
+ return details.model_dump(by_alias=True, exclude_none=True)
153
+
154
+ except Exception as e:
155
+ ctx.error(f"Failed to fetch certificate details: {str(e)}")
156
+ raise
@@ -0,0 +1,151 @@
1
+ """ETF MCP tools."""
2
+
3
+ from fastmcp import Context
4
+
5
+ from .. import mcp
6
+ from ..client import AvanzaClient
7
+ from ..models.etf import ETFFilter, ETFFilterRequest
8
+ from ..models.filter import SortBy
9
+ from ..services import MarketDataService
10
+
11
+
12
+ @mcp.tool()
13
+ async def filter_etfs(
14
+ ctx: Context,
15
+ offset: int = 0,
16
+ limit: int = 20,
17
+ asset_categories: list[str] | None = None,
18
+ sub_categories: list[str] | None = None,
19
+ exposures: list[str] | None = None,
20
+ risk_scores: list[str] | None = None,
21
+ directions: list[str] | None = None,
22
+ issuers: list[str] | None = None,
23
+ currency_codes: list[str] | None = None,
24
+ sort_field: str = "numberOfOwners",
25
+ sort_order: str = "desc",
26
+ ) -> dict:
27
+ """Filter and list ETFs with optional filtering and pagination.
28
+
29
+ Search through exchange-traded funds with filters for asset category,
30
+ geographic exposure, issuer, risk score, and more.
31
+
32
+ Args:
33
+ ctx: MCP context for logging
34
+ offset: Number of results to skip (default: 0)
35
+ limit: Maximum number of results (default: 20, max: 100)
36
+ asset_categories: Filter by category, e.g., ["stock"], ["commodity"]
37
+ sub_categories: Filter by sub-category, e.g., ["teknologi"], ["fastigheter"]
38
+ exposures: Filter by geographic exposure, e.g., ["usa"], ["global"]
39
+ risk_scores: Filter by risk score levels
40
+ directions: Filter by direction (for leveraged ETFs)
41
+ issuers: Filter by issuer, e.g., ["iShares"], ["Vanguard"]
42
+ currency_codes: Filter by currency, e.g., ["SEK"], ["USD"]
43
+ sort_field: Field to sort by (default: "numberOfOwners")
44
+ sort_order: Sort order "asc" or "desc" (default: "desc")
45
+
46
+ Returns:
47
+ Filtered list of ETFs with fees, yield, number of owners, etc.
48
+
49
+ Examples:
50
+ List most popular ETFs:
51
+ >>> filter_etfs(sort_field="numberOfOwners", sort_order="desc")
52
+
53
+ Find US technology ETFs:
54
+ >>> filter_etfs(exposures=["usa"], sub_categories=["teknologi"])
55
+
56
+ Get affordable ETFs with low fees:
57
+ >>> filter_etfs(sort_field="managementFee", sort_order="asc")
58
+ """
59
+ ctx.info(f"Filtering ETFs: offset={offset}, limit={limit}")
60
+
61
+ try:
62
+ filter_req = ETFFilterRequest(
63
+ filter=ETFFilter(
64
+ assetCategories=asset_categories or [],
65
+ subCategories=sub_categories or [],
66
+ exposures=exposures or [],
67
+ riskScores=risk_scores or [],
68
+ directions=directions or [],
69
+ issuers=issuers or [],
70
+ currencyCodes=currency_codes or [],
71
+ ),
72
+ offset=offset,
73
+ limit=min(limit, 100),
74
+ sortBy=SortBy(field=sort_field, order=sort_order),
75
+ )
76
+
77
+ async with AvanzaClient() as client:
78
+ service = MarketDataService(client)
79
+ result = await service.filter_etfs(filter_req)
80
+
81
+ ctx.info(f"Retrieved {len(result.etfs)} ETFs")
82
+ return result.model_dump(by_alias=True, exclude_none=True)
83
+
84
+ except Exception as e:
85
+ ctx.error(f"Failed to filter ETFs: {str(e)}")
86
+ raise
87
+
88
+
89
+ @mcp.tool()
90
+ async def get_etf_info(ctx: Context, instrument_id: str) -> dict:
91
+ """Get detailed information about a specific ETF.
92
+
93
+ Provides comprehensive ETF data including current NAV, dividend yield,
94
+ management fees, risk score, and historical performance.
95
+
96
+ Args:
97
+ ctx: MCP context for logging
98
+ instrument_id: Avanza ETF ID
99
+
100
+ Returns:
101
+ Detailed ETF information with fees, yield, performance, etc.
102
+
103
+ Examples:
104
+ Get ETF info:
105
+ >>> get_etf_info(instrument_id="742236")
106
+ """
107
+ ctx.info(f"Fetching ETF info for ID: {instrument_id}")
108
+
109
+ try:
110
+ async with AvanzaClient() as client:
111
+ service = MarketDataService(client)
112
+ etf = await service.get_etf_info(instrument_id)
113
+
114
+ ctx.info(f"Retrieved info for: {etf.name}")
115
+ return etf.model_dump(by_alias=True, exclude_none=True)
116
+
117
+ except Exception as e:
118
+ ctx.error(f"Failed to fetch ETF info: {str(e)}")
119
+ raise
120
+
121
+
122
+ @mcp.tool()
123
+ async def get_etf_details(ctx: Context, instrument_id: str) -> dict:
124
+ """Get extended details about a specific ETF.
125
+
126
+ Provides additional detailed information beyond basic ETF info.
127
+
128
+ Args:
129
+ ctx: MCP context for logging
130
+ instrument_id: Avanza ETF ID
131
+
132
+ Returns:
133
+ Extended ETF details
134
+
135
+ Examples:
136
+ Get detailed info:
137
+ >>> get_etf_details(instrument_id="742236")
138
+ """
139
+ ctx.info(f"Fetching ETF details for ID: {instrument_id}")
140
+
141
+ try:
142
+ async with AvanzaClient() as client:
143
+ service = MarketDataService(client)
144
+ details = await service.get_etf_details(instrument_id)
145
+
146
+ ctx.info("Retrieved ETF details")
147
+ return details.model_dump(by_alias=True, exclude_none=True)
148
+
149
+ except Exception as e:
150
+ ctx.error(f"Failed to fetch ETF details: {str(e)}")
151
+ raise