1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
bcae3d4e29
Ruff
Some checks failed
Test Python / Test (push) Failing after 26s
2024-07-27 03:14:12 +02:00
5f89c5d6df
Use nordnet client 2024-07-27 03:13:50 +02:00
6 changed files with 43 additions and 92 deletions

View File

@ -2,9 +2,9 @@
import datetime
import logging
import time
from decimal import Decimal
import time
import fin_defs
import kucoin.client
@ -138,9 +138,9 @@ class KucoinDepoFetcher(DepoFetcher):
# Determine order details
return self._get_order_details(response['orderId'], input_asset, output_asset)
def _get_order_details(self, order_id: str,
input_asset: fin_defs.Asset,
output_asset: fin_defs.Asset) -> TradeOrderDetails:
def _get_order_details(
self, order_id: str, input_asset: fin_defs.Asset, output_asset: fin_defs.Asset,
) -> TradeOrderDetails:
"""Determine the order details for the order with the given id.
Retries the order a few times, as KuCoin might not have propagated the
@ -164,13 +164,15 @@ class KucoinDepoFetcher(DepoFetcher):
fee_asset=fin_defs.WELL_KNOWN_SYMBOLS[order_details['feeCurrency']],
fee_amount=Decimal(order_details['fee']),
executed_time=datetime.datetime.fromtimestamp(
order_details['createdAt'] / 1000, tz=datetime.UTC
order_details['createdAt'] / 1000, tz=datetime.UTC,
),
order_id=order_id,
raw_order_details=order_details,
)
def _get_order_with_retries(self, order_id: str, *, num_retries: int, sleep_between_tries:float = 1.0) -> dict:
def _get_order_with_retries(
self, order_id: str, *, num_retries: int, sleep_between_tries: float = 1.0,
) -> dict:
"""Get the order details from KuCoin backend.
Retries the order a few times, as KuCoin might not have propagated the
@ -179,6 +181,6 @@ class KucoinDepoFetcher(DepoFetcher):
for _ in range(num_retries):
try:
return self.kucoin_client.get_order(order_id)
except kucoin.exceptions.KucoinAPIException as e: # noqa
except kucoin.exceptions.KucoinAPIException as e: # noqa
time.sleep(sleep_between_tries)
return self.kucoin_client.get_order(order_id)

View File

