From eb9042eaa4f32e34d7e10fb58433bcd43cc33f7f Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 7 Jan 2022 14:46:30 -0800 Subject: [PATCH] Add Non-KYC Bitcoin price historical records --- api/admin.py | 16 ++++++++++++---- api/logics.py | 14 ++++++-------- api/models.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- api/utils.py | 12 ++++++++++++ api/views.py | 34 ++++++++++++++++++++-------------- 5 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 api/utils.py diff --git a/api/admin.py b/api/admin.py index 9b57b651..a5b32506 100644 --- a/api/admin.py +++ b/api/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django_admin_relation_links import AdminChangeLinksMixin from django.contrib.auth.models import Group, User from django.contrib.auth.admin import UserAdmin -from .models import Order, LNPayment, Profile +from .models import Order, LNPayment, Profile, MarketTick admin.site.unregister(Group) admin.site.unregister(User) @@ -17,8 +17,8 @@ class ProfileInline(admin.StackedInline): @admin.register(User) class EUserAdmin(UserAdmin): inlines = [ProfileInline] - list_display = ('avatar_tag',) + UserAdmin.list_display - list_display_links = ['username'] + list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff') + list_display_links = ('id','username') def avatar_tag(self, obj): return obj.profile.avatar_tag() @@ -27,16 +27,24 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') list_display_links = ('id','type') change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') + list_filter = ('is_disputed','is_fiat_sent','type','currency','status') @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') + list_filter = ('type','concept','status') @admin.register(Profile) class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes') list_display_links = ('avatar_tag','id') change_links =['user'] - readonly_fields = ['avatar_tag'] \ No newline at end of file + readonly_fields = ['avatar_tag'] + +@admin.register(MarketTick) +class MarketTickAdmin(admin.ModelAdmin): + list_display = ('timestamp','price','volume','premium','currency','fee') + readonly_fields = ('timestamp','price','volume','premium','currency','fee') + list_filter = ['currency'] \ No newline at end of file diff --git a/api/logics.py b/api/logics.py index 19dd971b..140f8ae1 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1,10 +1,10 @@ from datetime import timedelta from django.utils import timezone -import requests from .lightning import LNNode -from .models import Order, LNPayment, User +from .models import Order, LNPayment, MarketTick, User from decouple import config +from .utils import get_exchange_rate FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) @@ -61,11 +61,9 @@ class Logics(): if order.is_explicit: satoshis_now = order.satoshis else: - # TODO Add fallback Public APIs and error handling - # Think about polling price data in a different way (e.g. store locally every t seconds) - market_prices = requests.get(MARKET_PRICE_API).json() - exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) - satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + exchange_rate = get_exchange_rate(Order.Currencies(order.currency).label) + premium_rate = exchange_rate * (1+float(order.premium)/100) + satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000 return int(satoshis_now) @@ -306,7 +304,7 @@ class Logics(): order.save() return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} - + def settle_escrow(order): ''' Settles the trade escrow HTLC''' # TODO ERROR HANDLING diff --git a/api/models.py b/api/models.py index 054381c9..d69cd767 100644 --- a/api/models.py +++ b/api/models.py @@ -7,18 +7,14 @@ from django.utils.html import mark_safe from decouple import config from pathlib import Path +from .utils import get_exchange_rate -############################# -# TODO -# Load hparams from .env file MIN_TRADE = int(config('MIN_TRADE')) MAX_TRADE = int(config('MAX_TRADE')) FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) - - class LNPayment(models.Model): class Types(models.IntegerChoices): @@ -192,3 +188,48 @@ class Profile(models.Model): def avatar_tag(self): return mark_safe('' % self.get_avatar()) +class MarketTick(models.Model): + ''' + Records tick by tick Non-KYC Bitcoin price. + Data to be aggregated and offered via public API. + + It is checked against current cex prices for nice + insight on the historical premium of Non-KYC BTC + + Price is set when both taker bond is locked. Both + maker and taker are commited with bonds (contract + is finished and cancellation has a cost) + ''' + + price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)]) + volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)]) + premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) + currency = models.PositiveSmallIntegerField(choices=Order.Currencies.choices, null=True) + timestamp = models.DateTimeField(auto_now_add=True) + + # Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed + fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)]) + + def log_a_tick(order): + ''' + Adds a new tick + ''' + if not order.taker_bond: + return None + + elif order.taker_bond.status == LNPayment.Status.LOCKED: + volume = order.last_satoshis / 100000000 + price = float(order.amount) / volume # Amount Fiat / Amount BTC + premium = 100 * (price / get_exchange_rate(Order.Currencies(order.currency).label) - 1) + + tick = MarketTick.objects.create( + price=price, + volume=volume, + premium=premium, + currency=order.currency) + tick.save() + + def __str__(self): + return f'Tick: {self.id}' + + diff --git a/api/utils.py b/api/utils.py new file mode 100644 index 00000000..0b1b934b --- /dev/null +++ b/api/utils.py @@ -0,0 +1,12 @@ + +from decouple import config +import requests + +def get_exchange_rate(currency): + # TODO Add fallback Public APIs and error handling + # Think about polling price data in a different way (e.g. store locally every t seconds) + + market_prices = requests.get(config('MARKET_PRICE_API')).json() + exchange_rate = float(market_prices[currency]['last']) + + return exchange_rate \ No newline at end of file diff --git a/api/views.py b/api/views.py index 8ac2fd3e..d8ccf874 100644 --- a/api/views.py +++ b/api/views.py @@ -322,23 +322,27 @@ class UserView(APIView): # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) context['found'] = 'Bad luck, this nickname is taken' context['bad_request'] = 'Enter a different token' - return Response(context, status=status.HTTP_403_FORBIDDEN) + return Response(context, status.HTTP_403_FORBIDDEN) def delete(self,request): - user = User.objects.get(id = request.user.id) + ''' Pressing "give me another" deletes the logged in user ''' + user = request.user + if not user: + return Response(status.HTTP_403_FORBIDDEN) - # TO DO. Pressing "give me another" deletes the logged in user - # However it might be a long time recovered user - # Only delete if user live is < 5 minutes + # Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake + if user.date_joined < (timezone.now() - timedelta(minutes=30)): + return Response(status.HTTP_400_BAD_REQUEST) - # TODO check if user exists AND it is not a maker or taker! - if user is not None: - logout(request) - user.delete() + # Check if it is not a maker or taker! + if not Logics.validate_already_maker_or_taker(user): + return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST) - return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND) + logout(request) + user.delete() + return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY) - return Response(status=status.HTTP_403_FORBIDDEN) + class BookView(ListAPIView): serializer_class = ListOrderSerializer @@ -367,14 +371,16 @@ class BookView(ListAPIView): class InfoView(ListAPIView): - def get(self, request, format = None): + def get(self): context = {} + context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)) context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)) - context['num_active_robots'] = None # Todo + context['last_day_avg_btc_premium'] = None # Todo + context['num_active_robots'] = None context['total_volume'] = None - return Response(context, status.HTTP_200_ok) + return Response(context, status.HTTP_200_OK)