mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 02:21:35 +00:00
Add Non-KYC Bitcoin price historical records
This commit is contained in:
parent
b472b4928c
commit
eb9042eaa4
16
api/admin.py
16
api/admin.py
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||||||
from django_admin_relation_links import AdminChangeLinksMixin
|
from django_admin_relation_links import AdminChangeLinksMixin
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.auth.admin import UserAdmin
|
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(Group)
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
@ -17,8 +17,8 @@ class ProfileInline(admin.StackedInline):
|
|||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class EUserAdmin(UserAdmin):
|
class EUserAdmin(UserAdmin):
|
||||||
inlines = [ProfileInline]
|
inlines = [ProfileInline]
|
||||||
list_display = ('avatar_tag',) + UserAdmin.list_display
|
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
|
||||||
list_display_links = ['username']
|
list_display_links = ('id','username')
|
||||||
def avatar_tag(self, obj):
|
def avatar_tag(self, obj):
|
||||||
return obj.profile.avatar_tag()
|
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 = ('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')
|
list_display_links = ('id','type')
|
||||||
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
|
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)
|
@admin.register(LNPayment)
|
||||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
|
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
|
||||||
list_display_links = ('id','concept')
|
list_display_links = ('id','concept')
|
||||||
change_links = ('sender','receiver')
|
change_links = ('sender','receiver')
|
||||||
|
list_filter = ('type','concept','status')
|
||||||
|
|
||||||
@admin.register(Profile)
|
@admin.register(Profile)
|
||||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
|
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||||
list_display_links = ('avatar_tag','id')
|
list_display_links = ('avatar_tag','id')
|
||||||
change_links =['user']
|
change_links =['user']
|
||||||
readonly_fields = ['avatar_tag']
|
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']
|
@ -1,10 +1,10 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import requests
|
|
||||||
from .lightning import LNNode
|
from .lightning import LNNode
|
||||||
|
|
||||||
from .models import Order, LNPayment, User
|
from .models import Order, LNPayment, MarketTick, User
|
||||||
from decouple import config
|
from decouple import config
|
||||||
|
from .utils import get_exchange_rate
|
||||||
|
|
||||||
FEE = float(config('FEE'))
|
FEE = float(config('FEE'))
|
||||||
BOND_SIZE = float(config('BOND_SIZE'))
|
BOND_SIZE = float(config('BOND_SIZE'))
|
||||||
@ -61,11 +61,9 @@ class Logics():
|
|||||||
if order.is_explicit:
|
if order.is_explicit:
|
||||||
satoshis_now = order.satoshis
|
satoshis_now = order.satoshis
|
||||||
else:
|
else:
|
||||||
# TODO Add fallback Public APIs and error handling
|
exchange_rate = get_exchange_rate(Order.Currencies(order.currency).label)
|
||||||
# Think about polling price data in a different way (e.g. store locally every t seconds)
|
premium_rate = exchange_rate * (1+float(order.premium)/100)
|
||||||
market_prices = requests.get(MARKET_PRICE_API).json()
|
satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
|
||||||
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
|
|
||||||
|
|
||||||
return int(satoshis_now)
|
return int(satoshis_now)
|
||||||
|
|
||||||
@ -306,7 +304,7 @@ class Logics():
|
|||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
|
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
|
||||||
|
|
||||||
def settle_escrow(order):
|
def settle_escrow(order):
|
||||||
''' Settles the trade escrow HTLC'''
|
''' Settles the trade escrow HTLC'''
|
||||||
# TODO ERROR HANDLING
|
# TODO ERROR HANDLING
|
||||||
|
@ -7,18 +7,14 @@ from django.utils.html import mark_safe
|
|||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from .utils import get_exchange_rate
|
||||||
|
|
||||||
#############################
|
|
||||||
# TODO
|
|
||||||
# Load hparams from .env file
|
|
||||||
|
|
||||||
MIN_TRADE = int(config('MIN_TRADE'))
|
MIN_TRADE = int(config('MIN_TRADE'))
|
||||||
MAX_TRADE = int(config('MAX_TRADE'))
|
MAX_TRADE = int(config('MAX_TRADE'))
|
||||||
FEE = float(config('FEE'))
|
FEE = float(config('FEE'))
|
||||||
BOND_SIZE = float(config('BOND_SIZE'))
|
BOND_SIZE = float(config('BOND_SIZE'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LNPayment(models.Model):
|
class LNPayment(models.Model):
|
||||||
|
|
||||||
class Types(models.IntegerChoices):
|
class Types(models.IntegerChoices):
|
||||||
@ -192,3 +188,48 @@ class Profile(models.Model):
|
|||||||
def avatar_tag(self):
|
def avatar_tag(self):
|
||||||
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
return mark_safe('<img src="%s" width="50" height="50" />' % 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}'
|
||||||
|
|
||||||
|
|
||||||
|
12
api/utils.py
Normal file
12
api/utils.py
Normal file
@ -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
|
34
api/views.py
34
api/views.py
@ -322,23 +322,27 @@ class UserView(APIView):
|
|||||||
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
|
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
|
||||||
context['found'] = 'Bad luck, this nickname is taken'
|
context['found'] = 'Bad luck, this nickname is taken'
|
||||||
context['bad_request'] = 'Enter a different token'
|
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):
|
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
|
# Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake
|
||||||
# However it might be a long time recovered user
|
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
|
||||||
# Only delete if user live is < 5 minutes
|
return Response(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# TODO check if user exists AND it is not a maker or taker!
|
# Check if it is not a maker or taker!
|
||||||
if user is not None:
|
if not Logics.validate_already_maker_or_taker(user):
|
||||||
logout(request)
|
return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
|
||||||
user.delete()
|
|
||||||
|
|
||||||
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):
|
class BookView(ListAPIView):
|
||||||
serializer_class = ListOrderSerializer
|
serializer_class = ListOrderSerializer
|
||||||
@ -367,14 +371,16 @@ class BookView(ListAPIView):
|
|||||||
|
|
||||||
class InfoView(ListAPIView):
|
class InfoView(ListAPIView):
|
||||||
|
|
||||||
def get(self, request, format = None):
|
def get(self):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
|
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_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
|
context['total_volume'] = None
|
||||||
|
|
||||||
return Response(context, status.HTTP_200_ok)
|
return Response(context, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user