Skip to content

Instantly share code, notes, and snippets.

@bat52
Last active May 14, 2025 22:06
Show Gist options
  • Save bat52/5728fa60f12e8dc9ba64ba75ff06be8e to your computer and use it in GitHub Desktop.
Save bat52/5728fa60f12e8dc9ba64ba75ff06be8e to your computer and use it in GitHub Desktop.
Calculates tax gains for RSUs fetching data from Yahoo Finance
#!/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