Программа в 50 строк на Java/Scala, которая сэкономит вам 50 тыс. р. при подаче декларации 3-НДФЛ +4


Видео-версия:

Вы, наверное, знаете, с любых доходов нужно платить налоги, в т.ч. с доходов от торговли ценными бумагами и производными инструментами (проще говоря, от игры на бирже). Очень удобно, когда брокер делает расчеты за вас, перед тем как вы выведите деньги со счета, выполняя функции налогового агента.

Но если брокер такой как у меня - Interactive Brokers (организация, третьего дня запрещенная на территории РФ), декларацию вам придется делать и подавать самому. Делать это всем, конечно же лень, и неплохо бы отдать подготовку на аутсорс; поэтому в установленный законом срок (до 30.04) я подал нулевую декларацию, и только сейчас озаботился подготовкой уточненки, так как уже скоро нужно платить сам налог.

Каково же было мое удивление, когда я узнал расценки на составление у своих товарищей по опасному биржевому бизнесу. Оказалось, что услуги консультантов разнятся от количество проведенных сделок. А количество сделок зависит от того, не болеете ли вы лудоманией (лат. ludo — играю + др.-греч. ????? — безумие, влечение), и не совершаете ли внутри дня множество хаотичных сделок, закармливая брокера комиссией. В США, FINRA пытается остудить горячую голову лудоманов с помощью т.н. Pattern day trader rule, когда лудоман убил свой счет до $25K им блокируют сделки.

Так вот, мои товарищи умудрились довести сумму за подготовку декларации до 50 и более тысяч рублей. У меня дела шли не настолько хорошо, чтобы платить такие конские суммы нахлебникам (можете ознакомиться с моей прошлой заметкой "Мой маржин-кол: как теряют деньги на бирже"). Поэтому я решил изучить вопрос и сделать все самостоятельно, по инструкциям, представленным в интернете.

Официальная программа для ввода сумм доходов в декларацию
Официальная программа для ввода сумм доходов в декларацию

Кроме этой программы, есть еще веб-версия, но она совсем неудобна, и мне совершенно не улыбалось вбивать ничего вручную даже в десктопное приложение. На выходе из этой программы, получается xml, который, как я быстро сообразил, можно сгенерить автоматически. В первой его части имеются итоги, которые можно поправить и в блокноте, а окончание выглядит вот так:

...<ДоходИстИно Ставка="13"> <РасчДохНалИно ОКСМ="840" НаимИстДох="BFT WS" КодВалют="840" КодВидДох="32" ДатаДох="09.12.2020" КурсВалютДох="73.6618" ДоходИноВал="219.81" ДоходИноРуб="16191.70" НалЗачРФОбщ="893"/>...

</ДоходИстИно> <ДохОперЦБ ВидОпер="01" ДохСовОпер="0.00" РасхРеалЦБ="41.15" РасхУмДохОпер="41.15" ПризУчетУбыт="0"/>

<ДохОперЦБ ВидОпер="06" ДохСовОпер="500.15" РасхРеалЦБ="42.52" РасхУмДохОпер="42.52" ПризУчетУбыт="0"/> </НДФЛ3></Документ></Файл>

Разные инструкции рекомендовали примерно одно и то же - умножить поле "Реализованная П/У" из отчета брокера на дату сделки, и вколотить эти цифры в строки. Были правда и такие, которые предлагали переводить стоимость активов на дату покупки также в рубли, и организовать отдельный суммовой учет по ФИФО, или по средней себестоимости; кто-то считал, что в отчет должна вноситься выручка. Как при этом учитывать коротки позиции, было не понятно, и непонятно, как всю это ерунду должен проверять инспектор в налоговой.

Кстати, что меня удивило - я не увидел в xml отдельных сумм своих вычетов, они суммируются и выгружаются одной строкой по каждому коду - что серьезно осложнит автоматическую сверку данных, даже если налоговая захочет это проделать. Поэтому я решил не умничать, а считать доходом то, что считает им брокер (в итоге все равно алгоритмы сойдутся к примерно тому же результату). Брокер IB дает csv такой структуры:

Для формирования собственного xml я написал вот такую программу, которая получилась довольно небольшой, поэтому она могла бы послужить кому-то примером для обучения :

import com.typesafe.scalalogging.Logger
import org.slf4j.LoggerFactory

import java.io.PrintWriter
import java.nio.charset.Charset
import java.text.DecimalFormatSymbols
import java.util.Locale
import scala.io.Source

object Tax   {
  def main(args: Array[String]): Unit = {
    implicit val logger = Logger(LoggerFactory.getLogger(this.getClass))
    import java.text.DecimalFormat
    val df = new DecimalFormat("0.00")
    val dfRate = new DecimalFormat("0.0000")
    df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH))
    dfRate.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH))
    val str = (Source.fromFile("d:\\tax\\1.csv","UTF-8").getLines().toSeq ++ Source.fromFile("d:\\tax\\2.csv","UTF-8").getLines().toSeq)
      .map(_.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1).toList)
    val rates = Source.fromFile("d:\\tax\\r.csv","windows-1252").getLines().map(_.split(';')).toSeq
      .map(e1=>e1.updated(1,e1(1).split('.').reverse.mkString("-")))
    def rate(d:String)={
      rates.sortBy(_(1)).findLast(e=>e(1)<=d).map{x=>
        if (x(1)!=d) logger.debug(s"Нет курса на дату $d, последний ${x(1)}")
        BigDecimal(x(2).replaceAll(",","."))
      }.get
    }
    val str1 = str.filter(_.length > 15).filter(e => e(0) == "Сделки" && e(1) == "Data").filter{e=> e(13)!=""}
      .map(e=>(e,{
        val date = e(6).replaceAll("\"","" ).split(',')(0)
        val rt = rate(date)
        val sumR = (rt * BigDecimal(e(13)))
        val codes = e(3).trim match {
          case "Контракты на разницу (CFD)" => ("893","06")//1532
          case "Опционы на акции и индексы" => ("893","06")//1532
          case "Варранты" => ("893","06")//1532
          case "Акции" => ("79","01")//1530
        }
        val xml = s"""<РасчДохНалИно ОКСМ="840" НаимИстДох="${e(5)}" КодВалют="840" КодВидДох="32" ДатаДох="${
          date.split('-').reverse.mkString(".")
        }" КурсВалютДох="${dfRate.format(rt)}" ДоходИноВал="${df.format(BigDecimal(e(13)))}" ДоходИноРуб="${df.format(sumR)}" НалЗачРФОбщ="${codes._1}"/>"""
        (xml,sumR,codes)
      }))
    val txt = str1.filter(_._2._2>0).map(_._2._1).mkString("") +
      str1.groupBy(_._2._3).map(e=>(e._1,{
        val x = e._2.map(_._2._2)
        (x.filter(_ >0).sum,-x.filter(_ <0).sum)
      })).map(e=>
        s"""<ДохОперЦБ ВидОпер="${e._1._2}" ДохСовОпер="${df.format(e._2._1)}" РасхРеалЦБ="${df.format(e._2._2)}" РасхУмДохОпер="${df.format(e._2._2)}" ПризУчетУбыт="0"/>"""
      ).mkString("\n") match {
      case e=> new PrintWriter("d:\\tax\\tax_xml.txt", Charset.forName("UTF-8")) { write(e); close }
    }
    logger.debug(s"Дох=${str1.map(e=>e._2._2).filter(_ >0 ).sum} " +
      s"Расх=${str1.map(e=>e._2._2).filter(_ <0 ).sum}" +
      s"База=${str1.map(e=>e._2._2).sum}")
  }
}

Несколько комментариев по коду:

  1. В программе конкатенируется несколько файлов, потому что при торговле CFD используется отдельный субсчет, на который автоматически скидываются деньги, отчет по нему отдельный.

  2. Американский формат дат типа "2020-12-24" позволяет сортировать даты для быстрого нахождения последнего курса на дату (файл ЦБ не содержит курсов в пн. и по праздникам), поэтому пришлось привести данные к этому формату.

  3. Наличие в файле кавычек не позволяет делать split(','), требуется использовать regexp.

Почему я заикнулся о том, что этот мой лапшекод (design pattern, all rights reserved) с захардкоженными именами файла и прочими нелицеприятным решениями, можно использовать для обучения? Во-первых, что начинающий программист в первую очередь должен уметь читать и поддерживать чужой код, написанный на скорую руку. Если он неспособен разобраться даже в такой программке, очевидно, что в боевой обстановке он растеряется еще больше.

А во-вторых, все мы когда-то мечтали что будем запускать ракеты на Марс, или писать игры категории triple-A, а на самом деле, производственные задачи - вещь намного более скучная, это в основном бухучет, 1С, SAP, либо с случае банков, java/scala, таблицы Excel, запросы SQL, генерация xml и json. Чем раньше у начинающего программиста развеятся вредные иллюзии, тем лучше.

Поэтому, думаю, даже если вы не торгуете у зарубежных брокеров, и вам не надо сдавать 3-НДФЛ, данная заметка может быть интересной и для вас.




К сожалению, не доступен сервер mySQL