def change(self, reason, shares, commission_spec: CommissionSpec): if shares != self.shares: log.debug("%s shares: %s -> %s (%s).", self.short_name, self.shares, shares, reason) self.commission = self.commission_for(shares, commission_spec) self.shares = shares self.value = shares * self.price
def distribute_free_assets( holdings: List[Holding], expected_total_value, free_assets, currency, commission_spec: CommissionSpec, min_trade_volume, underfunded_only ): if free_assets <= 0: return free_assets, False def difference_from_expected(holding: Holding): expected_value = expected_total_value * holding.expected_weight if expected_value == 0: return -holding.value else: return (expected_value - holding.value) / expected_value for holding in sorted(holdings, key=difference_from_expected, reverse=True): expected_value = expected_total_value * holding.expected_weight if ( holding.expected_weight == 0 or # A special case for deprecated positions holding.value >= holding.current_value and holding.buying_restricted or holding.value >= expected_value and underfunded_only ): continue log.debug("Trying to distribute free %s over %s...", format_assets(free_assets, currency), holding.name) if holding.is_group: free_assets, distributed = distribute_free_assets( holding.holdings, expected_value, free_assets, currency, commission_spec, min_trade_volume, underfunded_only) if distributed: holding.value = sum(holding.value for holding in holding.holdings) return free_assets, True else: previous_commission = holding.commission extra_shares = (free_assets + previous_commission) // holding.price extra_shares = limit_extra_shares_to_minimum(holding, extra_shares, min_trade_volume) if extra_shares > 0: commission = commission_spec.calculate( abs(holding.shares + extra_shares - holding.current_shares), holding.price) extra_shares = (free_assets + previous_commission - commission) // holding.price extra_shares = limit_extra_shares_to_minimum(holding, extra_shares, min_trade_volume) if extra_shares > 0: result_shares = holding.shares + extra_shares if not ( result_shares < holding.current_shares and holding.selling_restricted or result_shares > holding.current_shares and holding.buying_restricted or abs(result_shares - holding.current_shares) * holding.price < min_trade_volume ): holding.change("free assets distribution", result_shares, commission_spec) free_assets -= extra_shares * holding.price - (holding.commission - previous_commission) return free_assets, True return free_assets, False
def calculate(portfolio: Portfolio, api_key, *, fake_prices=False): tickers = set() def process(name, holdings: List[Holding]): if holdings and sum(holding.expected_weight for holding in holdings) != 1: raise Error("Invalid weights for {!r}.", name) for holding in holdings: if holding.is_group: process(holding.name, holding.holdings) else: tickers.add(holding.ticker) process(portfolio.name, portfolio.holdings) prices = get_prices(tickers, api_key, fake_prices) current_value = calculate_current_value(portfolio.holdings, prices) total_assets = current_value + portfolio.free_assets rebalance_to = total_assets - portfolio.min_free_assets if not fake_prices: calculate_restrictions(portfolio.holdings) correct_weights_for_buying_restriction(portfolio.holdings, rebalance_to) # TODO: Display underuse? correct_weights_for_selling_restriction(portfolio.holdings, rebalance_to) # TODO: Display overuse? rebalanced_value = rebalance(portfolio.holdings, rebalance_to, portfolio.commission_spec, portfolio.min_trade_volume) commissions = calculate_total_commissions(portfolio.holdings) free_assets = total_assets - rebalanced_value - commissions free_assets_to_distribute = free_assets - portfolio.min_free_assets for underfunded_only in (True, False): if free_assets_to_distribute <= 0: break log.debug("Trying to distribute free %s %s positions...", format_assets(free_assets_to_distribute, portfolio.currency), "over underfunded only" if underfunded_only else "all") while True: free_assets_to_distribute, distributed = distribute_free_assets( portfolio.holdings, rebalanced_value, free_assets_to_distribute, portfolio.currency, portfolio.commission_spec, portfolio.min_trade_volume, underfunded_only) if not distributed: break free_assets = free_assets_to_distribute + portfolio.min_free_assets rebalanced_value = sum(holding.value for holding in portfolio.holdings) commissions = calculate_total_commissions(portfolio.holdings) return rebalanced_value, free_assets, commissions
def _cleanup(backup_dir, repositories): try: files = os.listdir(backup_dir) except EnvironmentError as e: raise Error("Unable to list '{}' directory: {}.", backup_dir, e) cleanup_files = set(files) - set(repositories) - {_LOCK_FILE_NAME} for file_name in cleanup_files: path = os.path.join(backup_dir, file_name) if file_name.startswith("."): log.debug("Removing '%s'.", path) else: log.warning("Remove deleted repository '%s'.", file_name) _rm_path(path)
def main(): try: _configure_signal_handling() args = _parse_args() backup_dir = args.backup_dir pcli.log.setup( name="git-backup", debug_mode=args.debug, level=logging.WARNING if not args.debug and args.cron else None) _check_backup_dir(backup_dir) lock_file_path = os.path.expanduser(os.path.join(backup_dir, _LOCK_FILE_NAME)) try: lock_file_fd = psys.daemon.acquire_pidfile(lock_file_path) except psys.daemon.PidFileLockedError as e: if args.cron: log.debug("Exiting: %s", e) else: raise Error("{}", e) except psys.daemon.PidFileLockError as e: raise Error("{}", e) else: try: _backup(args.user, backup_dir) finally: try: os.unlink(lock_file_path) except EnvironmentError as e: log.error("Failed to delete lock file '%s': %s.", lock_file_path, e) finally: eintr_retry(os.close)(lock_file_fd) except Error as e: sys.exit("Error: {}".format(e))
def on_sell_blocked(self, reason): log.debug("%s: sell blocked: %s.", self.short_name, reason) self.sell_blocked = True
def on_buy_blocked(self, reason): log.debug("%s: buy blocked: %s.", self.short_name, reason) self.buy_blocked = True
def set_weight(self, reason, weight): if weight != self.weight: log.debug("%s weight: %s -> %s (%s).", self.short_name, self.weight, weight, reason) self.weight = weight