Skip to content
This repository was archived by the owner on Jun 30, 2021. It is now read-only.

Commit 87dffb3

Browse files
committed
WIP. Almost complete adaptation to new splitting/trade management strategy
1 parent d4a1bbc commit 87dffb3

File tree

3 files changed

+131
-88
lines changed

3 files changed

+131
-88
lines changed

simplecoin/models.py

Lines changed: 115 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,60 @@ def make_upper_lower(trim=None, span=None, offset=None, clip=None, fmt="dt"):
4242
return lower, upper
4343

4444

45+
class TradeResult(base):
46+
""" Represents the results of a single trade update from our trading
47+
backend. When an update to a TradeRequest gets posted a new trade result
48+
will get created to record associated trade information. The results of the
49+
trade are distributed among it's credits. Creation is handled by
50+
TradeRequest.update """
51+
id = db.Column(db.Integer, primary_key=True)
52+
# Quantity of currency to be traded
53+
quantity = db.Column(db.Numeric, nullable=False)
54+
exchanged_quantity = db.Column(db.Numeric, default=None)
55+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
56+
57+
req_id = db.Column(db.Integer, db.ForeignKey('trade_request.id'))
58+
req = db.relationship('TradeRequest', foreign_keys=[req_id],
59+
backref='results')
60+
61+
def distribute(self, payable_amount):
62+
# calculate user payouts based on percentage of the total
63+
# exchanged value
64+
if self.type == "sell":
65+
portions = {c.id: c.amount for c in credits}
66+
elif self.type == "buy":
67+
portions = {c.id: c.sell_amount for c in credits
68+
if c.payable is False}
69+
70+
amts_copy = portions.copy()
71+
amounts = distributor(payable_amount, amts_copy, scale=50)
72+
73+
for credit in credits:
74+
if self.type == "sell":
75+
assert credit.sell_amount is None
76+
credit.sell_amount = amounts[credit.id]
77+
elif self.type == "buy":
78+
if credit.payable is False:
79+
credit.buy_amount = amounts[credit.id]
80+
# Mark the credit ready for payout to users
81+
credit.payable = True
82+
83+
4584
class TradeRequest(base):
4685
"""
4786
Used to provide info necessary to external applications for trading currencies
4887
4988
Created rows will be checked + updated externally
5089
"""
5190
id = db.Column(db.Integer, primary_key=True)
52-
# 3-8 letter code for the currency to be traded
91+
# 3-8 letter code for the currency to be traded. This is the currency to
92+
# buy for a buy, and the currency we're selling for a sale
5393
currency = db.Column(db.String, nullable=False)
5494
# Quantity of currency to be traded
5595
quantity = db.Column(db.Numeric, nullable=False)
96+
quantity_traded = db.Column(db.Numeric, nullable=False, default=0)
5697
created_at = db.Column(db.DateTime, default=datetime.utcnow)
5798
type = db.Column(db.Enum("sell", "buy", name="req_type"), nullable=False)
58-
59-
# The quantity of the desired currency received by fulfilling this request
60-
exchanged_quantity = db.Column(db.Numeric, default=None)
61-
# Fees from fulfilling this tr (represented in Currency, not BTC)
62-
fees = db.Column(db.Numeric, default=None)
6399
_status = db.Column(db.SmallInteger, default=0)
64100

65101
@property
@@ -69,81 +105,81 @@ def avg_price(self):
69105
elif self.type == "sell":
70106
return (self.exchanged_quantity + self.fees) / self.quantity
71107

72-
def distribute(self, stuck_quantity, applied_fees):
73-
assert self.type in ["buy", "sell"], "Invalid type!"
74-
assert self.exchanged_quantity > 0
75-
108+
def update(self, quantity, source_quantity, fees):
76109
credits = self.credits # Do caching here, avoid multiple lookups
77110
if not credits:
78111
current_app.logger.warn("Trade request #{} has no attached credits"
79112
.format(self.id))
80113
return
81114

82-
# Check config to see if we're charging exchange fees or not
83-
payable_amount = self.exchanged_quantity - stuck_quantity
84-
85-
# If we're covering the exchange fees we'll need to add them in
86-
if current_app.config.get('cover_autoex_fees', False):
87-
payable_amount += applied_fees
88-
89-
# Remove previously paid amounts from the payable amount
90-
if self.type == "buy":
91-
with decimal.localcontext(decimal.BasicContext) as ctx:
92-
ctx.traps[decimal.Inexact] = True
93-
ctx.prec = 100
94-
95-
payable_amount -= sum([credit.buy_amount for credit in credits
96-
if credit.payable is True])
97-
98-
# calculate user payouts based on percentage of the total
99-
# exchanged value
100-
if self.type == "sell":
101-
portions = {c.id: c.amount for c in credits}
102-
elif self.type == "buy":
103-
portions = {c.id: c.sell_amount for c in credits
104-
if c.payable is False}
105-
106-
amts_copy = portions.copy()
107-
amounts = distributor(payable_amount, amts_copy, scale=50)
108-
115+
# Get the amount of source currency that hasn't been distributed to
116+
# credits
117+
total_unpaid = 0
118+
unpaid_credits = []
109119
for credit in credits:
110-
if self.type == "sell":
111-
assert credit.sell_amount is None
112-
credit.sell_amount = amounts[credit.id]
113-
elif self.type == "buy":
114-
if credit.payable is False:
115-
credit.buy_amount = amounts[credit.id]
116-
# Mark the credit ready for payout to users
117-
credit.payable = True
120+
# We need to skip credits that are already attached to a result
121+
if credit.trade_result:
122+
continue
118123

119-
# If its an update redistribute + create new credits
120-
if self._status == 5:
121-
assert stuck_quantity > 0
124+
unpaid_credits.append(credit)
122125

123-
# Build a dict containing each credit id + amount
124-
credit_amts = {credit.id: credit.amount for credit in credits}
125-
126-
# Get the distribution for the stuck amount
127-
amts_copy = credit_amts.copy()
128-
curr_distrib = distributor(stuck_quantity, amts_copy)
126+
if self.type == "sell":
127+
total_unpaid += credit.amount
128+
else:
129+
total_unpaid += credit.sell_amount
130+
131+
source_total = 0
132+
destination_total = 0
133+
fee_total = 0
134+
for result in self.trade_results:
135+
source_total += result.quantity
136+
destination_total += result.exchanged_quantity
137+
fee_total += result.fees
138+
139+
if quantity <= destination_total or source_quantity <= source_total:
140+
current_app.logger.warn(
141+
"Nothing to update, quantity and source_quantity have not changed")
142+
return
129143

130-
# Calculate the BTC going to the old credits and new credits
131-
amts = {'old': payable_amount + self.fees, 'new': stuck_quantity}
132-
amts_copy = amts.copy()
133-
btc_distrib = distributor(self.quantity, amts_copy)
144+
new_tr = TradeResult(quantity=source_quantity - source_total,
145+
exchanged_quantity=quantity - destination_total,
146+
fees=fees - fee_total)
147+
db.session.add(new_tr)
148+
149+
distribute_amount = new_tr.exchanged_quantity
150+
# If we're not covering exchange fees, remove them from the amount
151+
# we distribute
152+
if not current_app.config.get('cover_autoex_fees', False):
153+
distribute_amount -= new_tr.fees
154+
155+
# If the upaid credits sum up to more than the amount of the
156+
# TradeResult then we're going to have to split the credit objects so
157+
# we can perform a partial credit
158+
if total_unpaid > new_tr.exchanged_quantity:
159+
credit_amts = {credit.id: credit.amount for credit in unpaid_credits}
160+
# Distributing the amount that we'll be paying
161+
curr_distrib = distributor(distribute_amount, credit_amts)
162+
amount_distrib = distributor(new_tr.quantity, credit_amts)
163+
164+
# Split a few values evenly based on the ratio between stuck amount
165+
# and payable amount
166+
amts = {'traded': payable_amount + self.fees,
167+
'remaining': stuck_quantity}
168+
if self.type == "sell":
169+
btc_distrib = distributor(self.quantity, amts.copy())
134170

135-
# Calculate the BTC distribution, based on the stuck currency amounts
136-
amts_copy = credit_amts.copy()
137-
new_btc_distrib = distributor(btc_distrib['new'], amts_copy)
171+
# Calculate the BTC distribution, based on the stuck currency
172+
# amounts
173+
new_btc_distrib = distributor(btc_distrib['new'], amts.copy())
138174

139175
with decimal.localcontext(decimal.BasicContext) as ctx:
140-
ctx.traps[decimal.Inexact] = True
176+
# A sufficiently large number that we don't cause rounding
177+
# error
141178
ctx.prec = 50
142179

143180
i = 0
144-
orig_len = len(credits)
145181
# Loop + add a new credit for each old credit
146-
while i < orig_len:
182+
while i < len(credits):
147183
credit = credits[i]
148184

149185
# subtract the credits cut from the old credit's amount
@@ -174,15 +210,17 @@ def distribute(self, stuck_quantity, applied_fees):
174210
"amount {:,} payable and {:,} stuck.".
175211
format(self.id, payable_amount, stuck_quantity))
176212

177-
db.session.flush()
213+
if source_quantity == self.quantity:
214+
self._status = 6
215+
else:
216+
self._status = 5
178217

179-
if self._status == 6:
180-
assert stuck_quantity == 0
181-
current_app.logger.info(
182-
"Successfully pushed trade result for request id {:,} and "
183-
"amount {:,} to {:,} credits.".
184-
format(self.id, self.exchanged_quantity, len(credits)))
218+
db.session.flush()
185219

220+
current_app.logger.info(
221+
"Successfully pushed trade result for request id {:,} and "
222+
"amount {:,} to {:,} credits.".
223+
format(self.id, self.exchanged_quantity, len(credits)))
186224

187225
@property
188226
def credits(self):
@@ -521,10 +559,16 @@ class CreditExchange(Credit):
521559
sell_req_id = db.Column(db.Integer, db.ForeignKey('trade_request.id'))
522560
sell_req = db.relationship('TradeRequest', foreign_keys=[sell_req_id],
523561
backref='sell_credits')
562+
sell_res_id = db.Column(db.Integer, db.ForeignKey('trade_result.id'))
563+
sell_res = db.relationship('TradeResult', foreign_keys=[buy_req_id],
564+
backref='sell_credits')
524565
sell_amount = db.Column(db.Numeric)
525566
buy_req_id = db.Column(db.Integer, db.ForeignKey('trade_request.id'))
526567
buy_req = db.relationship('TradeRequest', foreign_keys=[buy_req_id],
527568
backref='buy_credits')
569+
buy_res_id = db.Column(db.Integer, db.ForeignKey('trade_result.id'))
570+
buy_res = db.relationship('TradeResult', foreign_keys=[buy_req_id],
571+
backref='buy_credits')
528572
buy_amount = db.Column(db.Numeric)
529573

530574
@property

simplecoin/rpc_views.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def update_trade_requests():
6666
assert 'status' in tr
6767
g.signed['trs'][tr_id]['status'] = int(tr['status'])
6868
if tr['status'] == 5 or tr['status'] == 6:
69-
tr['stuck_quantity'] = Decimal(tr['stuck_quantity'])
69+
tr['source_quantity'] = Decimal(tr['source_quantity'])
7070
tr['quantity'] = Decimal(tr['quantity'])
7171
tr['fees'] = Decimal(tr['fees'])
7272
except (AssertionError, TypeError, KeyError):
@@ -84,13 +84,11 @@ def update_trade_requests():
8484

8585
if status == 5 or status == 6:
8686
tr.exchanged_quantity = tr_dict['quantity'] + tr_dict['stuck_quantity']
87-
if tr.fees:
88-
applied_fees = tr_dict['fees'] - tr.fees
89-
else:
90-
applied_fees = tr_dict['fees']
9187
tr.fees = tr_dict['fees']
92-
stuck_quantity = tr_dict['stuck_quantity']
93-
tr.distribute(stuck_quantity, applied_fees)
88+
# Get the amount of fees that we incurred during this trade
89+
# update
90+
applied_fees = tr_dict['fees'] - (tr.fees or 0)
91+
tr.distribute(tr_dict['stuck_quantity'], applied_fees)
9492

9593
except Exception:
9694
db.session.rollback()

simplecoin/tests/test_payout.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def test_update_tr_buy(self):
270270
"stuck_quantity": str(posted_stuck_amt)}}}
271271
db.session.expunge_all()
272272

273-
with self.app.test_request_context('/?name=Peter'):
273+
with self.app.test_request_context():
274274
flask.g.signer = TimedSerializer(self.app.config['rpc_signature'])
275275
flask.g.signed = push_data
276276
update_trade_requests()
@@ -312,15 +312,16 @@ def test_update_tr_buy(self):
312312
# Check that new credit attrs are the same as old (fees, etc)
313313
for credit in credits2:
314314
if credit.payable is False:
315-
assert credits2[credit.id - 20].fee_perc == credit.fee_perc
316-
assert credits2[credit.id - 20].pd_perc == credit.pd_perc
317-
assert credits2[credit.id - 20].block == credit.block
318-
assert credits2[credit.id - 20].user == credit.user
319-
assert credits2[credit.id - 20].sharechain_id == credit.sharechain_id
320-
assert credits2[credit.id - 20].address == credit.address
321-
assert credits2[credit.id - 20].currency == credit.currency
322-
assert credits2[credit.id - 20].type == credit.type
323-
assert credits2[credit.id - 20].payout == credit.payout
315+
c2 = credits2[credit.id - 20]
316+
assert c2.fee_perc == credit.fee_perc
317+
assert c2.pd_perc == credit.pd_perc
318+
assert c2.block == credit.block
319+
assert c2.user == credit.user
320+
assert c2.sharechain_id == credit.sharechain_id
321+
assert c2.address == credit.address
322+
assert c2.currency == credit.currency
323+
assert c2.type == credit.type
324+
assert c2.payout == credit.payout
324325

325326
# Assert that the status is 5, partially completed
326327
assert tr2._status == 5

0 commit comments

Comments
 (0)