Add basic practice account support
							parent
							
								
									435b2a56e8
								
							
						
					
					
						commit
						462c419970
					
				| 
						 | 
					@ -25,7 +25,8 @@ asks.init('trio')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log = get_logger(__name__)
 | 
					log = get_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_refresh_token_ep = 'https://login.questrade.com/oauth2/'
 | 
					_use_practice_account = False
 | 
				
			||||||
 | 
					_refresh_token_ep = 'https://{}login.questrade.com/oauth2/'
 | 
				
			||||||
_version = 'v1'
 | 
					_version = 'v1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# stock queries/sec
 | 
					# stock queries/sec
 | 
				
			||||||
| 
						 | 
					@ -102,7 +103,10 @@ class _API:
 | 
				
			||||||
        resp = await self._sess.get(path=f'/{path}', params=params)
 | 
					        resp = await self._sess.get(path=f'/{path}', params=params)
 | 
				
			||||||
        return resproc(resp, log)
 | 
					        return resproc(resp, log)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def _new_auth_token(self, refresh_token: str) -> dict:
 | 
					    async def _new_auth_token(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        refresh_token: str,
 | 
				
			||||||
 | 
					    ) -> dict:
 | 
				
			||||||
        """Request a new api authorization ``refresh_token``.
 | 
					        """Request a new api authorization ``refresh_token``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Gain api access using either a user provided or existing token.
 | 
					        Gain api access using either a user provided or existing token.
 | 
				
			||||||
| 
						 | 
					@ -112,18 +116,42 @@ class _API:
 | 
				
			||||||
        http://www.questrade.com/api/documentation/security
 | 
					        http://www.questrade.com/api/documentation/security
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        resp = await self._sess.get(
 | 
					        resp = await self._sess.get(
 | 
				
			||||||
            _refresh_token_ep + 'token',
 | 
					            self.client._auth_ep + 'token',
 | 
				
			||||||
            params={'grant_type': 'refresh_token',
 | 
					            params={'grant_type': 'refresh_token',
 | 
				
			||||||
                    'refresh_token': refresh_token}
 | 
					                    'refresh_token': refresh_token}
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return resproc(resp, log)
 | 
					        return resproc(resp, log)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def _revoke_auth_token(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        practise: bool = False,
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        """Revoke api access for the current token.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        token = self.access_data['refresh_token']
 | 
				
			||||||
 | 
					        log.debug(f"Revoking token {token}")
 | 
				
			||||||
 | 
					        resp = await asks.post(
 | 
				
			||||||
 | 
					            self.client._auth_ep + 'revoke',
 | 
				
			||||||
 | 
					            headers={'token': token}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # accounts end points
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def accounts(self) -> dict:
 | 
					    async def accounts(self) -> dict:
 | 
				
			||||||
        return await self._get('accounts')
 | 
					        return await self._get('accounts')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def time(self) -> dict:
 | 
					    async def time(self) -> dict:
 | 
				
			||||||
        return await self._get('time')
 | 
					        return await self._get('time')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def balances(self, id: str) -> dict:
 | 
				
			||||||
 | 
					        return await self._get(f'accounts/{id}/balances')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def postions(self, id: str) -> dict:
 | 
				
			||||||
 | 
					        return await self._get(f'accounts/{id}/positions')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # market end points
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def markets(self) -> dict:
 | 
					    async def markets(self) -> dict:
 | 
				
			||||||
        return await self._get('markets')
 | 
					        return await self._get('markets')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,12 +174,6 @@ class _API:
 | 
				
			||||||
    async def candles(self, id: str, start: str, end, interval) -> dict:
 | 
					    async def candles(self, id: str, start: str, end, interval) -> dict:
 | 
				
			||||||
        return await self._get(f'markets/candles/{id}', params={})
 | 
					        return await self._get(f'markets/candles/{id}', params={})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def balances(self, id: str) -> dict:
 | 
					 | 
				
			||||||
        return await self._get(f'accounts/{id}/balances')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def postions(self, id: str) -> dict:
 | 
					 | 
				
			||||||
        return await self._get(f'accounts/{id}/positions')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def option_contracts(self, symbol_id: str) -> dict:
 | 
					    async def option_contracts(self, symbol_id: str) -> dict:
 | 
				
			||||||
        "Retrieve all option contract API ids with expiry -> strike prices."
 | 
					        "Retrieve all option contract API ids with expiry -> strike prices."
 | 
				
			||||||
        contracts = await self._get(f'symbols/{symbol_id}/options')
 | 
					        contracts = await self._get(f'symbols/{symbol_id}/options')
 | 
				
			||||||
| 
						 | 
					@ -189,10 +211,16 @@ class Client:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Provides a high-level api which wraps the underlying endpoint calls.
 | 
					    Provides a high-level api which wraps the underlying endpoint calls.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    def __init__(self, config: configparser.ConfigParser):
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        config: configparser.ConfigParser,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
        self._sess = asks.Session()
 | 
					        self._sess = asks.Session()
 | 
				
			||||||
        self.api = _API(self)
 | 
					        self.api = _API(self)
 | 
				
			||||||
        self._conf = config
 | 
					        self._conf = config
 | 
				
			||||||
 | 
					        self._is_practise_account = _use_practice_account
 | 
				
			||||||
 | 
					        self._auth_ep = _refresh_token_ep.format(
 | 
				
			||||||
 | 
					            'practice' if _use_practice_account else '')
 | 
				
			||||||
        self.access_data = {}
 | 
					        self.access_data = {}
 | 
				
			||||||
        self._reload_config(config)
 | 
					        self._reload_config(config)
 | 
				
			||||||
        self._symbol_cache: Dict[str, int] = {}
 | 
					        self._symbol_cache: Dict[str, int] = {}
 | 
				
			||||||
| 
						 | 
					@ -209,17 +237,6 @@ class Client:
 | 
				
			||||||
        self._conf = config or get_config(**kwargs)
 | 
					        self._conf = config or get_config(**kwargs)
 | 
				
			||||||
        self.access_data = dict(self._conf['questrade'])
 | 
					        self.access_data = dict(self._conf['questrade'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def _revoke_auth_token(self) -> None:
 | 
					 | 
				
			||||||
        """Revoke api access for the current token.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        token = self.access_data['refresh_token']
 | 
					 | 
				
			||||||
        log.debug(f"Revoking token {token}")
 | 
					 | 
				
			||||||
        resp = await asks.post(
 | 
					 | 
				
			||||||
            _refresh_token_ep + 'revoke',
 | 
					 | 
				
			||||||
            headers={'token': token}
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return resp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def write_config(self):
 | 
					    def write_config(self):
 | 
				
			||||||
        """Save access creds to config file.
 | 
					        """Save access creds to config file.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -256,7 +273,7 @@ class Client:
 | 
				
			||||||
                if not access_token or (
 | 
					                if not access_token or (
 | 
				
			||||||
                    expires < time.time()
 | 
					                    expires < time.time()
 | 
				
			||||||
                ) or force_refresh:
 | 
					                ) or force_refresh:
 | 
				
			||||||
                    log.info("REFRESHING TOKENS!")
 | 
					                    log.info("Refreshing API tokens")
 | 
				
			||||||
                    log.debug(
 | 
					                    log.debug(
 | 
				
			||||||
                        f"Refreshing access token {access_token} which expired"
 | 
					                        f"Refreshing access token {access_token} which expired"
 | 
				
			||||||
                        f" at {expires_stamp}")
 | 
					                        f" at {expires_stamp}")
 | 
				
			||||||
| 
						 | 
					@ -304,7 +321,7 @@ class Client:
 | 
				
			||||||
                    # write to config to disk
 | 
					                    # write to config to disk
 | 
				
			||||||
                    self.write_config()
 | 
					                    self.write_config()
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    log.info(
 | 
					                    log.debug(
 | 
				
			||||||
                        f"\nCurrent access token {access_token} expires at"
 | 
					                        f"\nCurrent access token {access_token} expires at"
 | 
				
			||||||
                        f" {expires_stamp}\n")
 | 
					                        f" {expires_stamp}\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -525,17 +542,21 @@ def get_config(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@asynccontextmanager
 | 
					@asynccontextmanager
 | 
				
			||||||
async def get_client() -> Client:
 | 
					async def get_client(**kwargs) -> Client:
 | 
				
			||||||
    """Spawn a broker client for making requests to the API service.
 | 
					    """Spawn a broker client for making requests to the API service.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    conf = get_config()
 | 
					    conf = get_config(config_path=kwargs.get('config_path'))
 | 
				
			||||||
    log.debug(f"Loaded config:\n{colorize_json(dict(conf['questrade']))}")
 | 
					    log.debug(f"Loaded config:\n{colorize_json(dict(conf['questrade']))}")
 | 
				
			||||||
    client = Client(conf)
 | 
					    client = Client(conf, **kwargs)
 | 
				
			||||||
    await client.ensure_access()
 | 
					    await client.ensure_access()
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        log.debug("Check time to ensure access token is valid")
 | 
					        log.debug("Check time to ensure access token is valid")
 | 
				
			||||||
 | 
					        # XXX: the `time()` end point requires acc_read Oauth access.
 | 
				
			||||||
 | 
					        # In order to use a client you need at least one key with this
 | 
				
			||||||
 | 
					        # access enabled in order to do symbol searches and id lookups.
 | 
				
			||||||
        await client.api.time()
 | 
					        await client.api.time()
 | 
				
			||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        raise
 | 
				
			||||||
        # access token is likely no good
 | 
					        # access token is likely no good
 | 
				
			||||||
        log.warn(f"Access tokens {client.access_data} seem"
 | 
					        log.warn(f"Access tokens {client.access_data} seem"
 | 
				
			||||||
                 f" expired, forcing refresh")
 | 
					                 f" expired, forcing refresh")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue