def _validate(self) -> None: if self.acct_type not in VALID_ACCT_TYPES: raise ValueError('What is this account type? {}\nMust be checking or credit' .format(self.acct_type)) if self.acct_type == CREDIT: self.balance = self.balance * (-1.0) if 28 > int(self.payback_date) > 0: self.payback_date = int(self.payback_date) self.credit_limit = Q_(float(self.credit_limit.replace('$', '')), 'usd')
def process_tx(self, amount_extractable_obj: Union[AccountInterface, Transaction]) -> None: """ figure out which attribute the amount is stored in if its a transaction object use the amount attribute if its an account object then use the balance attribute """ if hasattr(amount_extractable_obj, 'amount'): amount = amount_extractable_obj.amount elif hasattr(amount_extractable_obj, 'balance'): amount = amount_extractable_obj.balance self.balance += amount if self.acct_type == CREDIT: bal_credit_ratio = round(abs(self.balance / self.credit_limit) * 100., 1) if bal_credit_ratio > 20: logger.info("{}\nbe careful, you're debt/limit ratio is {}%\n\ anything over 20% may hurt your credit score." .format(self, bal_credit_ratio)) elif self.acct_type == CHECKING: if self.balance < Q_(0, 'usd'): logger.info("{} has just overdrafted.".format(self)) # check if balance is an attribute, and update it if hasattr(amount_extractable_obj, "balance"): # don't just set to 0.0 because future functionality might pay a fraction of balance amount_extractable_obj.balance -= amount logger.debug("credit account {} was paid off". format(amount_extractable_obj))
def process_tx(self, amount_extractable_obj: Union[AccountInterface, Transaction], simulated_day: datetime) -> None: """ figure out which attribute the amount is stored in if its a transaction object use the amount attribute if its an account object then use the balance attribute """ if hasattr(amount_extractable_obj, 'amount'): amount = amount_extractable_obj.amount elif hasattr(amount_extractable_obj, 'balance'): amount = amount_extractable_obj.balance self.balance += amount if self.acct_type == CREDIT: bal_credit_ratio = round( abs(self.balance / self.credit_limit) * 100., 1) if bal_credit_ratio > 25: logger.info( f"{self.name}\nbe careful, you're debt/limit ratio is {bal_credit_ratio}%\n\ anything over 25% may hurt your credit score.") issue = { "ISSUE": DEBT_RATIO, "DATE": simulated_day, "VALUE": bal_credit_ratio } self.issues.append(issue) if abs(self.balance) > self.credit_limit: logger.info( f"You've spent more than your credit limit for {self.name}" ) issue = { "ISSUE": OVERCREDIT, "DATE": simulated_day, "VALUE": self.balance } self.issues.append(issue) elif self.acct_type == CHECKING: if self.balance < Q_(0, 'usd'): logger.info(f"{self} has just overdrafted.") issue = { "ISSUE": OVERDRAFT, "DATE": simulated_day, "VALUE": self.balance } self.issues.append(issue) # check if balance is an attribute, and update it if hasattr(amount_extractable_obj, "balance"): # don't just set to 0.0 because future functionality might pay a fraction of balance amount_extractable_obj.balance -= amount logger.debug("credit account {} was paid off".format( amount_extractable_obj))
def _parse_attributes(self) -> None: self.amount = Q_(float(self.amount.replace('$', '')), 'usd') if self.transaction_type.lower() == 'deduction': self.amount = self.amount * (-1.0) elif self.transaction_type.lower() == 'payment': pass self.frequency = Q_( self.frequency.replace('d', 'day').replace('w', 'week').replace( ' ', '+')).to('week') self.sample_date = parser.parse(self.sample_date) try: self.until_date = parser.parse(self.until_date) except TypeError: self.until_date = FOREVER_RECURRING
def _parse_attributes(self) -> None: self.amount = Q_(float(self.amount.replace('$', '')), 'usd') if self.transaction_type.lower() == 'deduction': self.amount = self.amount * (-1.0) elif self.transaction_type.lower() == 'payment': pass try: self.frequency = Q_( self.frequency.replace('d', 'day').replace('w', 'week').replace( ' ', '+')).to('week') except ValueError: logger.info( f'received frequency of {self.frequency} assuming units of days' ) self.frequency = Q_(f'{self.frequency} days') try: self.sample_date = parser.parse(self.sample_date) except ValueError as e: logger.error(f'{self.sample_date} is not a valid date') raise e try: self.until_date = parser.parse(self.until_date) except (TypeError, ValueError): self.until_date = FOREVER_RECURRING
def __init__(self, name: str, bal: str, acct_type: str, payback_date: Optional[float64] = None, payback_src: Optional[Union[str, float]]=None, credit_limit: Optional[Union[str, float]]=None) -> None: self.name = name self.balance = Q_(float(bal.replace('$', '')), 'usd') self.acct_type = acct_type.upper() self.payback_date = payback_date self.payback_src = payback_src self.credit_limit = credit_limit self._validate()
class Account(AccountInterface): def __init__(self, name: str, bal: str, acct_type: str, payback_date: Optional[float64] = None, payback_src: Optional[Union[str, float]]=None, credit_limit: Optional[Union[str, float]]=None) -> None: self.name = name self.balance = Q_(float(bal.replace('$', '')), 'usd') self.acct_type = acct_type.upper() self.payback_date = payback_date self.payback_src = payback_src self.credit_limit = credit_limit self._validate() def __repr__(self) -> str: return "{}: {}".format(self.name, self.balance) def _validate(self) -> None: if self.acct_type not in VALID_ACCT_TYPES: raise ValueError('What is this account type? {}\nMust be checking or credit' .format(self.acct_type)) if self.acct_type == CREDIT: self.balance = self.balance * (-1.0) if 28 > int(self.payback_date) > 0: self.payback_date = int(self.payback_date) self.credit_limit = Q_(float(self.credit_limit.replace('$', '')), 'usd') def process_tx(self, amount_extractable_obj: Union[AccountInterface, Transaction]) -> None: """ figure out which attribute the amount is stored in if its a transaction object use the amount attribute if its an account object then use the balance attribute """ if hasattr(amount_extractable_obj, 'amount'): amount = amount_extractable_obj.amount elif hasattr(amount_extractable_obj, 'balance'): amount = amount_extractable_obj.balance self.balance += amount if self.acct_type == CREDIT: bal_credit_ratio = round(abs(self.balance / self.credit_limit) * 100., 1) if bal_credit_ratio > 20: logger.info("{}\nbe careful, you're debt/limit ratio is {}%\n\ anything over 20% may hurt your credit score." .format(self, bal_credit_ratio)) elif self.acct_type == CHECKING: if self.balance < Q_(0, 'usd'): logger.info("{} has just overdrafted.".format(self)) # check if balance is an attribute, and update it if hasattr(amount_extractable_obj, "balance"): # don't just set to 0.0 because future functionality might pay a fraction of balance amount_extractable_obj.balance -= amount logger.debug("credit account {} was paid off". format(amount_extractable_obj)) def payoff_credit_acct(self, account_object: AccountInterface) -> None: """ modify the account_object by paying off its balance """ if account_object.acct_type == CREDIT: if self.acct_type == CHECKING: self.process_tx(account_object) else: logger.warning("Need to payoff_credt_acct with a checking account.\ Skipping this operation.") return else: logger.warning("Cannot payoff_credit_acct with {} type acct.\n\ skipping this operation." .format(account_object.acct_type)) return
class Transaction(object): def __init__(self, f: str, a: str, t: str, d: str, sd: str, sc: str, u: Union[str, float]) -> None: # change to have the units recognized by pint and the + as a mathematical operation self.frequency = f self.amount = a self.transaction_type = t self.description = d self.sample_date = sd self.source = sc.strip().upper() self.until_date = u self._parse_attributes() def __repr__(self): return self.description def _parse_attributes(self) -> None: self.amount = Q_(float(self.amount.replace('$', '')), 'usd') if self.transaction_type.lower() == 'deduction': self.amount = self.amount * (-1.0) elif self.transaction_type.lower() == 'payment': pass try: self.frequency = Q_( self.frequency.replace('d', 'day').replace('w', 'week').replace( ' ', '+')).to('week') except ValueError: logger.info( f'received frequency of {self.frequency} assuming units of days' ) self.frequency = Q_(f'{self.frequency} days') try: self.sample_date = parser.parse(self.sample_date) except ValueError as e: logger.error(f'{self.sample_date} is not a valid date') raise e try: self.until_date = parser.parse(self.until_date) except (TypeError, ValueError): self.until_date = FOREVER_RECURRING def should_payment_occur_today(self, datetime_object: datetime, check_cycles: int = 1) -> bool: """ Given a datetime object determine if this transaction would have occurred on a given date function does some math based on the sample date provided and the frequency indicated TODO: this is slow and inefficient TODO: intelligent update check_cycle based on sample date and datetime_object :param check_cycles: number of occurrences (in weeks) to check in either direction from sample date """ cycles = range(check_cycles + 1) dtc = datetime_object.day mtc = datetime_object.month ytc = datetime_object.year time_delta = datetime_object - self.sample_date """ if there is more time between the sample date and current simulated day (datetime_obj) than would be reachable within the check_cycles of frequency then update the sample_date to be further in the future """ # frequency is a quantity with units so update weeks to days before comparing integers range_of_time_reachable = (self.frequency * check_cycles).to('days') # import ipdb # ipdb.set_trace() while abs(time_delta.days) >= range_of_time_reachable.magnitude: # if time_delta days is positive, then the sample date is too far in the past, step forward if time_delta.days > 0: self.sample_date += range_of_time_reachable # if time_delta days is negative, then the sample date is too far in the future, step backwards elif time_delta.days < 0: self.sample_date -= range_of_time_reachable # update time_delta with new sample date time_delta = datetime_object - self.sample_date # TODO: this for loop is likely not needed anymore, avoiding refactoring until unit tests are set up for occ in cycles: forward = self.sample_date + self.frequency * occ backward = self.sample_date - self.frequency * occ if ((backward.day == dtc) and (backward.month == mtc) and (backward.year == ytc)): logger.debug("Found it on {}th occurence".format(occ)) # update the sample date such that it always stays close to the simulated day self.sample_date = backward return True elif (forward.day == dtc) and (forward.month == mtc) and (forward.year == ytc): logger.debug("Found it on {}th occurence".format(occ)) # update the sample date such that it always stays close to the simulated day self.sample_date = forward return True else: continue return False
def lambda_handler(event, context): ''' Lambda that triggers on s3 put into the userdata directory of backend data ''' now = datetime.datetime.now() placement_key = event['Records'][0]['s3']['object']['key'] userid = placement_key.split('/')[1] accounts, transactions = get_data(userid) #accts_dict = {} accts_list = [] for account in accounts: acctname = account['AccountName'] balance = account['Balance'] account_type = account['Type'] paydate = account['PayoffDay'] paysrc = account['PayoffSource'] creditlim = account['CreditLimit'] accts_list.append( Account(name=acctname, bal=balance, acct_type=account_type, payback_date=paydate, payback_src=paysrc, credit_limit=creditlim)) accts_dict = {acct.name: acct for acct in accts_list} txs_list = [] for tx in transactions: desc = tx['Description'] freq = tx['Occurrence'] amt = tx['Amount'] tx_type = tx['Type'] samp_d = tx['Sample_Date'] src = tx['Source'] until = tx['Until'] tx_obj = Transaction(f=freq, a=amt, t=tx_type, d=desc, sd=samp_d, sc=src, u=until) txs_list.append(tx_obj) days = range(DAYS_TO_PROJECT) aggregate_df = [] # This the actual simulation running through days for day in days: txs_occurring_today = [] # use units to add one day per iteration simulated_day = now + Q_(day, 'day') # loop through all the available transactions for tx in txs_list: # determine if a transaction should occur on simulated day if tx.should_payment_occur_today(simulated_day): logger.debug( f"Paying {tx.description} Today\nSample Date: {tx.sample_date}\nSimulated Day:{simulated_day}\n" ) logger.debug(f"From Acct: {tx.source}") # attempt to grab the account that the transactions is coming from or into or both. try: acct_obj = accts_dict[tx.source] except KeyError: raise KeyError( f"Transaction Source {tx.source} is not an account in {accts_dict.keys()}" ) # take the money from the account it is coming from acct_obj.process_tx(tx, simulated_day) # track the transactions that happened today txs_occurring_today.append( (tx.description, tx.source, tx.amount.magnitude)) # update transaction list to only include ones that are forever recurring """ Update transaction list This is important bc of the "until_date" attribute. If the current simulated day is past the until date, then the transaction should no longer be processed in the future. TODO: move this logic into transaction object itself. A method that takes a datetime object and compares it to its until_date attribute and returns a boolean of [tx for tx in txs_list if tx.is_active(simulated_day)] """ txs_list = [ tx for tx in txs_list if tx.until_date == FOREVER_RECURRING or tx.until_date > simulated_day ] # check all the accounts and see if its a payoff date for acct_obj in accts_dict.values(): # only credit accounts can get paid off if acct_obj.acct_type == CREDIT and simulated_day.day == acct_obj.payback_date: try: payback_src_acct = accts_dict[acct_obj.payback_src] except KeyError: raise KeyError( "Credit Acct Payoff Source {} is not an account in {}". format(acct_obj.payback_src, accts_dict.keys())) # this function modifies both accounts in place payback_src_acct.payoff_credit_acct(acct_obj, simulated_day) logger.debug(f"Day: {simulated_day}".format(simulated_day)) logger.debug(f"Amount: {accts_dict.values()}") acct_data = [] acct_names = [] for acc in accts_dict.values(): acct_data.append(acc.balance.magnitude) acct_names.append(acc.name) # create a row to concatenate to dataframe datalist = [simulated_day, txs_occurring_today] col_list = ['date', 'transactions'] # extend modifies list in place datalist.extend(acct_data) col_list.extend(acct_names) # newrow = pd.DataFrame([datalist], columns=col_list) # use columns as keys and data as values newrow = OrderedDict({k: v for k, v in zip(col_list, datalist)}) aggregate_df.append(newrow) for curract in accts_dict.keys(): acct_specific_txs = [ specify_txs(tx, curract) for tx in get_column_data(aggregate_df, 'transactions') ] new_col_name = f'{curract}transactions' # aggregate_df[new_col_name] = acct_specific_txs aggregate_df = place_column_data(aggregate_df, new_col_name, acct_specific_txs) place_forecasted_data(userid, aggregate_df) # take issues from accounts and write to s3 place_money_warning_data(userid, list(accts_dict.values()))
u=until) txs_list.append(tx) DAYS_TO_PROJECT = args.forecast now = datetime.datetime.now() #now = datetime.datetime(month=3, day=25, year=2019) tings2plot = [] days = range(DAYS_TO_PROJECT) acct_lines = {} aggregate_df = pd.DataFrame() # This the actual simulation running through days for day in days: txs_occurring_today = [] # use units to add one day per iteration simulated_day = now + Q_(day, 'day') # loop through all the available transactions for tx in txs_list: # determine if a transaction should occur on simulated day if tx.should_payment_occur_today(simulated_day): logger.debug("Paying {} Today\nSample Date: {}\nSimulated Day:{}\n" .format(tx.description, tx.sample_date, simulated_day)) logger.debug("From Acct: {}".format(tx.source)) # attempt to grab the account that the transactions is coming from or into or both. try: acct_obj = accts_dict[tx.source] except KeyError: raise KeyError("Transaction Source {} is not an account in {}" .format(tx.source, accts_dict.keys()))