finatic-server-python 0.1.3__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.
Files changed (21) hide show
  1. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/PKG-INFO +1 -1
  2. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/pyproject.toml +1 -1
  3. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/core/api_client.py +102 -0
  4. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/core/client.py +315 -4
  5. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/__init__.py +4 -0
  6. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/broker.py +39 -0
  7. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/PKG-INFO +1 -1
  8. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/README.md +0 -0
  9. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/setup.cfg +0 -0
  10. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/__init__.py +0 -0
  11. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/core/__init__.py +0 -0
  12. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/auth.py +0 -0
  13. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/common.py +0 -0
  14. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/orders.py +0 -0
  15. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/types/portfolio.py +0 -0
  16. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/utils/__init__.py +0 -0
  17. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server/utils/errors.py +0 -0
  18. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/SOURCES.txt +0 -0
  19. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/dependency_links.txt +0 -0
  20. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/requires.txt +0 -0
  21. {finatic_server_python-0.1.3 → finatic_server_python-0.1.4}/src/finatic_server_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finatic-server-python
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Python SDK for Finatic Server API
5
5
  Author-email: Finatic <support@finatic.dev>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "finatic-server-python"
7
- version = "0.1.3"
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" }
@@ -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,
@@ -760,6 +762,68 @@ class ApiClient:
760
762
  navigation_callback
761
763
  )
762
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
+
763
827
  # Helper methods to get all data across pages
764
828
  async def get_all_broker_accounts(self, options: Optional[BrokerDataOptions] = None, filters: Optional[AccountsFilter] = None) -> List[BrokerAccount]:
765
829
  """Get all broker accounts across all pages."""
@@ -811,6 +875,23 @@ class ApiClient:
811
875
  page += 1
812
876
 
813
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
814
895
 
815
896
  async def get_broker_connections_auto(self) -> List[BrokerConnection]:
816
897
  """Get broker connections using stored access token."""
@@ -821,6 +902,27 @@ class ApiClient:
821
902
  access_token=access_token
822
903
  )
823
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
824
926
 
825
927
  # Trading context methods
826
928
  def set_broker(self, broker: str):
@@ -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
- return response.data['portal_url']
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:
@@ -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
  ]
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finatic-server-python
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Python SDK for Finatic Server API
5
5
  Author-email: Finatic <support@finatic.dev>
6
6
  License: MIT