Created
April 22, 2020 02:57
-
-
Save brotsky/bcad15117b1ebeb3169a76e4f25c6b66 to your computer and use it in GitHub Desktop.
Calculon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import json | |
import base64 | |
from decimal import * | |
from flask import Flask, jsonify, request | |
from datetime import date, datetime, timedelta | |
from dateutil import parser | |
import collections | |
app = Flask(__name__) | |
headers = { | |
'Access-Control-Allow-Origin': '*' | |
} | |
# Global Config | |
# TODO: move global constants to firebase once it is live | |
SHARING_MULTIPLIER = Decimal(0.01) # 0.01 per shar-199 | |
CASH_NEED_MULTIPLIER = Decimal(0.001) # 0.001 per Brandon & Steve discussion 20200412 🦆 | |
SHARING_MINIMUM_AMOUNT = 20 | |
SHARING_PERIOD_LENGTH = 28 | |
# Fees are a post-mvp feature as of 20200412 | |
# USER_FEES = {'Account Fee': 100, 'Other User Fee': 20} | |
def cache_sharing_amount(user_id, sharing_amount): | |
""" | |
# TODO: define cache location | |
""" | |
return False | |
def round_sharing_amount(sharing_amount): | |
""" | |
We use Bankers' Rounding or the "Round Half to Even" Rounding Strategy which is the rounding rule defined in https://en.wikipedia.org/wiki/IEEE_754#Rounding_rules | |
The first google result on this topic is helpful: https://realpython.com/python-rounding/ | |
"The “rounding half to even strategy” is the ... default rounding rule in the IEEE-754 standard." | |
also: "The default precision of the decimal module is twenty-eight digits..." | |
""" | |
# .quantize() uses decimal.ROUND_HALF_EVEN rounding strategy | |
return Decimal(sharing_amount).quantize(Decimal("1.00")) | |
def did_user_register_recently(registration_date): | |
""" | |
# TODO: | |
# - This is disabled until we can discuss where this belongs architecturally | |
# - This is part of a larger discussion around "Sharing Filters" | |
""" | |
today = date.today() | |
days_since_registration = today - registration_date | |
return days_since_registration | |
def get_next_positive_account(positive_accounts, current_positive_user_id): | |
""" | |
# grab next user id with a positive potential | |
""" | |
print('positive_accounts: ', positive_accounts) | |
for user_id in sorted(positive_accounts, key=positive_accounts.get, reverse=True): | |
print('positive_accounts[user_id]: ', positive_accounts[user_id]) | |
if positive_accounts[user_id] > 0: | |
return user_id | |
return False | |
def get_next_negative_account(negative_accounts, current_negative_user_id): | |
""" | |
# grab next user id with a negative need | |
""" | |
print('negative_accounts: ', negative_accounts) | |
last_user_id = 0 | |
for user_id in sorted(negative_accounts, key=negative_accounts.get, reverse=True): | |
print('negative_accounts[user_id]: ', negative_accounts[user_id]) | |
if abs(negative_accounts[user_id]) > 0: | |
return user_id | |
return False | |
def user_recently_registered(users, user_id): | |
""" | |
# TODO: | |
# - This is disabled until we can discuss where this belongs architecturally | |
# - This is part of a larger discussion around "Sharing Filters" | |
""" | |
half_sharing_period = SHARING_PERIOD_LENGTH / 2 | |
fourteen_days_ago = datetime.today() - timedelta(days=half_sharing_period) | |
for user in users: | |
if user['userId'] == user_id: | |
return parser.parse(user['registrationDate']) | |
time_since_registration = datetime.now() - registration_date | |
if time_since_registration > fourteen_days_ago: | |
return False | |
return True | |
def does_sharing_zero(transfers): | |
""" | |
# Make sure the transfers balance to 0 | |
""" | |
total_transfer_amount = 0 | |
for user_id, transfer_amount in transfers: | |
total_transfer_amount += Decimal(transfer_amount) | |
if total_transfer_amount != 0: | |
print('Unbalanced Distribution: Positive and negative transfer amounts must balance to 0. Transfer Balance: ', total_transfer_amount) | |
return False | |
return True | |
def is_transfer_above_minimum_threshold(positive_transfer_amount, negative_fill_need): | |
# sharing filter 1 - make sure all transfers are larger than the minimum sharing amount | |
# - When a user has less than the minimum allowed for a transfer they will owe to the community. | |
if positive_transfer_amount < SHARING_MINIMUM_AMOUNT and negative_fill_need < SHARING_MINIMUM_AMOUNT: | |
print('++cache++ transfer below SHARING_MINIMUM_AMOUNT: ', | |
positive_transfer_amount) | |
# TODO: cache this | |
# - where? | |
# - in the same place as global config (firebase) | |
return False | |
return True | |
# fees are a post-mvp feature as of 20200412 | |
# as of 20200421 there is still some question about this | |
# DJ_POWER_POINT #39: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7e5529e358_5_10 | |
# - F = AUM fee (0.25% - may vary by user if tiered and/or waived for small accounts) | |
# - if this is the True Spec then fees are defined here. 0.25% bb, you can calculate that!!! | |
# def get_total_fee(fees): | |
# total_fee = 0 | |
# for fee_type, fee in fees.items(): | |
# total_fee += fee | |
# return total_fee | |
def get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): | |
print('positive_accounts: ', positive_accounts) | |
print('positive_user_id: ', positive_user_id) | |
return positive_accounts[positive_user_id] | |
def get_remaining_negative_need(negative_accounts, negative_user_id): | |
return negative_accounts[negative_user_id] | |
@app.route('/') | |
def nope(): | |
return '✋' | |
@app.route('/determineSharingAmountAverage', methods=['POST']) | |
def determine_sharing_amount_average(): | |
""" Get the average annual sharing amount for all accounts | |
1. get total value of all accounts (total in Dwolla + total in brokerage accounts including stock value and cash) | |
2. get average of all accounts | |
3. return 1% of the average | |
`TOTAL_ACCOUNT_VALUE / NUMBER_OF_ACCOUNTS * 0.01` per shar-199 | |
This gives the yearly average. If we did monthly, we would divide by 12. | |
""" | |
total_account_value = request.get_json()['totalAccountValue'] | |
number_of_accounts = request.get_json()['numberOfAccounts'] | |
try: | |
total_account_value = Decimal(total_account_value) | |
number_of_accounts = Decimal(number_of_accounts) | |
except Exception as e: | |
print(e) | |
sharing_amount_average = (total_account_value / | |
number_of_accounts) * SHARING_MULTIPLIER | |
sharing_amount_average = round_sharing_amount(sharing_amount_average) | |
return {"sharingAmountAverage": str(sharing_amount_average)} | |
@app.route('/determineSharingAmountPerUser', methods=['POST']) | |
def determine_sharing_amount_per_user(): | |
""" | |
Get the sharing amount based on userAccountValue and cache by userID | |
`USER_ACCOUNT_VALUE * 0.01` per shar-202 | |
This should be cached in the Database | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#40: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7e5529e358_5_10 | |
# TODO: troubleshoot the difference in these specs | |
Update per-user accruals after daily data update | |
Variables used below: | |
UAV = user account value | |
D = days in current year (365 or 366) | |
W = withdrawal allowance (5.2%) | |
F = AUM fee (0.25% - may vary by user if tiered and/or waived for small accounts) | |
“Sharing payable” accrual: UAV * 1% / D | |
“Fees payable” accrual: UAV * F / D | |
“Effective net deposits” withdrawal allowance accrual: UAV * W / D | |
“Active days” - increase active days for account by 1 | |
Used in allocation of the 1% of sharing | |
Alternatively - could allocate the 1% daily | |
""" | |
user_id = request.get_json()['userID'] | |
user_account_value = request.get_json()['userAccountValue'] | |
try: | |
user_account_value = Decimal(user_account_value) | |
except Exception as e: | |
print(e) | |
user_sharing_amount = user_account_value * SHARING_MULTIPLIER | |
user_sharing_amount = round_sharing_amount(user_sharing_amount) | |
cache_sharing_amount(user_id, user_sharing_amount) | |
return {"userSharingAmount": str(user_sharing_amount)} | |
@app.route('/determineNetSharingAmountPerUser', methods=['POST']) | |
def determine_net_sharing_amount_per_user(): | |
""" Get the net sharing amount based on userSharingAmount and communitySharingAmount | |
`USER_NET_SHARING_AMOUNT = USER_SHARING_AMOUNT - COMMUNITY_SHARING_AMOUNT` per shar-203 | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#40: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7e5529e358_5_10 | |
# TODO: troubleshoot the difference in these specs | |
Update per-user accruals after daily data update | |
Variables used below: | |
UAV = user account value | |
D = days in current year (365 or 366) | |
W = withdrawal allowance (5.2%) | |
F = AUM fee (0.25% - may vary by user if tiered and/or waived for small accounts) | |
“Sharing payable” accrual: UAV * 1% / D | |
“Fees payable” accrual: UAV * F / D | |
“Effective net deposits” withdrawal allowance accrual: UAV * W / D | |
“Active days” - increase active days for account by 1 | |
Used in allocation of the 1% of sharing | |
Alternatively - could allocate the 1% daily | |
""" | |
user_sharing_amount = request.get_json()['userSharingAmount'] | |
community_sharing_amount = request.get_json()['communitySharingAmount'] | |
try: | |
user_sharing_amount = Decimal(user_sharing_amount) | |
community_sharing_amount = Decimal(community_sharing_amount) | |
except Exception as e: | |
print(e) | |
user_net_sharing_amount = user_sharing_amount - community_sharing_amount | |
user_net_sharing_amount = round_sharing_amount(user_net_sharing_amount) | |
return {"userSharingAmount": str(user_net_sharing_amount)} | |
@app.route('/matchUsersForTransfers', methods=['POST']) | |
def match_users_for_transfers(): | |
""" | |
Match Users For Transfers | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#42: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7e5529e358_5_15 | |
# TODO: troubleshoot the difference in these specs | |
Calculate sharing pool to be allocated | |
Pool = sum of “sharing payable” - “sharing receivable” across all accounts in the system | |
NOTE: Always ensure that calculations preserve money | |
Don’t lose small bits though rounding or careless calculations | |
Divide total pool into two components (1% and 99%): | |
C = 1% | |
S = 99% | |
Allocate the 1% equally to all active accounts | |
AD_i = “active days” in current period for account i | |
AD = sum of AD_i over i = total “active days” in current period across all accounts | |
C_d = C / AD = account-day allocation of C | |
C_i = C_d * AD_i = allocation of C to account i | |
Increase “sharing receivable” of account i by C_i | |
""" | |
orders = [] | |
user_transfers = {} | |
users = request.get_json()['users'] | |
print(user_transfers) | |
for user in users: | |
user_id = user['userId'] | |
transfer_amount = user['balanceDetails']['transferAmount'] | |
# TODO: | |
# - This is disabled until we can discuss where this belongs architecturally | |
# - This is part of a larger discussion around "Sharing Filters" | |
# if not user_recently_registered(users, user_id): | |
user_transfers.update({user_id: transfer_amount}) | |
print('user_transfers: ', user_transfers) | |
print('user_transfers type: ', type(user_transfers)) | |
print('user_transfers.items() type: ', type(user_transfers.items())) | |
# Make sure the transfers balance to 0 | |
if not does_sharing_zero(user_transfers.items()): | |
return {"exception": "Unbalanced Distribution: Positive and negative transfer amounts must balance to 0."} | |
# TODO: If Calculon can always count on well ordered input then this isn't needed | |
# - but if well ordered input can't be expected then implement this sort | |
# fastest dict sort by values proposed in PEP 265 .. this is the path to optimization when needed | |
# - source: https://writeonly.wordpress.com/2008/08/30/sorting-dictionaries-by-value-in-python-improved/ | |
# user_transfers = sorted(user_transfers, key=itemgetter(1), reverse=True) | |
# sorted_dict = collections.OrderedDict(sorted_x) | |
positive_accounts = {} | |
negative_accounts = {} | |
try: | |
for user_id, transfer_amount in user_transfers.items(): | |
if Decimal(transfer_amount) > 0: | |
positive_accounts[user_id] = abs(Decimal(transfer_amount)) | |
elif Decimal(transfer_amount) < 0: | |
negative_accounts[user_id] = abs(Decimal(transfer_amount)) | |
positive_user_id = 0 | |
negative_user_id = 0 | |
negative_user_id = get_next_negative_account( | |
negative_accounts, negative_user_id) | |
print('get_next_positive_account(positive_accounts, positive_user_id): ', | |
get_next_positive_account(positive_accounts, positive_user_id)) | |
while get_next_positive_account(positive_accounts, positive_user_id): | |
positive_user_id = get_next_positive_account( | |
positive_accounts, positive_user_id) | |
print('positive_user_id: ', positive_user_id) | |
print('get_remaining_negative_need(negative_accounts, negative_user_id): ', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
print('get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): ', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
# sharing filters | |
# sharing filter 1 - make sure all transfers are larger than the minimum sharing amount | |
# - When a user has less than the minimum allowed for a transfer they will owe to the community. | |
if get_remaining_positive_transfer_amount(positive_accounts, positive_user_id) < SHARING_MINIMUM_AMOUNT or get_remaining_negative_need(negative_accounts, negative_user_id) < SHARING_MINIMUM_AMOUNT: | |
print('++cache++ transfer below SHARING_MINIMUM_AMOUNT: ', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
print('get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
print('get_remaining_negative_need(negative_accounts, negative_user_id)', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
# TODO: cache this once the cache is defined | |
orders.append('cache ${:0,.2f} from {} to {}'.format( | |
abs(get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)), positive_user_id, negative_user_id)) | |
break | |
# Sharing Filter 2 - Skip people that registered recently | |
# if user_recently_registered(users, user_id): | |
# print('++cache++ time since registration is less than min amount') | |
# # TODO: how do we balance the sharing equation if they are part of the initial balancing | |
# break | |
while get_remaining_negative_need(negative_accounts, negative_user_id) > 0 and get_remaining_positive_transfer_amount(positive_accounts, positive_user_id) > 0: | |
# is there enough in the first positive account to fill the first negative account ? | |
if get_remaining_negative_need(negative_accounts, negative_user_id) <= get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): | |
# execute transfer | |
positive_accounts[positive_user_id] = get_remaining_positive_transfer_amount( | |
positive_accounts, positive_user_id) - get_remaining_negative_need(negative_accounts, negative_user_id) | |
# move the full available negative_fill_need | |
print('move ${} from {} to {}'.format( | |
abs(get_remaining_negative_need(negative_accounts, negative_user_id)), positive_user_id, negative_user_id)) | |
orders.append('move ${:0,.2f} from {} to {}'.format( | |
abs(get_remaining_negative_need(negative_accounts, negative_user_id)), positive_user_id, negative_user_id)) | |
# finalize transfer execution by zeroing the transferred balance | |
negative_accounts[negative_user_id] = 0 | |
# get next negative fill need | |
if get_next_negative_account(negative_accounts, negative_user_id): | |
negative_user_id = get_next_negative_account( | |
negative_accounts, negative_user_id) | |
negative_accounts[negative_user_id] = negative_accounts[negative_user_id] | |
# not enough in the first positive account to fill the first negative account | |
# execute partial transfer!!! | |
elif get_remaining_positive_transfer_amount(positive_accounts, positive_user_id) > 0: | |
print('about to execute partial get_remaining_negative_need(negative_accounts, negative_user_id): ', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
print('with partial get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): ', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
negative_accounts[negative_user_id] = get_remaining_negative_need( | |
negative_accounts, negative_user_id) - get_remaining_positive_transfer_amount(positive_accounts, positive_user_id) | |
print('executed partial get_remaining_negative_need(negative_accounts, negative_user_id): ', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
print('executed partial get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): ', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
# TODO: | |
# should we cache partial transfers < SHARING_MINIMUM_AMOUNT | |
# - This is disabled until we can discuss where this belongs architecturally | |
if abs(get_remaining_negative_need(negative_accounts, negative_user_id)) < SHARING_MINIMUM_AMOUNT: | |
# move the partial negative_transfer_amount | |
print( | |
'partial ++cache++ transfer below SHARING_MINIMUM_AMOUNT: ') | |
print('partial get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
print('partial get_remaining_negative_need(negative_accounts, negative_user_id)', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
orders.append('partial transfer below cache threshold ${:0,.2f} from {} to {}'.format( | |
abs(get_remaining_negative_need(negative_accounts, negative_user_id)), positive_user_id, negative_user_id)) | |
positive_accounts[positive_user_id] = get_remaining_positive_transfer_amount( | |
positive_accounts, positive_user_id) - abs(get_remaining_negative_need(negative_accounts, negative_user_id)) | |
else: | |
# move the full available positive_transfer_amount | |
orders.append('partial move ${:0,.2f} from {} to {}'.format( | |
abs(get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)), positive_user_id, negative_user_id)) | |
# finalize transfer execution by zeroing the transferred balance | |
positive_accounts[positive_user_id] = 0 | |
print('get_remaining_positive_transfer_amount(positive_accounts, positive_user_id): ', | |
get_remaining_positive_transfer_amount(positive_accounts, positive_user_id)) | |
print('get_remaining_negative_need(negative_accounts, negative_user_id): ', | |
get_remaining_negative_need(negative_accounts, negative_user_id)) | |
print('positive_accounts: ', positive_accounts) | |
print('negative_accounts: ', negative_accounts) | |
except Exception as e: | |
print(e) | |
raise | |
return {"orders": orders} | |
def estimate_sharing_payable_cash_need(total_account_value, number_of_accounts): | |
""" | |
- Sharing Payable is the sum of .1% of the **average** account value of each person in sharing | |
- and .1% per week for the total community amount | |
TODO: what is the difference between the total account value of each person and the total community amount? | |
Aren't those the same thing? | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#39: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7db0d288e7_0_102 | |
# TODO: troubleshoot the difference in these specs | |
Used to help maintain minimal but sufficient cash to cover expected flows over the near term | |
Estimate cash needs over the next time interval (3 months) | |
Estimate is a total of the following items estimated for the time period: | |
Estimated fees over time period | |
Estimated sharing payable | |
Withdrawals scheduled and pending | |
""" | |
average_user_account_value = ( | |
Decimal(total_account_value) / int(number_of_accounts)) | |
sum_of_average_account_values = Decimal( | |
average_user_account_value) * int(number_of_accounts) | |
reserve_for_sharing_users_amount = Decimal( | |
sum_of_average_account_values) * Decimal(CASH_NEED_MULTIPLIER) | |
print('reserve_for_sharing_users_amount: ', | |
reserve_for_sharing_users_amount) | |
reserve_for_community_amount = Decimal( | |
total_account_value) * Decimal(CASH_NEED_MULTIPLIER) | |
print('reserve_for_community_amount: ', reserve_for_community_amount) | |
return round_sharing_amount(reserve_for_sharing_users_amount + reserve_for_community_amount) | |
def estimate_withdrawal_cash_need(total_account_value): | |
""" | |
- The Withdrawal Estimate is based on .1% per week if it's weekly, 5.2% per year annually | |
- What is the difference between this and reserve_for_community_amount above? | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#39: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7db0d288e7_0_102 | |
# TODO: troubleshoot the difference in these specs | |
Used to help maintain minimal but sufficient cash to cover expected flows over the near term | |
Estimate cash needs over the next time interval (3 months) | |
Estimate is a total of the following items estimated for the time period: | |
Estimated fees over time period | |
Estimated sharing payable | |
Withdrawals scheduled and pending | |
""" | |
reserve_for_sharing_users_amount = Decimal( | |
total_account_value) * Decimal(CASH_NEED_MULTIPLIER) | |
return round_sharing_amount(reserve_for_sharing_users_amount) | |
@app.route('/estimateCashNeed', methods=['POST']) | |
def estimate_cash_need(): | |
""" A helper function to determine cash need for each individual customer on the system. | |
Used to help maintain minimal but sufficient cash to cover expected flows over the near term | |
Determine a time period to estimate cash need (~ 3 months?) | |
Estimate is a total of the following items estimated for the time period: | |
- Estimated fees over time period | |
- Estimated sharing payable | |
- Withdrawals scheduled and pending | |
shar-443 | |
--- | |
# SPEC COLLISION WARNING!!! | |
this appears to also be defined in DJ_POWER_POINT#39: https://docs.google.com/presentation/d/15avMjQcwcBAqN1H_u-q6zwxBaAjvF9Vpcn3K_w1ZJQ4/edit#slide=id.g7db0d288e7_0_102 | |
# TODO: troubleshoot the difference in these specs | |
Used to help maintain minimal but sufficient cash to cover expected flows over the near term | |
Estimate cash needs over the next time interval (3 months) | |
Estimate is a total of the following items estimated for the time period: | |
Estimated fees over time period | |
Estimated sharing payable | |
Withdrawals scheduled and pending | |
""" | |
total_account_value = request.get_json()['totalAccountValue'] | |
number_of_accounts = request.get_json()['numberOfAccounts'] | |
# old, silly, attempt .. delete this after I vreify that I'm doing this right | |
# average_account_value_per_user = (Decimal(total_account_value) / int(number_of_accounts)) | |
# estimated_cash_need_per_user_per_week = round_sharing_amount(Decimal(average_account_value_per_user) * Decimal(0.001)) | |
# # this is relevant per brandon's notes .. why is this relevant? | |
# point_one_percent_of_total_community_amount = round_sharing_amount(Decimal(total_account_value) * Decimal(0.001)) | |
# # estimated_cash_need = sharing_amount_per_user + point_one_percent_of_total_community_amount | |
sharing_payable_cash_need = estimate_sharing_payable_cash_need( | |
total_account_value, number_of_accounts) | |
withdrawals_cash_need = estimate_withdrawal_cash_need(total_account_value) | |
estimated_cash_need_per_week = sharing_payable_cash_need + withdrawals_cash_need | |
return {"estimatedCashNeedPerWeek": str(estimated_cash_need_per_week)} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment