예제 #1
0
    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
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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)
예제 #5
0
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))
예제 #6
0
 def on_sell_blocked(self, reason):
     log.debug("%s: sell blocked: %s.", self.short_name, reason)
     self.sell_blocked = True
예제 #7
0
 def on_buy_blocked(self, reason):
     log.debug("%s: buy blocked: %s.", self.short_name, reason)
     self.buy_blocked = True
예제 #8
0
    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