1+ """
2+ API integration module for spread creation.
3+
4+ This module provides functions for ticker validation and price fetching
5+ using the existing Tinkoff integration and Django API endpoints.
6+ """
7+
8+ import aiohttp
9+ from typing import Dict , List , Optional
10+ from settings import ENDPOINT_HOST , ENDPOINTS , RETRY_SETTINGS , TCS_RO_TOKEN
11+ from tinkoff .invest .retrying .aio .client import AsyncRetryingClient
12+ from tinkoff .invest .utils import quotation_to_decimal
13+
14+
15+ class APIError (Exception ):
16+ """Base exception for API-related errors."""
17+ pass
18+
19+
20+ class TickerNotFoundError (APIError ):
21+ """Raised when a ticker is not found in the database."""
22+ pass
23+
24+
25+ class PriceUnavailableError (APIError ):
26+ """Raised when price data is unavailable."""
27+ pass
28+
29+
30+ async def validate_ticker (ticker : str ) -> Dict :
31+ """
32+ Validate ticker existence and API trading availability.
33+
34+ Args:
35+ ticker: The ticker symbol to validate
36+
37+ Returns:
38+ Dict containing ticker information including figi, lot size, etc.
39+
40+ Raises:
41+ TickerNotFoundError: If ticker doesn't exist
42+ APIError: If API trading is not available or connection fails
43+ """
44+ url = ENDPOINT_HOST + ENDPOINTS ['ticker' ] + ticker + '/'
45+
46+ try :
47+ async with aiohttp .ClientSession () as session :
48+ async with session .get (url ) as response :
49+ if response .status == 404 :
50+ raise TickerNotFoundError (f"Ticker { ticker } not found" )
51+ if response .status != 200 :
52+ raise APIError (f"Server error: { response .status } " )
53+
54+ data = await response .json ()
55+
56+ # Check if API trading is available
57+ if not data .get ('api_trading_available' , False ):
58+ raise APIError (f"API trading not available for { ticker } " )
59+
60+ return data
61+
62+ except aiohttp .ClientError as e :
63+ raise APIError (f"Unable to connect to trading system: { str (e )} " )
64+
65+
66+ async def get_current_prices (
67+ figis : List [str ],
68+ include_order_book : bool = False ) -> Dict [str , Dict ]:
69+ """
70+ Fetch current market prices for given FIGIs.
71+
72+ Args:
73+ figis: List of FIGI identifiers
74+ include_order_book: Whether to include bid/ask prices from order book
75+
76+ Returns:
77+ Dict mapping FIGI to price data containing:
78+ - last_price: Latest trade price
79+ - bid_price: Best bid price (if include_order_book=True)
80+ - ask_price: Best ask price (if include_order_book=True)
81+
82+ Raises:
83+ PriceUnavailableError: If no price data is available
84+ APIError: If connection fails
85+ """
86+ try :
87+ async with AsyncRetryingClient (TCS_RO_TOKEN , RETRY_SETTINGS ) as client :
88+ # Get last prices
89+ response = await client .market_data .get_last_prices (figi = figis )
90+
91+ if not response .last_prices :
92+ raise PriceUnavailableError ("No price data available" )
93+
94+ # Build price data dictionary
95+ price_data = {}
96+ for price_item in response .last_prices :
97+ figi = price_item .figi
98+ price_data [figi ] = {
99+ 'figi' : figi ,
100+ 'last_price' : quotation_to_decimal (price_item .price )
101+ }
102+
103+ # Optionally fetch order book data for bid/ask prices
104+ if include_order_book :
105+ await _add_order_book_data (client , figis , price_data )
106+
107+ return price_data
108+
109+ except Exception as e :
110+ if isinstance (e , (PriceUnavailableError , APIError )):
111+ raise
112+ raise APIError (f"Unable to fetch prices: { str (e )} " )
113+
114+
115+ async def _add_order_book_data (client , figis : List [str ], price_data : Dict ):
116+ """Helper function to add order book data to price data."""
117+ for figi in figis :
118+ if figi in price_data :
119+ try :
120+ order_book = await client .market_data .get_order_book (
121+ figi = figi , depth = 1
122+ )
123+
124+ if order_book .bids :
125+ price_data [figi ]['bid_price' ] = quotation_to_decimal (
126+ order_book .bids [0 ].price
127+ )
128+
129+ if order_book .asks :
130+ price_data [figi ]['ask_price' ] = quotation_to_decimal (
131+ order_book .asks [0 ].price
132+ )
133+
134+ except Exception :
135+ # If order book fails, continue without bid/ask data
136+ pass
137+
138+
139+ async def create_spread (spread_data : Dict ) -> int :
140+ """
141+ Create a new spread in the database.
142+
143+ Args:
144+ spread_data: Dictionary containing spread parameters:
145+ - far_leg_figi: FIGI of the far leg
146+ - near_leg_figi: FIGI of the near leg
147+ - sell: Boolean indicating sell direction
148+ - price: Spread price
149+ - amount: Amount to trade
150+ - editable_ratio: Optional ratio override
151+
152+ Returns:
153+ The ID of the created spread
154+
155+ Raises:
156+ APIError: If creation fails or connection issues occur
157+ """
158+ url = ENDPOINT_HOST + ENDPOINTS ['spreads' ]
159+
160+ # Prepare payload for Django API
161+ payload = {
162+ 'far_leg_figi' : spread_data ['far_leg_figi' ],
163+ 'near_leg_figi' : spread_data ['near_leg_figi' ],
164+ 'sell' : spread_data ['sell' ],
165+ 'price' : spread_data ['price' ],
166+ 'amount' : spread_data ['amount' ]
167+ }
168+
169+ # Add optional ratio if provided
170+ if 'editable_ratio' in spread_data and spread_data ['editable_ratio' ]:
171+ payload ['editable_ratio' ] = spread_data ['editable_ratio' ]
172+
173+ try :
174+ async with aiohttp .ClientSession () as session :
175+ async with session .post (url , json = payload ) as response :
176+ if response .status == 201 :
177+ data = await response .json ()
178+ return data ['id' ]
179+ error_text = await response .text ()
180+ raise APIError (f"Error creating spread: { error_text } " )
181+
182+ except aiohttp .ClientError as e :
183+ raise APIError (f"Unable to connect to trading system: { str (e )} " )
184+
185+
186+ async def check_duplicate_spread (
187+ far_leg_figi : str ,
188+ near_leg_figi : str ) -> Optional [int ]:
189+ """
190+ Check if a spread with the same legs already exists.
191+
192+ Args:
193+ far_leg_figi: FIGI of the far leg
194+ near_leg_figi: FIGI of the near leg
195+
196+ Returns:
197+ The ID of the existing spread if found, None otherwise
198+
199+ Raises:
200+ APIError: If connection fails or server error occurs
201+ """
202+ url = ENDPOINT_HOST + ENDPOINTS ['spreads' ]
203+
204+ try :
205+ async with aiohttp .ClientSession () as session :
206+ async with session .get (url ) as response :
207+ if response .status != 200 :
208+ raise APIError (f"Server error: { response .status } " )
209+
210+ spreads = await response .json ()
211+
212+ # Check for duplicate spread with same legs
213+ for spread in spreads :
214+ far_match = spread ['far_leg' ]['figi' ] == far_leg_figi
215+ near_match = spread ['near_leg' ]['figi' ] == near_leg_figi
216+ if far_match and near_match :
217+ return spread ['id' ]
218+
219+ return None
220+
221+ except aiohttp .ClientError as e :
222+ raise APIError (f"Unable to connect to trading system: { str (e )} " )
0 commit comments