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.
- avanza_mcp/__init__.py +29 -0
- avanza_mcp/client/__init__.py +25 -0
- avanza_mcp/client/base.py +375 -0
- avanza_mcp/client/endpoints.py +41 -0
- avanza_mcp/client/exceptions.py +66 -0
- avanza_mcp/models/__init__.py +41 -0
- avanza_mcp/models/common.py +50 -0
- avanza_mcp/models/fund.py +246 -0
- avanza_mcp/models/search.py +123 -0
- avanza_mcp/models/stock.py +268 -0
- avanza_mcp/prompts/__init__.py +6 -0
- avanza_mcp/prompts/analysis.py +116 -0
- avanza_mcp/resources/__init__.py +6 -0
- avanza_mcp/resources/instruments.py +127 -0
- avanza_mcp/services/__init__.py +6 -0
- avanza_mcp/services/market_data_service.py +298 -0
- avanza_mcp/services/search_service.py +64 -0
- avanza_mcp/tools/__init__.py +8 -0
- avanza_mcp/tools/funds.py +267 -0
- avanza_mcp/tools/market_data.py +508 -0
- avanza_mcp/tools/search.py +119 -0
- avanza_mcp-1.0.0.dist-info/METADATA +99 -0
- avanza_mcp-1.0.0.dist-info/RECORD +26 -0
- avanza_mcp-1.0.0.dist-info/WHEEL +4 -0
- avanza_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- avanza_mcp-1.0.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -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,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,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)
|