Compare commits
2 Commits
4340d3ece2
...
bcae3d4e29
Author | SHA1 | Date | |
---|---|---|---|
bcae3d4e29 | |||
5f89c5d6df |
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import time
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
import kucoin.client
|
import kucoin.client
|
||||||
|
|
||||||
|
@ -138,9 +138,9 @@ class KucoinDepoFetcher(DepoFetcher):
|
||||||
# Determine order details
|
# Determine order details
|
||||||
return self._get_order_details(response['orderId'], input_asset, output_asset)
|
return self._get_order_details(response['orderId'], input_asset, output_asset)
|
||||||
|
|
||||||
def _get_order_details(self, order_id: str,
|
def _get_order_details(
|
||||||
input_asset: fin_defs.Asset,
|
self, order_id: str, input_asset: fin_defs.Asset, output_asset: fin_defs.Asset,
|
||||||
output_asset: fin_defs.Asset) -> TradeOrderDetails:
|
) -> TradeOrderDetails:
|
||||||
"""Determine the order details for the order with the given id.
|
"""Determine the order details for the order with the given id.
|
||||||
|
|
||||||
Retries the order a few times, as KuCoin might not have propagated the
|
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_asset=fin_defs.WELL_KNOWN_SYMBOLS[order_details['feeCurrency']],
|
||||||
fee_amount=Decimal(order_details['fee']),
|
fee_amount=Decimal(order_details['fee']),
|
||||||
executed_time=datetime.datetime.fromtimestamp(
|
executed_time=datetime.datetime.fromtimestamp(
|
||||||
order_details['createdAt'] / 1000, tz=datetime.UTC
|
order_details['createdAt'] / 1000, tz=datetime.UTC,
|
||||||
),
|
),
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
raw_order_details=order_details,
|
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.
|
"""Get the order details from KuCoin backend.
|
||||||
|
|
||||||
Retries the order a few times, as KuCoin might not have propagated the
|
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):
|
for _ in range(num_retries):
|
||||||
try:
|
try:
|
||||||
return self.kucoin_client.get_order(order_id)
|
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)
|
time.sleep(sleep_between_tries)
|
||||||
return self.kucoin_client.get_order(order_id)
|
return self.kucoin_client.get_order(order_id)
|
||||||
|
|
|
@ -13,33 +13,22 @@ import datetime
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
import requests
|
import requests
|
||||||
from frozendict import frozendict
|
from frozendict import frozendict
|
||||||
|
from nordnet_api_client import NordnetClient
|
||||||
|
|
||||||
from .data import Depo, DepoFetcher, DepoGroup, DepoSingle
|
from .data import Depo, DepoFetcher, DepoGroup, DepoSingle
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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'
|
def asset_from_instrument(intrument) -> fin_defs.Asset | None:
|
||||||
API_LOGIN_STEP_2 = API_ROOT + '/authentication/basic/login'
|
if intrument.instrument_group_type == 'FND':
|
||||||
|
|
||||||
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':
|
|
||||||
return None
|
return None
|
||||||
symbol = json['symbol']
|
symbol = intrument.symbol
|
||||||
exchange_id = json['tradables'][0]['mic']
|
exchange_id = intrument.tradables[0].mic
|
||||||
return fin_defs.Stock(symbol, fin_defs.EXCHANGES_BY_IDS[exchange_id])
|
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):
|
def __init__(self, session: requests.Session, username: str, password: str):
|
||||||
self.session = self.assert_param('session', requests.Session, session)
|
self.client = NordnetClient(session, username, password)
|
||||||
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
|
|
||||||
|
|
||||||
def get_depo(self) -> Depo:
|
def get_depo(self) -> Depo:
|
||||||
self.login()
|
|
||||||
now = datetime.datetime.now(
|
now = datetime.datetime.now(
|
||||||
tz=datetime.UTC,
|
tz=datetime.UTC,
|
||||||
) # TODO: Use info from json requests
|
) # TODO: Use info from json requests
|
||||||
|
|
||||||
json_accounts = self._get_json(API_ACCOUNTS)
|
accounts = self.client.get_accounts()
|
||||||
|
|
||||||
nested: list[Depo] = []
|
nested: list[Depo] = []
|
||||||
for json_account in json_accounts:
|
for account in accounts:
|
||||||
account_id = json_account['accid']
|
account_id = account.accid
|
||||||
|
|
||||||
assets: dict[fin_defs.Asset, Decimal] = {}
|
assets: dict[fin_defs.Asset, Decimal] = {}
|
||||||
|
|
||||||
# Determine amount of usable currency stored on Nordnet
|
# Determine amount of usable currency stored on Nordnet
|
||||||
if True:
|
for account_info in self.client.get_account_info(account_id):
|
||||||
json_account_info = self._get_json(
|
|
||||||
API_ACCOUNT_INFO.format(accid=account_id),
|
|
||||||
)
|
|
||||||
asset = fin_defs.FiatCurrency(
|
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
|
assets[asset] = amount
|
||||||
del asset, amount
|
del asset, amount
|
||||||
|
|
||||||
# Determine positions on Nordnet
|
# Determine positions on Nordnet
|
||||||
json_account_positions = self._get_json(
|
for pos in self.client.get_account_positions(account_id):
|
||||||
API_ACCOUNT_POSITIONS.format(accid=account_id),
|
asset = asset_from_instrument(pos.instrument)
|
||||||
)
|
|
||||||
for pos in json_account_positions:
|
|
||||||
asset = asset_from_instrument_json(pos['instrument'])
|
|
||||||
if asset is None:
|
if asset is None:
|
||||||
continue
|
continue
|
||||||
amount = Decimal(pos['qty'])
|
amount = Decimal(pos.qty)
|
||||||
assets[asset] = amount
|
assets[asset] = amount
|
||||||
del asset, amount
|
del asset, amount
|
||||||
|
|
||||||
nested.append(
|
nested.append(
|
||||||
DepoSingle(
|
DepoSingle(
|
||||||
name='{} {}'.format(json_account['type'], json_account['alias']),
|
name=f'{account.type} {account.alias}',
|
||||||
_assets=assets,
|
_assets=assets,
|
||||||
updated_time=now,
|
updated_time=now,
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,18 +4,18 @@ This module does not represent an API or other integration. This module
|
||||||
contains aggregators and static depositories.
|
contains aggregators and static depositories.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
import krakenex
|
|
||||||
import dataclasses
|
|
||||||
|
|
||||||
from .data import Depo, DepoFetcher, DepoSingle, DepoGroup
|
from .data import DepoFetcher, DepoGroup, DepoSingle
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class StaticDepoFetcher(DepoFetcher):
|
class StaticDepoFetcher(DepoFetcher):
|
||||||
"""Depository "fetcher" that doesn't do any fetching.
|
"""Depository "fetcher" that doesn't do any fetching.
|
||||||
|
@ -24,8 +24,10 @@ class StaticDepoFetcher(DepoFetcher):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
depo_assets: dict[fin_defs.Asset,Decimal]
|
depo_assets: dict[fin_defs.Asset, Decimal]
|
||||||
last_updated: datetime.datetime = dataclasses.field(default_factory=lambda: datetime.datetime.now(tz=datetime.UTC))
|
last_updated: datetime.datetime = dataclasses.field(
|
||||||
|
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC),
|
||||||
|
)
|
||||||
|
|
||||||
def get_depo(self) -> DepoSingle:
|
def get_depo(self) -> DepoSingle:
|
||||||
return DepoSingle(
|
return DepoSingle(
|
||||||
|
@ -34,6 +36,7 @@ class StaticDepoFetcher(DepoFetcher):
|
||||||
updated_time=self.last_updated,
|
updated_time=self.last_updated,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class AggregateDepoFetcher(DepoFetcher):
|
class AggregateDepoFetcher(DepoFetcher):
|
||||||
"""Depository "fetcher" that delegates to the aggregated fetchers."""
|
"""Depository "fetcher" that delegates to the aggregated fetchers."""
|
||||||
|
@ -43,7 +46,7 @@ class AggregateDepoFetcher(DepoFetcher):
|
||||||
|
|
||||||
def get_depo(self) -> DepoGroup:
|
def get_depo(self) -> DepoGroup:
|
||||||
return DepoGroup(
|
return DepoGroup(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
nested=[fetcher.get_depo() for fetcher in self.aggregated],
|
nested=[fetcher.get_depo() for fetcher in self.aggregated],
|
||||||
updated_time=datetime.datetime.now(tz=datetime.UTC), # TODO
|
updated_time=datetime.datetime.now(tz=datetime.UTC), # TODO
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
|
||||||
|
|
||||||
import fin_depo
|
import fin_depo
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
import pytest
|
import pytest
|
||||||
import datetime
|
|
||||||
|
|
||||||
import fin_depo
|
import fin_depo
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ fin_depo.defi_kucoin.logger.setLevel('INFO')
|
||||||
|
|
||||||
NOW = datetime.datetime.now(tz=datetime.UTC)
|
NOW = datetime.datetime.now(tz=datetime.UTC)
|
||||||
|
|
||||||
|
|
||||||
@needs_secrets
|
@needs_secrets
|
||||||
def test_get_depo():
|
def test_get_depo():
|
||||||
"""Can inspect kucoin depository."""
|
"""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)
|
assert NOW <= order_details.executed_time <= NOW + datetime.timedelta(minutes=10)
|
||||||
|
|
||||||
|
|
||||||
@needs_secrets
|
@needs_secrets
|
||||||
def test_place_sell_side_order():
|
def test_place_sell_side_order():
|
||||||
"""Client can place sell side market orders."""
|
"""Client can place sell side market orders."""
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
import fin_defs
|
import fin_defs
|
||||||
|
|
||||||
import fin_depo
|
import fin_depo
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
def test_get_depo():
|
def test_get_depo():
|
||||||
fetcher = fin_depo.static.StaticDepoFetcher(
|
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()
|
depo = fetcher.get_depo()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user