@ -13,33 +13,22 @@ import datetime
import logging
from collections.abc import Mapping
from decimal import Decimal
from typing import Any
import fin_defs
import requests
from frozendict import frozendict
from nordnet_api_client import NordnetClient
from .data import Depo, DepoFetcher, DepoGroup, DepoSingle
logger = logging.getLogger(__name__)
API_HOSTNAME = 'public.nordnet.dk'
API_ROOT = f'https://{API_HOSTNAME}/api/2'
API_LOGIN_STEP_1 = 'https://www.nordnet.dk/logind'
API_LOGIN_STEP_2 = API_ROOT + '/authentication/basic/login'
API_ACCOUNTS = API_ROOT + '/accounts'
API_ACCOUNT_INFO = API_ROOT + '/accounts/{accid}/info'
API_ACCOUNT_LEDGERS = API_ROOT + '/accounts/{accid}/ledgers'
API_ACCOUNT_POSITIONS = API_ROOT + '/accounts/{accid}/positions'
def asset_from_instrument_json(json) -> fin_defs.Asset | None:
if json['instrument_group_type'] == 'FND':
def asset_from_instrument(intrument) -> fin_defs.Asset | None:
if intrument.instrument_group_type == 'FND':
return None
symbol = json['symbol']
exchange_id = json['tradables'][0]['mic']
symbol = intrument.symbol
exchange_id = intrument.tradables[0].mic
return fin_defs.Stock(symbol, fin_defs.EXCHANGES_BY_IDS[exchange_id])
@ -63,88 +52,42 @@ class NordnetDepoFetcher(DepoFetcher):
"""
def __init__(self, session: requests.Session, username: str, password: str):
self.session = self.assert_param('session', requests.Session, session)
self.username: str = self.assert_param('username', str, username)
self.password: str = self.assert_param('password', str, password)
self.is_logged_in = False
def _get_json(self, url: str, params: Mapping[str, str | int] = EMPTY_DICT) -> Any:
if not url.startswith(API_ROOT):
msg = f'Given url must be located below API ROOT: {url}'
raise ValueError(msg)
headers = {
'Accepts': 'application/json',
}
response = self.session.get(url, params=params, headers=headers)
json = response.json()
response.raise_for_status()
return json
def login(self) -> None:
"""Performs authentication with the login server if not already logged
in. Does not need to be manually called; most methods that require this
information will do it themselves.
This method is based on Morten Helmstedt's version:
<https://github.com/helmstedt/nordnet-utilities/blob/main/nordnet_login.py>
"""
if self.is_logged_in:
return
self.session.get(API_LOGIN_STEP_1)
self.session.headers['client-id'] = 'NEXT'
self.session.headers['sub-client-id'] = 'NEXT'
response = self.session.post(
API_LOGIN_STEP_2,
data={'username': self.username, 'password': self.password},
)
response.raise_for_status()
self.is_logged_in = True
self.client = NordnetClient(session, username, password)
def get_depo(self) -> Depo:
self.login()
now = datetime.datetime.now(
tz=datetime.UTC,
) # TODO: Use info from json requests
json_accounts = self._get_json(API_ACCOUNTS)
accounts = self.client.get_accounts()
nested: list[Depo] = []
for json_account in json_accounts:
account_id = json_account['accid']
for account in accounts:
account_id = account.accid
assets: dict[fin_defs.Asset, Decimal] = {}
# Determine amount of usable currency stored on Nordnet
if True:
json_account_info = self._get_json(
API_ACCOUNT_INFO.format(accid=account_id),
)
for account_info in self.client.get_account_info(account_id):
asset = fin_defs.FiatCurrency(
json_account_info[0]['account_sum']['currency'],
account_info.account_sum.currency,
)
amount = Decimal(json_account_info[0]['account_sum']['value'])
amount = Decimal(account_info.account_sum.value)
assets[asset] = amount
del asset, amount
# Determine positions on Nordnet
json_account_positions = self._get_json(
API_ACCOUNT_POSITIONS.format(accid=account_id),
)
for pos in json_account_positions:
asset = asset_from_instrument_json(pos['instrument'])
for pos in self.client.get_account_positions(account_id):
asset = asset_from_instrument(pos.instrument)
if asset is None:
continue
amount = Decimal(pos['qty'])
amount = Decimal(pos.qty)
assets[asset] = amount
del asset, amount
nested.append(
DepoSingle(
name='{} {}'.format(json_account['type'], json_account['alias']),
name=f'{account.type} {account.alias}',
_assets=assets,
updated_time=now,
),

View File

@ -4,18 +4,18 @@ This module does not represent an API or other integration. This module
contains aggregators and static depositories.
"""
import dataclasses
import datetime
import logging
from decimal import Decimal
import fin_defs
import krakenex
import dataclasses
from .data import Depo, DepoFetcher, DepoSingle, DepoGroup
from .data import DepoFetcher, DepoGroup, DepoSingle
logger = logging.getLogger(__name__)
@dataclasses.dataclass(frozen=True)
class StaticDepoFetcher(DepoFetcher):
"""Depository "fetcher" that doesn't do any fetching.
@ -24,8 +24,10 @@ class StaticDepoFetcher(DepoFetcher):
"""
name: str
depo_assets: dict[fin_defs.Asset,Decimal]
last_updated: datetime.datetime = dataclasses.field(default_factory=lambda: datetime.datetime.now(tz=datetime.UTC))
depo_assets: dict[fin_defs.Asset, Decimal]
last_updated: datetime.datetime = dataclasses.field(
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC),
)
def get_depo(self) -> DepoSingle:
return DepoSingle(
@ -34,6 +36,7 @@ class StaticDepoFetcher(DepoFetcher):
updated_time=self.last_updated,
)
@dataclasses.dataclass(frozen=True)
class AggregateDepoFetcher(DepoFetcher):
"""Depository "fetcher" that delegates to the aggregated fetchers."""
@ -43,7 +46,7 @@ class AggregateDepoFetcher(DepoFetcher):
def get_depo(self) -> DepoGroup:
return DepoGroup(
name=self.name,
nested=[fetcher.get_depo() for fetcher in self.aggregated],
updated_time=datetime.datetime.now(tz=datetime.UTC), # TODO
name=self.name,
nested=[fetcher.get_depo() for fetcher in self.aggregated],
updated_time=datetime.datetime.now(tz=datetime.UTC), # TODO
)

View File

@ -1,5 +1,4 @@
import pytest
import requests
import fin_depo

View File

@ -1,8 +1,8 @@
import datetime
from decimal import Decimal
import fin_defs
import pytest
import datetime
import fin_depo
@ -19,6 +19,7 @@ fin_depo.defi_kucoin.logger.setLevel('INFO')
NOW = datetime.datetime.now(tz=datetime.UTC)
@needs_secrets
def test_get_depo():
"""Can inspect kucoin depository."""
@ -86,6 +87,7 @@ def test_place_buy_side_order():
assert NOW <= order_details.executed_time <= NOW + datetime.timedelta(minutes=10)
@needs_secrets
def test_place_sell_side_order():
"""Client can place sell side market orders."""

View File

@ -1,11 +1,13 @@
from decimal import Decimal
import fin_defs
import fin_depo
from decimal import Decimal
def test_get_depo():
fetcher = fin_depo.static.StaticDepoFetcher(
'Test', {fin_defs.BTC: Decimal(1000), fin_defs.USD: Decimal(2000)}
'Test', {fin_defs.BTC: Decimal(1000), fin_defs.USD: Decimal(2000)},
)
depo = fetcher.get_depo()