示例#1
0
 def UpdateQuotes(self, start_timestamp, end_timestamp):
     self.PrepareRussianCBReader()
     jal_db = JalDB()
     query = executeSQL(
         "WITH _holdings AS ( "
         "SELECT l.asset_id AS asset, l.account_id FROM ledger AS l "
         "WHERE l.book_account = :assets_book AND l.timestamp <= :end_timestamp "
         "GROUP BY l.asset_id "
         "HAVING SUM(l.amount) > :tolerance "
         "UNION "
         "SELECT DISTINCT l.asset_id AS asset, l.account_id FROM ledger AS l "
         "WHERE l.book_account = :assets_book AND l.timestamp >= :start_timestamp "
         "AND l.timestamp <= :end_timestamp "
         "UNION "
         "SELECT c.id, NULL FROM currencies c "
         ") "
         "SELECT h.asset AS asset_id, coalesce(ac.currency_id, 1) AS currency_id, "
         "a.symbol AS name, a.quote_source AS feed_id, a.isin AS isin, "
         "MIN(q.timestamp) AS first_timestamp, MAX(q.timestamp) AS last_timestamp "
         "FROM _holdings AS h "
         "LEFT JOIN accounts AS ac ON ac.id=h.account_id "
         "LEFT JOIN assets_ext AS a ON a.id=h.asset AND a.currency_id=coalesce(ac.currency_id, 1) "
         "LEFT JOIN quotes AS q ON q.asset_id=h.asset "
         "GROUP BY h.asset, ac.currency_id "
         "ORDER BY feed_id",
         [(":start_timestamp", start_timestamp),
          (":end_timestamp", end_timestamp),
          (":assets_book", BookAccount.Assets),
          (":money_book", BookAccount.Money),
          (":liabilities_book", BookAccount.Liabilities),
          (":tolerance", Setup.CALC_TOLERANCE)])
     while query.next():
         asset = readSQLrecord(query, named=True)
         if asset['asset_id'] == int(
                 JalSettings().getValue('BaseCurrency')):
             continue
         first_timestamp = asset[
             'first_timestamp'] if asset['first_timestamp'] != '' else 0
         last_timestamp = asset[
             'last_timestamp'] if asset['last_timestamp'] != '' else 0
         if start_timestamp < first_timestamp:
             from_timestamp = start_timestamp
         else:
             from_timestamp = last_timestamp if last_timestamp > start_timestamp else start_timestamp
         if end_timestamp < from_timestamp:
             continue
         try:
             data = self.data_loaders[asset['feed_id']](
                 asset['asset_id'], asset['name'], asset['currency_id'],
                 asset['isin'], from_timestamp, end_timestamp)
         except (xml_tree.ParseError, pd.errors.EmptyDataError, KeyError):
             logging.warning(
                 self.tr("No data were downloaded for ") + f"{asset}")
             continue
         if data is not None:
             quotations = []
             for date, quote in data.iterrows(
             ):  # Date in pandas dataset is in UTC by default
                 quotations.append({
                     'timestamp': int(date.timestamp()),
                     'quote': float(quote[0])
                 })
             jal_db.update_quotes(asset['asset_id'], asset['currency_id'],
                                  quotations)
     jal_db.commit()
     logging.info(self.tr("Download completed"))
示例#2
0
文件: statement.py 项目: flmnvd/jal
 def _match_account_ids(self):
     for account in self._data[FOF.ACCOUNTS]:
         account_id = JalDB().find_account(account['number'], -account['currency'])
         if account_id:
             old_id, account['id'] = account['id'], -account_id
             self._update_id("account", old_id, account_id)
