-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun_t212_pl.py
More file actions
190 lines (160 loc) · 7.77 KB
/
Copy pathrun_t212_pl.py
File metadata and controls
190 lines (160 loc) · 7.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""
Run Trading 212 P&L calculation and generate IRS Annex J XML report.
Usage:
python run_t212_pl.py [user_id] [analysis_year] [nif] [second_person_nif]
Defaults:
user_id: daduarte
analysis_year: 2025
nif: 000000000
second_person_nif: None (single filing)
"""
import sys
import os
import logging
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Add app/ to Python path so modules can import each other
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
from brokers.adapters.t212_adapter import T212Adapter
from irs_report.stocks import calculate_broker_pl, PL_DETAILED
from irs_report.irs_annex_j import (
save_annex_j,
group_year_dividends_by_country,
)
from utils.utils import RestartException
def main():
user_id = sys.argv[1] if len(sys.argv) > 1 else "daduarte"
analysis_year = int(sys.argv[2]) if len(sys.argv) > 2 else 2025
nif = sys.argv[3] if len(sys.argv) > 3 else "000000000"
second_person_nif = sys.argv[4] if len(sys.argv) > 4 else None
print(f"\n{'='*60}")
print(f"TaxSnap - Trading 212 P&L + IRS Annex J Report")
print(f"{'='*60}")
print(f" User: {user_id}")
print(f" Analysis Year: {analysis_year}")
print(f" NIF: {nif}")
print(f" Filing Type: {'Joint (' + second_person_nif + ')' if second_person_nif else 'Single'}")
print(f" Calc Type: PL_DETAILED (FIFO)")
print(f"{'='*60}\n")
adapter = T212Adapter()
# ── Step 1: Calculate Stocks P&L ────────────────────────────
print("[1/3] Calculating stocks P&L (FIFO)...")
retry = True
max_retries = 10
retries = 0
result_df = None
while retry and retries < max_retries:
retries += 1
try:
result_df = calculate_broker_pl(
adapter=adapter,
user_id=user_id,
p_l_analysis_year=analysis_year,
p_l_calculation_type=PL_DETAILED
)
retry = False
except RestartException as e:
logging.warning(f"RestartException (attempt {retries}/{max_retries}): {e}")
retry = True
has_stocks = result_df is not None and len(result_df) > 0
if has_stocks:
# Save stocks CSV output
output_dir = os.path.join(os.getcwd(), "data", user_id, "outputs", "t212")
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, "t212_stocks_pl_details.csv")
result_df.to_csv(output_file, index=False)
print(f" Stocks P&L CSV saved to: {output_file}")
else:
print(f" No realized stocks P&L found for {analysis_year}.")
# ── Step 2: Process Dividends ───────────────────────────────
print(f"\n[2/3] Processing dividends for {analysis_year}...")
dividends_df = adapter.get_dividends(user_id=user_id, year=analysis_year)
has_dividends = dividends_df is not None and len(dividends_df) > 0
dividends_grouped_df = None
if has_dividends:
dividends_grouped_df = group_year_dividends_by_country(dividends_df)
if dividends_grouped_df is not None and len(dividends_grouped_df) > 0:
# Save dividends CSV
div_output_dir = os.path.join(os.getcwd(), "data", user_id, "outputs", "t212")
os.makedirs(div_output_dir, exist_ok=True)
div_output_file = os.path.join(div_output_dir, "t212_dividends_by_country.csv")
dividends_grouped_df.to_csv(div_output_file, index=False)
print(f" Found {len(dividends_df)} dividend entries across {len(dividends_grouped_df)} countries")
print(f" Dividends CSV saved to: {div_output_file}")
else:
print(f" No reportable dividends for {analysis_year} (all filtered out).")
else:
print(f" No dividends found for {analysis_year}.")
# ── Step 3: Generate IRS Annex J XML ────────────────────────
print(f"\n[3/3] Generating IRS Annex J XML report...")
if not has_stocks and (dividends_grouped_df is None or len(dividends_grouped_df) == 0):
print(" No data to generate Annex J (no stocks P&L and no dividends).")
return
xml_path = save_annex_j(
user_id=user_id,
year=analysis_year,
nif=nif,
stocks_pl_df=result_df if has_stocks else None,
dividends_grouped_df=dividends_grouped_df,
second_person_nif=second_person_nif,
)
print(f" Annex J XML saved to: {xml_path}")
# ── Summary ─────────────────────────────────────────────────
print(f"\n{'='*60}")
print(f"RESULTS SUMMARY — Tax Year {analysis_year}")
print(f"{'='*60}")
if has_stocks:
total_sell = result_df['realized_amount'].sum()
total_buy = result_df['buy_amount'].sum()
total_fees = result_df['trade_expenses'].sum()
total_pl = total_sell - total_buy - total_fees
print(f"\n STOCKS (Quadro 09 — Capital Gains)")
print(f" {'-'*56}")
print(f" Total realized entries: {len(result_df)}")
print(f" Total sell amount (EUR): {total_sell:>12.2f}")
print(f" Total buy amount (EUR): {total_buy:>12.2f}")
print(f" Total fees (EUR): {total_fees:>12.2f}")
print(f" Net P&L (EUR): {total_pl:>12.2f}")
encompass = "S (englobar)" if total_sell <= total_buy else "N (nao englobar)"
print(f" Encompass decision: {encompass}")
print(f"\n Per-ticker breakdown:")
print(f" {'Ticker':12s} | {'Sell':>10s} | {'Buy':>10s} | {'Fees':>7s} | {'P&L':>10s}")
print(f" {'-'*12}-+-{'-'*10}-+-{'-'*10}-+-{'-'*7}-+-{'-'*10}")
for ticker in result_df['ticker'].unique():
t = result_df[result_df['ticker'] == ticker]
t_sell = t['realized_amount'].sum()
t_buy = t['buy_amount'].sum()
t_fees = t['trade_expenses'].sum()
t_pl = t_sell - t_buy - t_fees
print(f" {ticker:12s} | {t_sell:10.2f} | {t_buy:10.2f} | {t_fees:7.2f} | {t_pl:10.2f}")
if dividends_grouped_df is not None and len(dividends_grouped_df) > 0:
total_div_gross = dividends_grouped_df['total_dividends'].sum()
total_div_tax = dividends_grouped_df['total_tax_paid'].sum()
print(f"\n DIVIDENDS (Quadro 08 — Foreign Income)")
print(f" {'-'*56}")
print(f" Countries: {len(dividends_grouped_df)}")
print(f" Total gross (EUR): {total_div_gross:>12.2f}")
print(f" Total tax paid (EUR): {total_div_tax:>12.2f}")
print(f" Net dividends (EUR): {total_div_gross - total_div_tax:>12.2f}")
print(f"\n Per-country breakdown:")
print(f" {'Country':10s} | {'Gross':>10s} | {'Tax Paid':>10s} | {'Net':>10s}")
print(f" {'-'*10}-+-{'-'*10}-+-{'-'*10}-+-{'-'*10}")
for _, row in dividends_grouped_df.iterrows():
code = row['dividend_country_code']
gross = row['total_dividends']
tax = row['total_tax_paid']
net = gross - tax
print(f" {code:10s} | {gross:10.2f} | {tax:10.2f} | {net:10.2f}")
print(f"\n{'='*60}")
print(f"Output files:")
if has_stocks:
print(f" Stocks P&L CSV: data/{user_id}/outputs/t212/t212_stocks_pl_details.csv")
if dividends_grouped_df is not None and len(dividends_grouped_df) > 0:
print(f" Dividends CSV: data/{user_id}/outputs/t212/t212_dividends_by_country.csv")
print(f" Annex J XML: {xml_path}")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()