@@ -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+
4584class 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
0 commit comments