Last active
September 10, 2026 07:53
-
-
Save 5j9/a69ce57302c36a617da4662a76eec43e to your computer and use it in GitHub Desktop.
Calculate one-month returns of Iranian silver ETFs
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
| # /// script | |
| # requires-python = ">=3.14" | |
| # dependencies = [ | |
| # "aiohttp", | |
| # "jdatetime", | |
| # "polars", | |
| # "pydantic", | |
| # "tsetmc", | |
| # ] | |
| # /// | |
| """ | |
| Answer to the Bashgah question: | |
| https://bashgah.com/Question/140506016/ | |
| Question: | |
| در یک ماه اخیر منتهی به ۱۶ شهریور بازدهی صندوق نقراط و میانگین صندوقها نقره چند درصد بوده؟ | |
| پاسخ خود را انتخاب کنید: | |
| گزینه 1: نقراط ۲۵ درصد و میانگین صندوقهای نقره ۲۰ درصد | |
| گزینه 2: نقراط ۲۸ درصد و میانگین صندوقهای نقره ۲۰ درصد | |
| گزینه 3: نقراط ۲۵ درصد و میانگین صندوقهای نقره ۲۳ درصد | |
| گزینه 4: نقراط ۲۸ درصد و میانگین صندوقهای نقره ۱۵ درصد | |
| The script gets the silver ETF list from TradersArena, then uses TSETMC | |
| closing prices to calculate the one-month returns. | |
| """ | |
| import asyncio | |
| from datetime import date | |
| import aiohttp | |
| from jdatetime import datetime as jdt | |
| from polars import col | |
| from pydantic import BaseModel | |
| from tsetmc.instruments import Instrument | |
| TRADERSARENA_URL = 'https://tradersarena.ir/data/industries/silver-funds/snapshot?timeframe=12' | |
| class Classification(BaseModel): | |
| tag: str | |
| class Fund(BaseModel): | |
| classification: Classification | |
| class Static(BaseModel): | |
| fund: Fund | |
| class Row(BaseModel): | |
| symbol: str | |
| static: Static | |
| class Snapshot(BaseModel): | |
| rows: list[Row] | |
| async def get_silver_l18s() -> list[str]: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get(TRADERSARENA_URL) as response: | |
| response.raise_for_status() | |
| data = await response.json() | |
| snapshot = Snapshot.model_validate(data) | |
| return [ | |
| row.symbol | |
| for row in snapshot.rows | |
| if row.static.fund.classification.tag == 'silver' | |
| ] | |
| def price_on_or_before(history, target_date: date) -> tuple[date, int]: | |
| row = ( | |
| history.filter(col('date') <= target_date) | |
| .sort('date', descending=True) | |
| .select('date', 'pc') | |
| .head(1) | |
| .collect() | |
| ) | |
| if row.is_empty(): | |
| raise ValueError(f'No price history before {target_date}') | |
| return row['date'][0], row['pc'][0] | |
| async def get_return( | |
| l18: str, | |
| start_date: date, | |
| end_date: date, | |
| ) -> tuple[date, int, date, int, float]: | |
| instrument = await Instrument.from_l18(l18) | |
| history = await instrument.price_history() | |
| actual_start_date, start_price = price_on_or_before(history, start_date) | |
| actual_end_date, end_price = price_on_or_before(history, end_date) | |
| return_pct = (end_price / start_price - 1) * 100 | |
| return ( | |
| actual_start_date, | |
| start_price, | |
| actual_end_date, | |
| end_price, | |
| return_pct, | |
| ) | |
| async def main(): | |
| # ۱۶ مرداد ۱۴۰۵ تا ۱۶ شهریور ۱۴۰۵ | |
| start_date = jdt(1405, 5, 16).togregorian() | |
| end_date = jdt(1405, 6, 16).togregorian() | |
| l18s = await get_silver_l18s() | |
| print(f'Silver funds: {len(l18s)}') | |
| print(f'Period: {start_date} → {end_date}') | |
| print() | |
| results = {} | |
| for l18 in l18s: | |
| try: | |
| result = await get_return(l18, start_date, end_date) | |
| except Exception as e: | |
| print(f'{l18}: ERROR: {e}') | |
| continue | |
| results[l18] = result | |
| ( | |
| actual_start_date, | |
| start_price, | |
| actual_end_date, | |
| end_price, | |
| return_pct, | |
| ) = result | |
| print( | |
| f'{l18:10} ' | |
| f'{actual_start_date} → {actual_end_date} ' | |
| f'{start_price:,} → {end_price:,} ' | |
| f'{return_pct:7.2f}%' | |
| ) | |
| if not results: | |
| raise RuntimeError('No returns calculated') | |
| average = sum(result[-1] for result in results.values()) / len(results) | |
| print() | |
| print(f'Average: {average:.2f}%') | |
| noghrat_return = results['نقراط'][-1] | |
| print(f'نقراط: {noghrat_return:.2f}%') | |
| print() | |
| if round(noghrat_return) == 28 and round(average) == 15: | |
| print('Answer: گزینه ۴') | |
| elif round(noghrat_return) == 25 and round(average) == 20: | |
| print('Answer: گزینه ۱') | |
| elif round(noghrat_return) == 28 and round(average) == 20: | |
| print('Answer: گزینه ۲') | |
| elif round(noghrat_return) == 25 and round(average) == 23: | |
| print('Answer: گزینه ۳') | |
| else: | |
| print('No answer choice matched.') | |
| if __name__ == '__main__': | |
| asyncio.run(main()) |
5j9
commented
Sep 10, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment