Last active
May 14, 2025 22:06
-
-
Save bat52/5728fa60f12e8dc9ba64ba75ff06be8e to your computer and use it in GitHub Desktop.
Calculates tax gains for RSUs fetching data from Yahoo Finance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
This script calculates the acquisition and sell gains for RSUs (Restricted Stock Units) in EUR. | |
It fetches the acquisition and sell prices from Yahoo Finance, converts them to EUR using the currency exchange rate, | |
and calculates the gains to be declared for tax purposes. | |
Requirements: | |
- yfinance | |
- pandas | |
""" | |
DISCLAIMER = """ | |
########################################################################## | |
# DISCLAIMER | |
########################################################################## | |
According to the French tax law: | |
- The USD/EUR exchange rate should be fetched from | |
Banque de France (https://www.banque-france.fr/fr/statistiques/taux-et-cours/taux-de-change-parites-quotidiennes-2025-04-28), | |
while this script uses data from European Central Bank (ECB) or Yahoo Finance. | |
Banque de France API key https://webstat.banque-france.fr/en/pages/guide-migration-api/ | |
It seems the exchange rate from ECB is aligned with Banque de France | |
- The acquisition gain (plus-value d'acquisition) is calculated based on value of the stock | |
at the opening of acquisition (vesting date, or date d'acquisition). | |
However, I am unsure regarding the sell date (date de cession). | |
This script calculates the sell gain (plus-value de cession) based on the opening price of the stock | |
at the sell date (date de cession). | |
########################################################################## | |
""" | |
import argparse | |
import yfinance as yf | |
import pandas as pd | |
from datetime import datetime, timedelta | |
EUR_VS_USD="EURUSD=X" | |
USD_VS_EUR="USDEUR=X" | |
class DateValidator: | |
@staticmethod | |
def validate_date(date_str: str, date_format: str = "%Y-%m-%d") -> datetime: | |
""" | |
Validates a date string and returns a datetime object. | |
Raises argparse.ArgumentTypeError if invalid. | |
""" | |
try: | |
date = datetime.strptime(date_str, date_format) | |
return date_str | |
except ValueError: | |
raise argparse.ArgumentTypeError(f"Invalid date: '{date_str}'. Expected format: {date_format}") | |
def rsu_parser(): | |
# Set up argument parser | |
parser = argparse.ArgumentParser(description="RSU tax calculator") | |
parser.add_argument( | |
"-t","--ticker", | |
type=str, | |
help="The ticker symbol to fetch data for.", | |
default="CEVA", | |
) | |
parser.add_argument( | |
"-q","--quantity", | |
type=int, | |
help="Number of RSUs", | |
default=10, | |
) | |
parser.add_argument( | |
"-v","--vesting_date", | |
type=DateValidator.validate_date, # Uses the validator function | |
help="The date to validate (format: YYYY-MM-DD). Example: 2023-12-31", | |
default="2020-05-13", | |
) | |
parser.add_argument( | |
"-s","--sell_date", | |
type=DateValidator.validate_date, # Uses the validator function | |
help="The date to validate (format: YYYY-MM-DD). Example: 2023-12-31", | |
default="2024-12-05", | |
) | |
parser.add_argument( | |
"-cusd","--bank_credit_usd", | |
type=float, | |
help="Bank credit in USD, net of fees. Leave empty if not applicable.", | |
default=None, | |
) | |
parser.add_argument( | |
"-ceur","--bank_credit_eur", | |
type=float, | |
help="Bank credit in EUR, net of fees. Leave empty if not applicable.", | |
default=None, | |
) | |
parser.add_argument( | |
"-uy","--use_yahoo", | |
help="Use Yahoo Finance to fetch EUR/USD exchange rate.", | |
action="store_true", | |
) | |
return parser | |
def get_ticker_val(ticker, date): | |
""" | |
Fetches historical market data for a given ticker symbol. | |
Parameters: | |
ticker (str): The ticker symbol to fetch data for. | |
date (tuple): The start date for historical data in 'YYYY-MM-DD' format. | |
Returns: | |
DataFrame: A DataFrame containing the historical market data. | |
""" | |
# Fetch historical data | |
date_start = datetime.strptime(date, "%Y-%m-%d") # Convert string to datetime object | |
next_day = date_start + timedelta(days=1) | |
data = yf.download(ticker, start=date, end=next_day) | |
return data | |
def get_price(ticker, date, price_type='Close'): | |
""" | |
Fetches the price for a given ticker symbol. | |
Parameters: | |
ticker (str): The ticker symbol to fetch data for. | |
date (tuple): The date for which to fetch the acquisition price in 'YYYY-MM-DD' format. | |
Returns: | |
float: The price. | |
""" | |
# Fetch historical data | |
data = get_ticker_val(ticker, date) | |
# Get the price | |
price = float(data[price_type].iloc[0].item()) # Assuming the first row is the acquisition price | |
return price | |
def get_acquisition_price(ticker, date): | |
""" | |
Fetches the acquisition price for a given ticker symbol. | |
Parameters: | |
ticker (str): The ticker symbol to fetch data for. | |
date (tuple): The date for which to fetch the acquisition price in 'YYYY-MM-DD' format. | |
Returns: | |
float: The acquisition price. | |
""" | |
# Get the acquisition price | |
return get_price(ticker, date, 'Open') | |
def get_usd_to_eur_yahoo(date): | |
""" | |
Fetches USD to EUR exchange rate at specified date. | |
Source: Yahoo Finance | |
""" | |
# Get the current exchange rate | |
return get_price(USD_VS_EUR, date, 'Close') | |
def get_eur_to_usd_yahoo(date): | |
""" | |
Fetches EUR to USD exchange rate at specified date. | |
Source: Yahoo Finance | |
""" | |
# Get the current exchange rate | |
return get_price(EUR_VS_USD, date, 'Close') | |
def get_eur_to_usd_ecb(date): | |
""" | |
Fetches EUR to USD exchange rate at specified date. | |
Source: ECB | |
""" | |
# Fetch ECB's CSV (updated daily) | |
url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.zip" | |
df = pd.read_csv(url, compression="zip") | |
# Filter USD rates | |
usd_rate = df[df["Date"]==date]["USD"].iloc[0] | |
# print(f"EUR/USD on {date}: {usd_rate}") | |
return usd_rate | |
def get_eur_to_usd(date, use_yahoo=False): | |
""" | |
Fetches EUR to USD exchange rate at specified date. | |
""" | |
if use_yahoo: | |
# Get the current exchange rate | |
eur_rate = get_eur_to_usd_yahoo(date) | |
else: | |
# Get the current exchange rate | |
eur_rate = get_eur_to_usd_ecb(date) | |
return eur_rate | |
def main(): | |
# Parse command line arguments | |
parser = rsu_parser() | |
args = parser.parse_args() | |
# Fetch and print the ticker data, at acquisition | |
acquisition_price = get_acquisition_price(args.ticker, args.vesting_date) | |
# Fetch the exchange rate | |
# acquisition_usd_to_eur = get_usd_to_eur(args.vesting_date) | |
acquisition_eur_to_usd = get_eur_to_usd(args.vesting_date, use_yahoo=args.use_yahoo) | |
acquisition_price_eur = acquisition_price / acquisition_eur_to_usd | |
# Calculate acquisition gain (plus-value d'acquisition) | |
acquisition_revenue_usd = acquisition_price * args.quantity | |
acquisition_revenue_eur = acquisition_revenue_usd / acquisition_eur_to_usd | |
# Fetch and print the ticker data, at sell | |
sell_price = get_acquisition_price(args.ticker, args.sell_date) | |
# Fetch the exchange rate | |
# sell_usd_to_eur = get_usd_to_eur(args.sell_date) | |
sell_eur_to_usd = get_eur_to_usd(args.sell_date,use_yahoo=args.use_yahoo) | |
sell_price_eur = sell_price / sell_eur_to_usd | |
# Calculate sell gain (plus-value de cession) | |
sell_revenue_usd = sell_price * args.quantity | |
if not args.bank_credit_usd is None: | |
sell_fees_usd = sell_revenue_usd - args.bank_credit_usd | |
sell_revenue_usd = args.bank_credit_usd | |
else: | |
sell_fees_usd = 0 | |
# Calculate sell fees | |
sell_revenue_eur = sell_revenue_usd / sell_eur_to_usd | |
if args.bank_credit_eur is not None: | |
sell_fees_eur = sell_revenue_eur - args.bank_credit_eur | |
sell_revenue_eur = args.bank_credit_eur | |
else: | |
sell_fees_eur = 0 | |
# Calculate the gain to be declared (plus-value à déclarer) | |
sell_gain_eur = sell_revenue_eur - acquisition_revenue_eur | |
# Calculate the acquisition gain to be declared | |
acquisition_gain_eur = acquisition_revenue_eur + sell_gain_eur | |
# Calculate the acquisition gain to be declared after 50% (plus-value à déclarer apres abbattement de 50%) | |
acquisition_gain_eur_after_50 = acquisition_gain_eur * 0.5 | |
# Print the results | |
print(DISCLAIMER) | |
print(f"Stock:{args.ticker}") | |
print(f" Quantity: {args.quantity}") | |
print("\n") | |
print("Acquisition:") | |
print(f" Vesting date: {args.vesting_date}") | |
print(f" Acquisition price: {acquisition_price} [USD]") | |
print(f" Acquisition price: {acquisition_price_eur} [EUR]") | |
# print(f" Acquisition revenue w/ sell: {acquisition_revenue_usd} [USD]") | |
# print(f" USD/EUR @ vest {args.vesting_date}: {acquisition_usd_to_eur}") | |
print(f" EUR/USD @ vest {args.vesting_date}: {acquisition_eur_to_usd}") | |
print(f" Acquisition revenue w/ sell: {acquisition_revenue_eur} [EUR]") | |
print(f" Acquisition gain 'plus-value d'acquisition' : {acquisition_gain_eur} [EUR]") | |
print(f" Acquisition gain - 50% (to be declared) 1TZ, 1WZ: {acquisition_gain_eur_after_50} [EUR]") | |
print("\n") | |
print("Sell:") | |
print(f" Sell date: {args.sell_date}") | |
print(f" Sell price: {sell_price} [USD]") | |
print(f" Sell price: {sell_price_eur} [EUR]") | |
print(f" Sell fees: {sell_fees_usd} [USD]") | |
print(f" Sell fees -> EUR: {sell_fees_usd / sell_eur_to_usd} [EUR]") | |
print(f" Sell revenue: {sell_revenue_usd} [USD]") | |
# print(f" USD/EUR @ sell {args.sell_date}: {sell_usd_to_eur}") | |
print(f" EUR/USD @ vest {args.sell_date}: {sell_eur_to_usd}") | |
print(f" Sell revenue: {sell_revenue_eur} [EUR]") | |
print(f" Sell fees: {sell_fees_eur} [EUR]") | |
print(f" Sell gain 'plus-value de cession' (to be declared): {sell_gain_eur} [EUR]") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment