finatic-server-python 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/PKG-INFO +1 -1
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/pyproject.toml +1 -1
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/__init__.py +2 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/api_client.py +111 -1
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/client.py +315 -4
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/__init__.py +4 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/broker.py +39 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/utils/__init__.py +10 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/utils/errors.py +9 -1
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/PKG-INFO +1 -1
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/README.md +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/setup.cfg +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/__init__.py +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/auth.py +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/common.py +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/orders.py +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/portfolio.py +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/SOURCES.txt +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/dependency_links.txt +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/requires.txt +0 -0
- {finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "finatic-server-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Python SDK for Finatic Server API"
|
|
9
9
|
authors = [{ name = "Finatic", email = "support@finatic.dev" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -65,6 +65,7 @@ from .utils.errors import (
|
|
|
65
65
|
OrderError,
|
|
66
66
|
OrderValidationError,
|
|
67
67
|
CompanyAccessError,
|
|
68
|
+
TradingNotEnabledError,
|
|
68
69
|
)
|
|
69
70
|
|
|
70
71
|
__version__ = "0.1.0"
|
|
@@ -130,4 +131,5 @@ __all__ = [
|
|
|
130
131
|
"OrderError",
|
|
131
132
|
"OrderValidationError",
|
|
132
133
|
"CompanyAccessError",
|
|
134
|
+
"TradingNotEnabledError",
|
|
133
135
|
]
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/api_client.py
RENAMED
|
@@ -23,11 +23,13 @@ from ..types import (
|
|
|
23
23
|
BrokerAccount,
|
|
24
24
|
BrokerOrder,
|
|
25
25
|
BrokerPosition,
|
|
26
|
+
BrokerBalance,
|
|
26
27
|
BrokerConnection,
|
|
27
28
|
BrokerDataOptions,
|
|
28
29
|
OrdersFilter,
|
|
29
30
|
PositionsFilter,
|
|
30
31
|
AccountsFilter,
|
|
32
|
+
BalancesFilter,
|
|
31
33
|
OrderResponse,
|
|
32
34
|
BrokerOrderParams,
|
|
33
35
|
BrokerExtras,
|
|
@@ -48,6 +50,7 @@ from ..utils.errors import (
|
|
|
48
50
|
OrderError,
|
|
49
51
|
OrderValidationError,
|
|
50
52
|
CompanyAccessError,
|
|
53
|
+
TradingNotEnabledError,
|
|
51
54
|
)
|
|
52
55
|
|
|
53
56
|
T = TypeVar('T')
|
|
@@ -268,7 +271,14 @@ class ApiClient:
|
|
|
268
271
|
if status == 401:
|
|
269
272
|
raise AuthenticationError(message)
|
|
270
273
|
elif status == 403:
|
|
271
|
-
|
|
274
|
+
# Check for specific 403 error codes
|
|
275
|
+
error_code = error_data.get("code") or error_data.get("detail", {}).get("code")
|
|
276
|
+
if error_code == "TRADING_NOT_ENABLED":
|
|
277
|
+
raise TradingNotEnabledError(message, error_data)
|
|
278
|
+
elif error_code == "NO_COMPANY_ACCESS":
|
|
279
|
+
raise CompanyAccessError(message, error_data)
|
|
280
|
+
else:
|
|
281
|
+
raise AuthorizationError(message)
|
|
272
282
|
elif status == 422:
|
|
273
283
|
raise ValidationError(message)
|
|
274
284
|
elif status == 429:
|
|
@@ -752,6 +762,68 @@ class ApiClient:
|
|
|
752
762
|
navigation_callback
|
|
753
763
|
)
|
|
754
764
|
|
|
765
|
+
async def get_broker_balances(self, page: int = 1, per_page: int = 100, options: Optional[BrokerDataOptions] = None, filters: Optional[BalancesFilter] = None) -> PaginatedResult:
|
|
766
|
+
"""Get broker balances with pagination support."""
|
|
767
|
+
access_token = await self.get_valid_access_token()
|
|
768
|
+
offset = (page - 1) * per_page
|
|
769
|
+
|
|
770
|
+
# Build query parameters
|
|
771
|
+
params = {
|
|
772
|
+
'limit': str(per_page),
|
|
773
|
+
'offset': str(offset),
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
# Add options
|
|
777
|
+
if options:
|
|
778
|
+
if options.broker_name:
|
|
779
|
+
params['broker_id'] = options.broker_name
|
|
780
|
+
if options.account_id:
|
|
781
|
+
params['account_id'] = options.account_id
|
|
782
|
+
if options.symbol:
|
|
783
|
+
params['symbol'] = options.symbol
|
|
784
|
+
|
|
785
|
+
# Add filters
|
|
786
|
+
if filters:
|
|
787
|
+
if filters.broker_id:
|
|
788
|
+
params['broker_id'] = filters.broker_id
|
|
789
|
+
if filters.connection_id:
|
|
790
|
+
params['connection_id'] = filters.connection_id
|
|
791
|
+
if filters.account_id:
|
|
792
|
+
params['account_id'] = filters.account_id
|
|
793
|
+
if filters.is_end_of_day_snapshot is not None:
|
|
794
|
+
params['is_end_of_day_snapshot'] = str(filters.is_end_of_day_snapshot).lower()
|
|
795
|
+
if filters.balance_created_after:
|
|
796
|
+
params['balance_created_after'] = filters.balance_created_after
|
|
797
|
+
if filters.balance_created_before:
|
|
798
|
+
params['balance_created_before'] = filters.balance_created_before
|
|
799
|
+
if filters.with_metadata is not None:
|
|
800
|
+
params['with_metadata'] = str(filters.with_metadata).lower()
|
|
801
|
+
|
|
802
|
+
# Make the API request
|
|
803
|
+
response = await self._make_request(
|
|
804
|
+
'GET',
|
|
805
|
+
f'{self.base_url}/api/v1/brokers/data/balances',
|
|
806
|
+
headers={'Authorization': f'Bearer {access_token}'},
|
|
807
|
+
params=params
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Create pagination info
|
|
811
|
+
pagination_info = ApiPaginationInfo(
|
|
812
|
+
has_more=response.get('pagination', {}).get('has_more', False),
|
|
813
|
+
next_offset=response.get('pagination', {}).get('next_offset', offset),
|
|
814
|
+
current_offset=response.get('pagination', {}).get('current_offset', offset),
|
|
815
|
+
limit=response.get('pagination', {}).get('limit', per_page),
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Create navigation callback
|
|
819
|
+
navigation_callback = self._create_navigation_callback('brokers/data/balances', access_token, params)
|
|
820
|
+
|
|
821
|
+
return PaginatedResult(
|
|
822
|
+
[BrokerBalance(**balance) for balance in response.get('response_data', [])],
|
|
823
|
+
pagination_info,
|
|
824
|
+
navigation_callback
|
|
825
|
+
)
|
|
826
|
+
|
|
755
827
|
# Helper methods to get all data across pages
|
|
756
828
|
async def get_all_broker_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
757
829
|
"""Get all broker accounts across all pages."""
|
|
@@ -803,6 +875,23 @@ class ApiClient:
|
|
|
803
875
|
page += 1
|
|
804
876
|
|
|
805
877
|
return all_positions
|
|
878
|
+
|
|
879
|
+
async def get_all_broker_balances(self, options: Optional[BrokerDataOptions] = None, filters: Optional[BalancesFilter] = None) -> List[BrokerBalance]:
|
|
880
|
+
"""Get all broker balances across all pages."""
|
|
881
|
+
all_balances = []
|
|
882
|
+
page = 1
|
|
883
|
+
per_page = 100
|
|
884
|
+
|
|
885
|
+
while True:
|
|
886
|
+
result = await self.get_broker_balances(page, per_page, options, filters)
|
|
887
|
+
if not result.data:
|
|
888
|
+
break
|
|
889
|
+
all_balances.extend(result.data)
|
|
890
|
+
if not result.has_next:
|
|
891
|
+
break
|
|
892
|
+
page += 1
|
|
893
|
+
|
|
894
|
+
return all_balances
|
|
806
895
|
|
|
807
896
|
async def get_broker_connections_auto(self) -> List[BrokerConnection]:
|
|
808
897
|
"""Get broker connections using stored access token."""
|
|
@@ -813,6 +902,27 @@ class ApiClient:
|
|
|
813
902
|
access_token=access_token
|
|
814
903
|
)
|
|
815
904
|
return [BrokerConnection(**connection) for connection in response.get('response_data', [])]
|
|
905
|
+
|
|
906
|
+
async def get_balances(self, options: Optional[BrokerDataOptions] = None) -> List[Dict[str, Any]]:
|
|
907
|
+
"""Get account balances."""
|
|
908
|
+
access_token = await self.get_valid_access_token()
|
|
909
|
+
response = await self._request(
|
|
910
|
+
method='GET',
|
|
911
|
+
path='/brokers/data/balances',
|
|
912
|
+
params=options or {},
|
|
913
|
+
access_token=access_token
|
|
914
|
+
)
|
|
915
|
+
return response.get('response_data', [])
|
|
916
|
+
|
|
917
|
+
async def disconnect_company(self, connection_id: str) -> Dict[str, Any]:
|
|
918
|
+
"""Disconnect a company from a broker connection."""
|
|
919
|
+
access_token = await self.get_valid_access_token()
|
|
920
|
+
response = await self._request(
|
|
921
|
+
method='DELETE',
|
|
922
|
+
path=f'/brokers/connections/{connection_id}',
|
|
923
|
+
access_token=access_token
|
|
924
|
+
)
|
|
925
|
+
return response
|
|
816
926
|
|
|
817
927
|
# Trading context methods
|
|
818
928
|
def set_broker(self, broker: str):
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/client.py
RENAMED
|
@@ -21,11 +21,13 @@ from ..types import (
|
|
|
21
21
|
BrokerAccount,
|
|
22
22
|
BrokerOrder,
|
|
23
23
|
BrokerPosition,
|
|
24
|
+
BrokerBalance,
|
|
24
25
|
BrokerConnection,
|
|
25
26
|
BrokerDataOptions,
|
|
26
27
|
OrdersFilter,
|
|
27
28
|
PositionsFilter,
|
|
28
29
|
AccountsFilter,
|
|
30
|
+
BalancesFilter,
|
|
29
31
|
OrderResponse,
|
|
30
32
|
BrokerOrderParams,
|
|
31
33
|
BrokerExtras,
|
|
@@ -176,6 +178,23 @@ class FinaticServerClient:
|
|
|
176
178
|
)
|
|
177
179
|
|
|
178
180
|
return session_response
|
|
181
|
+
|
|
182
|
+
async def set_user_id(self, user_id: str) -> None:
|
|
183
|
+
"""Set the user ID for the current session.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
user_id: The user ID to set for the session
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
AuthenticationError: If session is not initialized
|
|
190
|
+
"""
|
|
191
|
+
if not self._session_id:
|
|
192
|
+
raise AuthenticationError("Session not initialized. Please start a session first.")
|
|
193
|
+
|
|
194
|
+
self._user_id = user_id
|
|
195
|
+
# Update the API client with the new user ID if needed
|
|
196
|
+
if hasattr(self._api_client, 'set_user_id'):
|
|
197
|
+
await self._api_client.set_user_id(user_id)
|
|
179
198
|
|
|
180
199
|
async def request_otp(self, email: str) -> OtpRequestResponse:
|
|
181
200
|
"""Request OTP for session authentication.
|
|
@@ -294,11 +313,16 @@ class FinaticServerClient:
|
|
|
294
313
|
|
|
295
314
|
return auth_response
|
|
296
315
|
|
|
297
|
-
async def get_portal_url(self) -> str:
|
|
298
|
-
"""Get the portal URL for user authentication.
|
|
316
|
+
async def get_portal_url(self, theme: Optional[Dict[str, Any]] = None, brokers: Optional[List[str]] = None, email: Optional[str] = None) -> str:
|
|
317
|
+
"""Get the portal URL for user authentication with optional theming and configuration.
|
|
299
318
|
|
|
319
|
+
Args:
|
|
320
|
+
theme: Optional theme configuration (preset or custom)
|
|
321
|
+
brokers: Optional list of broker names to filter by
|
|
322
|
+
email: Optional email to pre-fill in the portal
|
|
323
|
+
|
|
300
324
|
Returns:
|
|
301
|
-
Portal URL string
|
|
325
|
+
Portal URL string with applied configuration
|
|
302
326
|
|
|
303
327
|
Raises:
|
|
304
328
|
AuthenticationError: If session is not initialized
|
|
@@ -308,9 +332,78 @@ class FinaticServerClient:
|
|
|
308
332
|
|
|
309
333
|
try:
|
|
310
334
|
response = await self._api_client.get_portal_url(self._session_id)
|
|
311
|
-
|
|
335
|
+
portal_url = response.data['portal_url']
|
|
336
|
+
|
|
337
|
+
# Use stored configuration as defaults if not provided
|
|
338
|
+
final_theme = theme or getattr(self, '_portal_theme', None)
|
|
339
|
+
final_brokers = brokers or getattr(self, '_portal_brokers', None)
|
|
340
|
+
final_email = email or getattr(self, '_portal_email', None)
|
|
341
|
+
|
|
342
|
+
# Apply theming and configuration to the URL
|
|
343
|
+
portal_url = self._apply_portal_config(portal_url, final_theme, final_brokers, final_email)
|
|
344
|
+
|
|
345
|
+
return portal_url
|
|
312
346
|
except Exception as e:
|
|
313
347
|
raise AuthenticationError(f"Failed to get portal URL: {str(e)}")
|
|
348
|
+
|
|
349
|
+
def _apply_portal_config(self, base_url: str, theme: Optional[Dict[str, Any]] = None, brokers: Optional[List[str]] = None, email: Optional[str] = None) -> str:
|
|
350
|
+
"""Apply theming and configuration to a portal URL."""
|
|
351
|
+
try:
|
|
352
|
+
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
|
353
|
+
import base64
|
|
354
|
+
import json
|
|
355
|
+
|
|
356
|
+
parsed = urlparse(base_url)
|
|
357
|
+
query_params = parse_qs(parsed.query)
|
|
358
|
+
|
|
359
|
+
# Apply theme configuration
|
|
360
|
+
if theme:
|
|
361
|
+
if theme.get('preset'):
|
|
362
|
+
query_params['theme'] = [theme['preset']]
|
|
363
|
+
elif theme.get('custom'):
|
|
364
|
+
# Encode custom theme as base64 JSON
|
|
365
|
+
theme_json = json.dumps(theme['custom'])
|
|
366
|
+
theme_b64 = base64.b64encode(theme_json.encode()).decode()
|
|
367
|
+
query_params['theme'] = ['custom']
|
|
368
|
+
query_params['themeObject'] = [theme_b64]
|
|
369
|
+
|
|
370
|
+
# Apply broker filtering
|
|
371
|
+
if brokers:
|
|
372
|
+
# Convert broker names to IDs and encode
|
|
373
|
+
supported_brokers = {
|
|
374
|
+
'alpaca': 'alpaca',
|
|
375
|
+
'robinhood': 'robinhood',
|
|
376
|
+
'tasty_trade': 'tasty_trade',
|
|
377
|
+
'ninja_trader': 'ninja_trader',
|
|
378
|
+
'tradovate': 'ninja_trader', # Alias
|
|
379
|
+
'interactive_brokers': 'interactive_brokers',
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
broker_ids = []
|
|
383
|
+
for broker in brokers:
|
|
384
|
+
broker_id = supported_brokers.get(broker.lower())
|
|
385
|
+
if broker_id:
|
|
386
|
+
broker_ids.append(broker_id)
|
|
387
|
+
|
|
388
|
+
if broker_ids:
|
|
389
|
+
brokers_json = json.dumps(broker_ids)
|
|
390
|
+
brokers_b64 = base64.b64encode(brokers_json.encode()).decode()
|
|
391
|
+
query_params['brokers'] = [brokers_b64]
|
|
392
|
+
|
|
393
|
+
# Apply email parameter
|
|
394
|
+
if email:
|
|
395
|
+
query_params['email'] = [email]
|
|
396
|
+
|
|
397
|
+
# Rebuild URL with new query parameters
|
|
398
|
+
new_query = urlencode(query_params, doseq=True)
|
|
399
|
+
new_parsed = parsed._replace(query=new_query)
|
|
400
|
+
|
|
401
|
+
return urlunparse(new_parsed)
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
# If URL manipulation fails, return original URL
|
|
405
|
+
print(f"Warning: Failed to apply portal configuration: {e}")
|
|
406
|
+
return base_url
|
|
314
407
|
|
|
315
408
|
async def get_session_user(self) -> Dict[str, Any]:
|
|
316
409
|
"""Get the user and tokens for a completed session.
|
|
@@ -539,9 +632,20 @@ class FinaticServerClient:
|
|
|
539
632
|
"""Get broker positions with pagination support."""
|
|
540
633
|
return await self._api_client.get_broker_positions(page, per_page, options, filters)
|
|
541
634
|
|
|
635
|
+
async def get_broker_balances(self, page: int = 1, per_page: int = 100, options: Optional[BrokerDataOptions] = None, filters: Optional[BalancesFilter] = None) -> PaginatedResult:
|
|
636
|
+
"""Get broker balances with pagination support."""
|
|
637
|
+
return await self._api_client.get_broker_balances(page, per_page, options, filters)
|
|
638
|
+
|
|
542
639
|
async def get_broker_connections(self) -> List[BrokerConnection]:
|
|
543
640
|
"""Get broker connections using stored access token."""
|
|
544
641
|
return await self._api_client.get_broker_connections_auto()
|
|
642
|
+
|
|
643
|
+
async def get_balances(self, options: Optional[BrokerDataOptions] = None) -> List[Dict[str, Any]]:
|
|
644
|
+
"""Get account balances for the authenticated user."""
|
|
645
|
+
if not self.is_authenticated():
|
|
646
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
647
|
+
|
|
648
|
+
return await self._api_client.get_balances(options)
|
|
545
649
|
|
|
546
650
|
# Helper methods to get all data across pages
|
|
547
651
|
async def get_all_broker_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
@@ -556,6 +660,170 @@ class FinaticServerClient:
|
|
|
556
660
|
"""Get all broker positions across all pages."""
|
|
557
661
|
return await self._api_client.get_all_broker_positions(options, filters)
|
|
558
662
|
|
|
663
|
+
async def get_all_orders(self, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
664
|
+
"""Get all orders across all pages (convenience method)."""
|
|
665
|
+
return await self._api_client.get_all_broker_orders(options, filters)
|
|
666
|
+
|
|
667
|
+
async def get_all_positions(self, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> List[BrokerPosition]:
|
|
668
|
+
"""Get all positions across all pages (convenience method)."""
|
|
669
|
+
return await self._api_client.get_all_broker_positions(options, filters)
|
|
670
|
+
|
|
671
|
+
async def get_all_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
672
|
+
"""Get all accounts across all pages (convenience method)."""
|
|
673
|
+
return await self._api_client.get_all_broker_accounts(options, filters)
|
|
674
|
+
|
|
675
|
+
async def disconnect_company(self, connection_id: str) -> Dict[str, Any]:
|
|
676
|
+
"""Disconnect a company from a broker connection.
|
|
677
|
+
|
|
678
|
+
Args:
|
|
679
|
+
connection_id: The connection ID to disconnect
|
|
680
|
+
|
|
681
|
+
Returns:
|
|
682
|
+
Disconnect response data
|
|
683
|
+
|
|
684
|
+
Raises:
|
|
685
|
+
AuthenticationError: If not authenticated
|
|
686
|
+
"""
|
|
687
|
+
if not self.is_authenticated():
|
|
688
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
689
|
+
|
|
690
|
+
return await self._api_client.disconnect_company(connection_id)
|
|
691
|
+
|
|
692
|
+
# Convenience filtering methods
|
|
693
|
+
async def get_open_positions(self, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> List[BrokerPosition]:
|
|
694
|
+
"""Get only open positions."""
|
|
695
|
+
if not self.is_authenticated():
|
|
696
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
697
|
+
|
|
698
|
+
open_filters = {**(filters or {}), 'position_status': 'open'}
|
|
699
|
+
result = await self.get_broker_positions(options=options, filters=open_filters)
|
|
700
|
+
return result.data or []
|
|
701
|
+
|
|
702
|
+
async def get_filled_orders(self, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
703
|
+
"""Get only filled orders."""
|
|
704
|
+
if not self.is_authenticated():
|
|
705
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
706
|
+
|
|
707
|
+
filled_filters = {**(filters or {}), 'status': 'filled'}
|
|
708
|
+
result = await self.get_broker_orders(options=options, filters=filled_filters)
|
|
709
|
+
return result.data or []
|
|
710
|
+
|
|
711
|
+
async def get_pending_orders(self, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
712
|
+
"""Get only pending orders."""
|
|
713
|
+
if not self.is_authenticated():
|
|
714
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
715
|
+
|
|
716
|
+
pending_filters = {**(filters or {}), 'status': 'pending'}
|
|
717
|
+
result = await self.get_broker_orders(options=options, filters=pending_filters)
|
|
718
|
+
return result.data or []
|
|
719
|
+
|
|
720
|
+
async def get_active_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
721
|
+
"""Get only active accounts."""
|
|
722
|
+
if not self.is_authenticated():
|
|
723
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
724
|
+
|
|
725
|
+
active_filters = {**(filters or {}), 'status': 'active'}
|
|
726
|
+
return await self.get_broker_accounts(options=options, filters=active_filters)
|
|
727
|
+
|
|
728
|
+
async def get_orders_by_symbol(self, symbol: str, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
729
|
+
"""Get orders filtered by symbol."""
|
|
730
|
+
if not self.is_authenticated():
|
|
731
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
732
|
+
|
|
733
|
+
symbol_filters = {**(filters or {}), 'symbol': symbol}
|
|
734
|
+
result = await self.get_broker_orders(options=options, filters=symbol_filters)
|
|
735
|
+
return result.data or []
|
|
736
|
+
|
|
737
|
+
async def get_positions_by_symbol(self, symbol: str, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> List[BrokerPosition]:
|
|
738
|
+
"""Get positions filtered by symbol."""
|
|
739
|
+
if not self.is_authenticated():
|
|
740
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
741
|
+
|
|
742
|
+
symbol_filters = {**(filters or {}), 'symbol': symbol}
|
|
743
|
+
result = await self.get_broker_positions(options=options, filters=symbol_filters)
|
|
744
|
+
return result.data or []
|
|
745
|
+
|
|
746
|
+
async def get_orders_by_broker(self, broker_id: str, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> List[BrokerOrder]:
|
|
747
|
+
"""Get orders filtered by broker."""
|
|
748
|
+
if not self.is_authenticated():
|
|
749
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
750
|
+
|
|
751
|
+
broker_filters = {**(filters or {}), 'broker_id': broker_id}
|
|
752
|
+
result = await self.get_broker_orders(options=options, filters=broker_filters)
|
|
753
|
+
return result.data or []
|
|
754
|
+
|
|
755
|
+
async def get_positions_by_broker(self, broker_id: str, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> List[BrokerPosition]:
|
|
756
|
+
"""Get positions filtered by broker."""
|
|
757
|
+
if not self.is_authenticated():
|
|
758
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
759
|
+
|
|
760
|
+
broker_filters = {**(filters or {}), 'broker_id': broker_id}
|
|
761
|
+
result = await self.get_broker_positions(options=options, filters=broker_filters)
|
|
762
|
+
return result.data or []
|
|
763
|
+
|
|
764
|
+
# Pagination helper methods
|
|
765
|
+
async def get_orders_page(self, page: int, per_page: int, options: Optional[BrokerDataOptions] = None, filters: Optional[OrdersFilter] = None) -> PaginatedResult:
|
|
766
|
+
"""Get a specific page of orders."""
|
|
767
|
+
if not self.is_authenticated():
|
|
768
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
769
|
+
|
|
770
|
+
return await self.get_broker_orders(page=page, per_page=per_page, options=options, filters=filters)
|
|
771
|
+
|
|
772
|
+
async def get_positions_page(self, page: int, per_page: int, options: Optional[BrokerDataOptions] = None, filters: Optional[PositionsFilter] = None) -> PaginatedResult:
|
|
773
|
+
"""Get a specific page of positions."""
|
|
774
|
+
if not self.is_authenticated():
|
|
775
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
776
|
+
|
|
777
|
+
return await self.get_broker_positions(page=page, per_page=per_page, options=options, filters=filters)
|
|
778
|
+
|
|
779
|
+
async def get_accounts_page(self, page: int, per_page: int, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
|
|
780
|
+
"""Get a specific page of accounts."""
|
|
781
|
+
if not self.is_authenticated():
|
|
782
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
783
|
+
|
|
784
|
+
return await self.get_broker_accounts(page=page, per_page=per_page, options=options, filters=filters)
|
|
785
|
+
|
|
786
|
+
async def get_next_orders_page(self, current_result: PaginatedResult) -> Optional[PaginatedResult]:
|
|
787
|
+
"""Get the next page of orders."""
|
|
788
|
+
if not self.is_authenticated():
|
|
789
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
790
|
+
|
|
791
|
+
if not current_result.has_next():
|
|
792
|
+
return None
|
|
793
|
+
|
|
794
|
+
# For now, return None as the API doesn't support cursor-based pagination
|
|
795
|
+
# This would need to be implemented based on the actual API pagination structure
|
|
796
|
+
return None
|
|
797
|
+
|
|
798
|
+
async def get_next_positions_page(self, current_result: PaginatedResult) -> Optional[PaginatedResult]:
|
|
799
|
+
"""Get the next page of positions."""
|
|
800
|
+
if not self.is_authenticated():
|
|
801
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
802
|
+
|
|
803
|
+
if not current_result.has_next():
|
|
804
|
+
return None
|
|
805
|
+
|
|
806
|
+
# For now, return None as the API doesn't support cursor-based pagination
|
|
807
|
+
# This would need to be implemented based on the actual API pagination structure
|
|
808
|
+
return None
|
|
809
|
+
|
|
810
|
+
async def get_next_accounts_page(self, current_page: int, per_page: int, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> Optional[List[BrokerAccount]]:
|
|
811
|
+
"""Get the next page of accounts."""
|
|
812
|
+
if not self.is_authenticated():
|
|
813
|
+
raise AuthenticationError("Not authenticated. Please complete authentication first.")
|
|
814
|
+
|
|
815
|
+
next_page = current_page + 1
|
|
816
|
+
accounts = await self.get_broker_accounts(page=next_page, per_page=per_page, options=options, filters=filters)
|
|
817
|
+
|
|
818
|
+
if not accounts:
|
|
819
|
+
return None
|
|
820
|
+
|
|
821
|
+
return accounts
|
|
822
|
+
|
|
823
|
+
async def get_all_broker_balances(self, options: Optional[BrokerDataOptions] = None, filters: Optional[BalancesFilter] = None) -> List[BrokerBalance]:
|
|
824
|
+
"""Get all broker balances across all pages."""
|
|
825
|
+
return await self._api_client.get_all_broker_balances(options, filters)
|
|
826
|
+
|
|
559
827
|
# ============================================================================
|
|
560
828
|
# TRADING METHODS
|
|
561
829
|
# ============================================================================
|
|
@@ -939,6 +1207,49 @@ class FinaticServerClient:
|
|
|
939
1207
|
except Exception as e:
|
|
940
1208
|
print(f"Warning: Error during client close: {e}")
|
|
941
1209
|
|
|
1210
|
+
# Portal configuration convenience methods
|
|
1211
|
+
def set_portal_theme(self, theme: Dict[str, Any]) -> None:
|
|
1212
|
+
"""Set the default portal theme configuration.
|
|
1213
|
+
|
|
1214
|
+
Args:
|
|
1215
|
+
theme: Theme configuration (preset or custom)
|
|
1216
|
+
"""
|
|
1217
|
+
self._portal_theme = theme
|
|
1218
|
+
|
|
1219
|
+
def set_portal_brokers(self, brokers: List[str]) -> None:
|
|
1220
|
+
"""Set the default broker filter for the portal.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
brokers: List of broker names to filter by
|
|
1224
|
+
"""
|
|
1225
|
+
self._portal_brokers = brokers
|
|
1226
|
+
|
|
1227
|
+
def set_portal_email(self, email: str) -> None:
|
|
1228
|
+
"""Set the default email for the portal.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
email: Email to pre-fill in the portal
|
|
1232
|
+
"""
|
|
1233
|
+
self._portal_email = email
|
|
1234
|
+
|
|
1235
|
+
def get_portal_config(self) -> Dict[str, Any]:
|
|
1236
|
+
"""Get the current portal configuration.
|
|
1237
|
+
|
|
1238
|
+
Returns:
|
|
1239
|
+
Dictionary with current portal configuration
|
|
1240
|
+
"""
|
|
1241
|
+
return {
|
|
1242
|
+
'theme': getattr(self, '_portal_theme', None),
|
|
1243
|
+
'brokers': getattr(self, '_portal_brokers', None),
|
|
1244
|
+
'email': getattr(self, '_portal_email', None),
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
def clear_portal_config(self) -> None:
|
|
1248
|
+
"""Clear all portal configuration settings."""
|
|
1249
|
+
self._portal_theme = None
|
|
1250
|
+
self._portal_brokers = None
|
|
1251
|
+
self._portal_email = None
|
|
1252
|
+
|
|
942
1253
|
def __del__(self):
|
|
943
1254
|
"""Destructor to ensure cleanup if context manager is not used."""
|
|
944
1255
|
try:
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/__init__.py
RENAMED
|
@@ -47,12 +47,14 @@ from .broker import (
|
|
|
47
47
|
BrokerAccount,
|
|
48
48
|
BrokerOrder,
|
|
49
49
|
BrokerPosition,
|
|
50
|
+
BrokerBalance,
|
|
50
51
|
BrokerInfo,
|
|
51
52
|
BrokerConnection,
|
|
52
53
|
BrokerDataOptions,
|
|
53
54
|
OrdersFilter,
|
|
54
55
|
PositionsFilter,
|
|
55
56
|
AccountsFilter,
|
|
57
|
+
BalancesFilter,
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
__all__ = [
|
|
@@ -94,10 +96,12 @@ __all__ = [
|
|
|
94
96
|
"BrokerAccount",
|
|
95
97
|
"BrokerOrder",
|
|
96
98
|
"BrokerPosition",
|
|
99
|
+
"BrokerBalance",
|
|
97
100
|
"BrokerInfo",
|
|
98
101
|
"BrokerConnection",
|
|
99
102
|
"BrokerDataOptions",
|
|
100
103
|
"OrdersFilter",
|
|
101
104
|
"PositionsFilter",
|
|
102
105
|
"AccountsFilter",
|
|
106
|
+
"BalancesFilter",
|
|
103
107
|
]
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/broker.py
RENAMED
|
@@ -41,6 +41,12 @@ class BrokerAccount(BaseModel):
|
|
|
41
41
|
created_at: str = Field(..., description="Creation timestamp")
|
|
42
42
|
updated_at: str = Field(..., description="Last update timestamp")
|
|
43
43
|
last_synced_at: str = Field(..., description="Last sync timestamp")
|
|
44
|
+
positions_synced_at: Optional[str] = Field(None, description="When positions were last synced")
|
|
45
|
+
orders_synced_at: Optional[str] = Field(None, description="When orders were last synced")
|
|
46
|
+
balances_synced_at: Optional[str] = Field(None, description="When balances were last synced")
|
|
47
|
+
account_created_at: Optional[str] = Field(None, description="When the account was created")
|
|
48
|
+
account_updated_at: Optional[str] = Field(None, description="When the account was last updated")
|
|
49
|
+
account_first_trade_at: Optional[str] = Field(None, description="When the first trade occurred")
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
class BrokerOrder(BaseModel):
|
|
@@ -84,6 +90,25 @@ class BrokerPosition(BaseModel):
|
|
|
84
90
|
updated_at: str = Field(..., description="Last update timestamp")
|
|
85
91
|
|
|
86
92
|
|
|
93
|
+
class BrokerBalance(BaseModel):
|
|
94
|
+
"""Broker balance information."""
|
|
95
|
+
|
|
96
|
+
id: str = Field(..., description="Balance ID")
|
|
97
|
+
account_id: str = Field(..., description="Account ID")
|
|
98
|
+
total_cash_value: Optional[float] = Field(None, description="Total cash value")
|
|
99
|
+
net_liquidation_value: Optional[float] = Field(None, description="Net liquidation value")
|
|
100
|
+
initial_margin: Optional[float] = Field(None, description="Initial margin")
|
|
101
|
+
maintenance_margin: Optional[float] = Field(None, description="Maintenance margin")
|
|
102
|
+
available_to_withdraw: Optional[float] = Field(None, description="Available to withdraw")
|
|
103
|
+
total_realized_pnl: Optional[float] = Field(None, description="Total realized P&L")
|
|
104
|
+
balance_created_at: Optional[str] = Field(None, description="Balance creation timestamp")
|
|
105
|
+
balance_updated_at: Optional[str] = Field(None, description="Balance update timestamp")
|
|
106
|
+
is_end_of_day_snapshot: Optional[bool] = Field(None, description="Whether this is an end-of-day snapshot")
|
|
107
|
+
raw_payload: Optional[Dict[str, Any]] = Field(None, description="Raw broker payload")
|
|
108
|
+
created_at: str = Field(..., description="Creation timestamp")
|
|
109
|
+
updated_at: str = Field(..., description="Last update timestamp")
|
|
110
|
+
|
|
111
|
+
|
|
87
112
|
class BrokerConnection(BaseModel):
|
|
88
113
|
"""Broker connection information."""
|
|
89
114
|
|
|
@@ -144,4 +169,18 @@ class AccountsFilter(BaseModel):
|
|
|
144
169
|
currency: Optional[str] = Field(None, description="Filter by currency")
|
|
145
170
|
limit: Optional[int] = Field(None, description="Result limit")
|
|
146
171
|
offset: Optional[int] = Field(None, description="Result offset")
|
|
172
|
+
with_metadata: Optional[bool] = Field(None, description="Include metadata")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class BalancesFilter(BaseModel):
|
|
176
|
+
"""Filter options for balances pagination."""
|
|
177
|
+
|
|
178
|
+
broker_id: Optional[str] = Field(None, description="Filter by broker ID")
|
|
179
|
+
connection_id: Optional[str] = Field(None, description="Filter by connection ID")
|
|
180
|
+
account_id: Optional[str] = Field(None, description="Filter by account ID")
|
|
181
|
+
is_end_of_day_snapshot: Optional[bool] = Field(None, description="Filter by end-of-day snapshot status")
|
|
182
|
+
limit: Optional[int] = Field(None, description="Result limit")
|
|
183
|
+
offset: Optional[int] = Field(None, description="Result offset")
|
|
184
|
+
balance_created_after: Optional[str] = Field(None, description="Filter by balance creation date after (ISO 8601)")
|
|
185
|
+
balance_created_before: Optional[str] = Field(None, description="Filter by balance creation date before (ISO 8601)")
|
|
147
186
|
with_metadata: Optional[bool] = Field(None, description="Include metadata")
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/utils/__init__.py
RENAMED
|
@@ -6,6 +6,11 @@ from .errors import (
|
|
|
6
6
|
AuthenticationError,
|
|
7
7
|
ValidationError,
|
|
8
8
|
RateLimitError,
|
|
9
|
+
AuthorizationError,
|
|
10
|
+
OrderError,
|
|
11
|
+
OrderValidationError,
|
|
12
|
+
CompanyAccessError,
|
|
13
|
+
TradingNotEnabledError,
|
|
9
14
|
)
|
|
10
15
|
|
|
11
16
|
__all__ = [
|
|
@@ -14,4 +19,9 @@ __all__ = [
|
|
|
14
19
|
"AuthenticationError",
|
|
15
20
|
"ValidationError",
|
|
16
21
|
"RateLimitError",
|
|
22
|
+
"AuthorizationError",
|
|
23
|
+
"OrderError",
|
|
24
|
+
"OrderValidationError",
|
|
25
|
+
"CompanyAccessError",
|
|
26
|
+
"TradingNotEnabledError",
|
|
17
27
|
]
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/utils/errors.py
RENAMED
|
@@ -83,4 +83,12 @@ class CompanyAccessError(ApiError):
|
|
|
83
83
|
|
|
84
84
|
def __init__(self, message: str, response_data: Optional[Dict[str, Any]] = None):
|
|
85
85
|
super().__init__(message, 403, response_data)
|
|
86
|
-
self.code = "COMPANY_ACCESS_ERROR"
|
|
86
|
+
self.code = "COMPANY_ACCESS_ERROR"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TradingNotEnabledError(ApiError):
|
|
90
|
+
"""Raised when trading is not enabled for the company."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, message: str, response_data: Optional[Dict[str, Any]] = None):
|
|
93
|
+
super().__init__(message, 403, response_data)
|
|
94
|
+
self.code = "TRADING_NOT_ENABLED"
|
|
File without changes
|
|
File without changes
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/core/__init__.py
RENAMED
|
File without changes
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/auth.py
RENAMED
|
File without changes
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/common.py
RENAMED
|
File without changes
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/orders.py
RENAMED
|
File without changes
|
{finatic_server_python-0.1.2 → finatic_server_python-0.1.4}/src/finatic_server/types/portfolio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|