示例#3
0
    def prepare_tax(self):
        _ = executeSQL("DELETE FROM t_last_dates")
        _ = executeSQL("DELETE FROM t_last_quotes")

        _ = executeSQL(
            "INSERT INTO t_last_dates(ref_id, timestamp) "
            "SELECT ref_id, coalesce(MAX(q.timestamp), 0) AS timestamp "
            "FROM ("
            "SELECT t.timestamp AS ref_id FROM trades AS t "
            "WHERE t.account_id=:account_id AND t.asset_id=:asset_id "
            "UNION "
            "SELECT t.settlement AS ref_id FROM trades AS t "
            "WHERE t.account_id=:account_id AND t.asset_id=:asset_id "
            ") LEFT JOIN accounts AS a ON a.id = :account_id "
            "LEFT JOIN quotes AS q ON ref_id >= q.timestamp AND a.currency_id=q.asset_id AND q.currency_id=:base_currency "
            "WHERE ref_id IS NOT NULL "
            "GROUP BY ref_id ORDER BY ref_id",
            [(":account_id", self.account_id), (":asset_id", self.asset_id),
             (":base_currency", JalSettings().getValue('BaseCurrency'))])
        _ = executeSQL(
            "INSERT INTO t_last_quotes(timestamp, asset_id, quote) "
            "SELECT MAX(timestamp) AS timestamp, asset_id, quote "
            "FROM quotes AS q LEFT JOIN accounts AS a ON a.id = :account_id "
            "WHERE q.asset_id = :asset_id OR q.asset_id = a.currency_id  AND q.currency_id=:base_currency "
            "GROUP BY asset_id",
            [(":account_id", self.account_id), (":asset_id", self.asset_id),
             (":base_currency", JalSettings().getValue('BaseCurrency'))])

        self.quote = readSQL(
            "SELECT quote FROM t_last_quotes WHERE asset_id=:asset_id",
            [(":asset_id", self.asset_id)])
        if self.quote is None:
            logging.error(
                self.tr("Can't get current quote for ") + self.asset_name)
            return
        self.currency_name = JalDB().get_asset_name(
            JalDB().get_account_currency(self.account_id))

        self.rate = readSQL(
            "SELECT quote FROM accounts AS a "
            "LEFT JOIN t_last_quotes AS q ON q.asset_id=a.currency_id WHERE id=:account_id",
            [(":account_id", self.account_id)])
        if self.rate is None:
            logging.error(
                self.tr("Can't get current rate for ") + self.currency_name)
            return

        query = executeSQL(
            "SELECT strftime('%d/%m/%Y', datetime(t.timestamp, 'unixepoch')) AS timestamp, "
            "t.qty AS qty, t.price AS o_price, oq.quote AS o_rate FROM trades AS t "
            "LEFT JOIN accounts AS ac ON ac.id = :account_id "
            "LEFT JOIN t_last_dates AS od ON od.ref_id = IIF(t.settlement=0, t.timestamp, t.settlement) "
            "LEFT JOIN quotes AS oq ON ac.currency_id=oq.asset_id AND oq.currency_id=:base_currency "
            "AND oq.timestamp=od.timestamp "
            "WHERE t.account_id=:account_id AND t.asset_id=:asset_id AND t.qty*(:total_qty)>0 "
            "ORDER BY t.timestamp DESC, t.id DESC",
            [(":account_id", self.account_id), (":asset_id", self.asset_id),
             (":total_qty", self.asset_qty),
             (":base_currency", JalSettings().getValue('BaseCurrency'))])
        table = []
        remainder = self.asset_qty
        profit = 0
        value = 0
        profit_rub = 0
        value_rub = 0
        while query.next():
            record = readSQLrecord(query, named=True)
            record['qty'] = record[
                'qty'] if record['qty'] <= remainder else remainder
            record['profit'] = record['qty'] * (self.quote - record['o_price'])
            record[
                'o_rate'] = 1 if record['o_rate'] == '' else record['o_rate']
            record['profit_rub'] = record['qty'] * (
                self.quote * self.rate - record['o_price'] * record['o_rate'])
            record['tax'] = 0.13 * record['profit_rub'] if record[
                'profit_rub'] > 0 else 0
            table.append(record)
            remainder -= record['qty']
            profit += record['profit']
            value += record['qty'] * record['o_price']
            profit_rub += record['profit_rub']
            value_rub += record['qty'] * record['o_price'] * record['o_rate']
            if remainder <= 0:
                break
        tax = 0.13 * profit_rub if profit_rub > 0 else 0
        table.append({
            'timestamp': self.tr("TOTAL"),
            'qty': self.asset_qty,
            'o_price': value / self.asset_qty,
            'o_rate': value_rub / value,
            'profit': profit,
            'profit_rub': profit_rub,
            'tax': tax
        })
        data = pd.DataFrame(table)
        self.dataframe = data
示例#4
0
    def MOEX_DataReader(self,
                        asset_id,
                        asset_code,
                        currency,
                        isin,
                        start_timestamp,
                        end_timestamp,
                        update_symbol=True):
        currency = JalDB().get_asset_name(currency)
        asset = self.MOEX_info(symbol=asset_code,
                               isin=isin,
                               currency=currency,
                               special=True)
        if (asset['engine'] is None) or (asset['market'] is
                                         None) or (asset['board'] is None):
            logging.warning(f"Failed to find {asset_code} on moex.com")
            return None
        if (asset['market'] == 'bonds') and (asset['board'] == 'TQCB'):
            asset_code = isin  # Corporate bonds are quoted by ISIN
        if (asset['market'] == 'shares') and (asset['board'] == 'TQIF'):
            asset_code = isin  # ETFs are quoted by ISIN
        if update_symbol:
            isin = asset['isin'] if 'isin' in asset else ''
            reg_number = asset['reg_number'] if 'reg_number' in asset else ''
            expiry = asset['expiry'] if 'expiry' in asset else 0
            principal = asset['principal'] if 'principal' in asset else 0
            details = {
                'isin': isin,
                'reg_number': reg_number,
                'expiry': expiry,
                'principal': principal
            }
            JalDB().update_asset_data(asset_id, details)

        # Get price history
        date1 = datetime.utcfromtimestamp(start_timestamp).strftime('%Y-%m-%d')
        date2 = datetime.utcfromtimestamp(end_timestamp).strftime('%Y-%m-%d')
        url = f"http://iss.moex.com/iss/history/engines/{asset['engine']}/markets/{asset['market']}/" \
              f"boards/{asset['board']}/securities/{asset_code}.xml?from={date1}&till={date2}"
        xml_root = xml_tree.fromstring(get_web_data(url))
        history_rows = xml_root.findall("data[@id='history']/rows/*")
        quotes = []
        for row in history_rows:
            if row.attrib['CLOSE']:
                if 'FACEVALUE' in row.attrib:  # Correction for bonds
                    price = float(row.attrib['CLOSE']) * float(
                        row.attrib['FACEVALUE']) / 100.0
                    quotes.append({
                        "Date": row.attrib['TRADEDATE'],
                        "Close": price
                    })
                else:
                    quotes.append({
                        "Date": row.attrib['TRADEDATE'],
                        "Close": row.attrib['CLOSE']
                    })
        data = pd.DataFrame(quotes, columns=["Date", "Close"])
        data['Date'] = pd.to_datetime(data['Date'], format="%Y-%m-%d")
        data['Close'] = pd.to_numeric(data['Close'])
        close = data.set_index("Date")
        return close
示例#5
0
    def load(self):
        # Read first table with account details
        try:
            data = pandas.read_html(self._filename,
                                    match='Account Capabilities')
        except:
            logging.error(self.tr("Can't read statement file"))
            return False
        if len(data) != 1:
            logging.error(
                self.tr("Can't get account details from the statement"))
            return False
        for i, row in data[0].iterrows():
            if row[0] == 'Account':
                self._account = row[1]
        if self._account == '':
            logging.error(
                self.tr("Can't get account number from the statement"))
            return False

        # Read Trades table
        try:
            data = pandas.read_html(self._filename,
                                    match='Date/Time',
                                    attrs={'id': 'summaryDetailTable'})
        except:
            logging.error(
                self.tr("Can't read Trades table from statement file"))
            return False
        if len(data) != 1:
            logging.error(self.tr("Can't get Trades table from the statement"))
            return False
        statement = data[0]
        statement = statement[statement['Symbol'].notna()]

        account_id = None
        for i, row in statement.iterrows():
            if row[0] == 'Forex':  # We reached end of Stock trades
                break
            if row[0].startswith('Total') or row[0] == 'Stocks' or row[
                    0] == 'Symbol':  # Skip totals and headers
                continue
            if row[0] == row[1]:  # it's a currency header - match account
                account_id = JalDB().get_account_id(self._account, row[0])
                continue
            if account_id is None:  # no reason to check further if we don't have valid account here
                continue
            asset_id = JalDB().get_asset_id(row[0])
            if asset_id is None:
                logging.warning(self.tr("Unknown asset ") + f"'{row[0]}'")
                continue
            timestamp = int(
                datetime.strptime(row[1], "%Y-%m-%d, %H:%M:%S").replace(
                    tzinfo=timezone.utc).timestamp())
            settlement = int(
                datetime.strptime(
                    row[1][:10],
                    "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp())
            number = f"{i}"
            qty = float(row[2])
            price = float(row[3])
            fee = float(row[6])
            JalDB().add_trade(account_id, asset_id, timestamp, settlement,
                              number, qty, price, -fee)
        logging.info(self.tr("Load IBKR Activity statement completed"))
        return True
示例#6
0
 def setCurrency(self, currency_id):
     if self._currency != currency_id:
         self._currency = currency_id
         self._currency_name = JalDB().get_asset_name(currency_id)
         self.calculateHoldings()
示例#7
0
    def find_dividend4tax(self, timestamp, account_id, asset_id, prev_tax, new_tax, note):
        PaymentInLiueOfDividend = 'PAYMENT IN LIEU OF DIVIDEND'
        TaxNotePattern = r"^(?P<symbol>.*\w) ?\((?P<isin>\w+)\)(?P<prefix>( \w*)+) +(?P<amount>\d+\.\d+)?(?P<suffix>.*)$"
        DividendNotePattern = r"^(?P<symbol>.*\w) ?\((?P<isin>\w+)\)(?P<prefix>( \w*)+) +(?P<amount>\d+\.\d+)?(?P<suffix>.*) \(.*\)$"

        dividends = [x for x in self._data[FOF.ASSET_PAYMENTS] if
                     x['type'] == FOF.PAYMENT_DIVIDEND and x['asset'] == asset_id and x['account'] == account_id]
        account = [x for x in self._data[FOF.ACCOUNTS] if x["id"] == account_id][0]
        currency = [x for x in self._data[FOF.ASSETS] if x["id"] == account['currency']][0]
        db_account = JalDB().get_account_id(account['number'], currency['symbol'])
        asset = [x for x in self._data[FOF.ASSETS] if x["id"] == asset_id][0]
        isin = asset['isin'] if 'isin' in asset else ''
        db_asset = JalDB().get_asset_id(asset['symbol'], isin=isin, dialog_new=False)
        if db_account is not None and db_asset is not None:
            query = executeSQL(
                "SELECT -id AS id, -account_id AS account, timestamp, number, "
                "-asset_id AS asset, amount, tax, note as description FROM dividends "
                "WHERE type=:div AND account_id=:account_id AND asset_id=:asset_id",
                [(":div", DividendSubtype.Dividend), (":account_id", db_account), (":asset_id", db_asset)],
                forward_only=True)
            while query.next():
                db_dividend = readSQLrecord(query, named=True)
                db_dividend['asset'] = asset_id
                db_dividend['account'] = account_id
                dividends.append(db_dividend)
        if datetime.utcfromtimestamp(timestamp).timetuple().tm_yday < 75:
            # We may have wrong date in taxes before March, 15 due to tax correction
            range_start = ManipulateDate.startOfPreviousYear(day=datetime.utcfromtimestamp(timestamp))
            dividends = [x for x in dividends if x['timestamp'] >= range_start]
        else:
            # For any other day - use exact time match
            dividends = [x for x in dividends if x['timestamp'] == timestamp]
        dividends = [x for x in dividends if 'tax' not in x or (abs(x['tax'] - prev_tax) < 0.0001)]
        dividends = sorted(dividends, key=lambda x: x['timestamp'])

        # Choose either Dividends or Payments in liue with regards to note of the matching tax
        if PaymentInLiueOfDividend in note.upper():
            dividends = list(filter(lambda item: PaymentInLiueOfDividend in item['description'], dividends))
            # we don't check for full match as there are a lot of records without amount
        else:
            dividends = list(filter(lambda item: PaymentInLiueOfDividend not in item['description'], dividends))
            # Check for full match
            for dividend in dividends:
                if (dividend['timestamp'] == timestamp) and (note.upper() == dividend['description'][:len(note)].upper()):
                    return dividend
        if len(dividends) == 0:
            return None

        # Chose most probable dividend - by amount, timestamp and description
        parts = re.match(TaxNotePattern, note, re.IGNORECASE)
        if not parts:
            logging.warning(self.tr("*** MANUAL ENTRY REQUIRED ***"))
            logging.warning(self.tr("Unhandled tax pattern found: ") + f"{note}")
            return None
        parts = parts.groupdict()
        note_prefix = parts['prefix']
        note_suffix = parts['suffix']
        try:
            note_amount = float(parts['amount'])
        except (ValueError, TypeError):
            note_amount = 0
        score = [0] * len(dividends)
        for i, dividend in enumerate(dividends):
            parts = re.match(DividendNotePattern, dividend['description'], re.IGNORECASE)
            if not parts:
                logging.warning(self.tr("*** MANUAL ENTRY REQUIRED ***"))
                logging.warning(self.tr("Unhandled dividend pattern found: ") + f"{dividend['description']}")
                return None
            parts = parts.groupdict()
            try:
                amount = float(parts['amount'])
            except (ValueError, TypeError):
                amount = 0
            if abs(amount - note_amount) <= 0.000005:            # Description has very similar amount +++++
                score[i] += 5
            if dividend['timestamp'] == timestamp:               # Timestamp exact match gives ++
                score[i] += 2
            if abs(0.1 * dividend['amount'] - new_tax) <= 0.01:  # New tax is 10% of dividend gives +
                score[i] += 1
            if parts['prefix'] == note_prefix:                   # Prefix part of description match gives +
                score[i] += 1
            if parts['suffix'] == note_suffix:                   # Suffix part of description match gives +
                score[i] += 1
        for i, vote in enumerate(score):
            if (vote == max(score)) and (vote > 0):
                return dividends[i]
        # Final check - if only one found, return it
        if len(dividends) == 1:
            return dividends[0]
        return None
示例#8
0
 def __init__(self, operation_id=None, display_type=None):
     labels = {
         Transfer.Outgoing: ('<', CustomColor.DarkBlue),
         Transfer.Incoming: ('>', CustomColor.DarkBlue),
         Transfer.Fee: ('=', CustomColor.DarkRed)
     }
     super().__init__(operation_id)
     self.table = "transfers"
     self._otype = LedgerTransaction.Transfer
     self._display_type = display_type
     self._data = readSQL(
         "SELECT t.withdrawal_timestamp, t.withdrawal_account, t.withdrawal, t.deposit_timestamp, "
         "t.deposit_account, t.deposit, t.fee_account, t.fee, t.asset, t.note "
         "FROM transfers AS t WHERE t.id=:oid", [(":oid", self._oid)],
         named=True)
     self._withdrawal_account = self._data['withdrawal_account']
     self._withdrawal_account_name = JalDB().get_account_name(
         self._withdrawal_account)
     self._withdrawal_timestamp = self._data['withdrawal_timestamp']
     self._withdrawal = self._data['withdrawal']
     self._withdrawal_currency = JalDB().get_asset_name(
         JalDB().get_account_currency(self._withdrawal_account))
     self._deposit_account = self._data['deposit_account']
     self._deposit_account_name = JalDB().get_account_name(
         self._deposit_account)
     self._deposit = self._data['deposit']
     self._deposit_currency = JalDB().get_asset_name(
         JalDB().get_account_currency(self._deposit_account))
     self._deposit_timestamp = self._data['deposit_timestamp']
     self._fee_account = self._data['fee_account']
     self._fee_currency = JalDB().get_asset_name(
         JalDB().get_account_currency(self._fee_account))
     self._fee_account_name = JalDB().get_account_name(self._fee_account)
     self._fee = self._data['fee']
     self._label, self._label_color = labels[display_type]
     self._note = self._data['note']
     if self._display_type == Transfer.Outgoing:
         self._reconciled = JalDB().account_reconciliation_timestamp(
             self._withdrawal_account) >= self._timestamp
     elif self._display_type == Transfer.Incoming:
         self._reconciled = JalDB().account_reconciliation_timestamp(
             self._deposit_account) >= self._timestamp
     elif self._display_type == Transfer.Fee:
         self._reconciled = JalDB().account_reconciliation_timestamp(
             self._fee_account) >= self._timestamp
     else:
         assert True, "Unknown transfer type"
示例#9
0
 def description(self) -> str:
     return self._note + "\n" + self.tr(
         "Tax: ") + JalDB().get_asset_country(self._asset)
示例#10
0
文件: ledger.py 项目: flmnvd/jal
 def getAmount(self, book, account_id, asset_id=None):
     if asset_id is None:
         asset_id = JalDB().get_account_currency(account_id)
     return self.amounts[(book, account_id, asset_id)]
示例#11
0
    def load(self):
        try:
            data = pandas.read_html(self._filename,
                                    encoding='cp1251',
                                    converters={
                                        self.Qty: convert_amount,
                                        self.Amount: convert_amount,
                                        self.Price: convert_amount,
                                        self.Coupon: convert_amount
                                    })
        except:
            logging.error(self.tr("Can't read statement file"))
            return False

        report_info = data[0]
        deals_info = data[1]
        parts = re.match(self.ClientPattern, report_info[0][2])
        if parts:
            account_id = JalDB().get_account_id(parts.group(1))
        else:
            logging.error(
                self.tr("Can't get account number from the statement."))
            return False
        if account_id is None:
            logging.error(
                self.tr("Account with number ") + f"{parts.group(1)}" +
                self.tr(" not found. Import cancelled."))
            return False

        for index, row in deals_info.iterrows():
            if row[self.Type] == self.Buy:
                qty = int(row[self.Qty])
            elif row[self.Type] == self.Sell:
                qty = -int(row[self.Qty])
            elif row[self.Type][:len(self.Total)] == self.Total:
                break  # End of statement reached
            else:
                logging.warning(
                    self.tr("Unknown operation type ") + f"'{row[self.Type]}'")
                continue
            asset_id = JalDB().get_asset_id(row[self.Symbol])
            if asset_id is None:
                logging.warning(
                    self.tr("Unknown asset ") + f"'{row[self.Symbol]}'")
                continue
            timestamp = int(
                datetime.strptime(row[self.DateTime],
                                  "%d.%m.%Y %H:%M:%S").replace(
                                      tzinfo=timezone.utc).timestamp())
            settlement = int(
                datetime.strptime(
                    row[self.SettleDate],
                    "%d.%m.%Y").replace(tzinfo=timezone.utc).timestamp())
            number = row[self.TradeNumber]
            price = row[self.Price]
            amount = row[self.Amount]
            lot_size = math.pow(10,
                                round(math.log10(amount / (price * abs(qty)))))
            qty = qty * lot_size
            fee = float(row[self.Fee])
            if self.FeeEx in row:  # Broker dependent fee import
                fee = fee + float(row[self.FeeEx])
            else:
                fee = fee + float(row[self.FeeEx1]) + float(
                    row[self.FeeEx2]) + float(row[self.FeeEx3])
            bond_interest = float(row[self.Coupon])
            JalDB().add_trade(account_id, asset_id, timestamp, settlement,
                              number, qty, price, fee)
            if bond_interest != 0:
                JalDB().add_dividend(DividendSubtype.BondInterest, timestamp,
                                     account_id, asset_id, bond_interest,
                                     "НКД", number)
        return True