Browse Source

Merge branch 'main' into bugfix/portfolio-performance-of-closed-position

pull/7610/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
9c78c51c5d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 127
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  3. 2
      apps/api/src/helper/country.helper.ts
  4. 22
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  5. 8
      apps/api/src/services/data-provider/data-provider.service.ts
  6. 3
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 63
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  8. 38
      libs/common/src/lib/personal-finance-tools.ts
  9. 8
      package-lock.json
  10. 2
      package.json

8
CHANGELOG.md

@ -7,9 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Refreshed the cryptocurrencies list
- Upgraded `countries-list` from version `3.4.0` to `3.4.1`
### Fixed
- Fixed the performance of closed positions in the portfolio calculator caused by a rounding remainder in the investment
- Fixed the missing mapping for Turkey in the country weightings of the _Financial Modeling Prep_ service
- Fixed the missing mapping for Czech Republic and Turkey in the data enhancer for asset profile data via _Yahoo Finance_
- Resolved an error when fetching dividends from _Yahoo Finance_ for date ranges without events
## 3.49.0 - 2026-08-12

127
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -113,7 +113,7 @@
"2OMB": "2omb Finance",
"2SHARES": "2SHARE",
"2TF": "2TF",
"2Z": "DoubleZero USD Price",
"2Z": "DoubleZero",
"300F": "300FIT",
"314DAO": "Tonken 314 DAO",
"32BIT": "32Bitcoin",
@ -212,7 +212,7 @@
"AAVEGOTCHIFOMO": "Aavegotchi FOMO",
"AAX": "Academic Labs",
"AAZ": "ATLAZ",
"AB": "Arma The Battle Ground USD Price",
"AB": "Arma The Battle Ground",
"AB1INCH": "1inch (Avalanche Bride)",
"ABA": "EcoBall",
"ABAT": "Aave BAT",
@ -806,7 +806,7 @@
"AMY": "Amygws",
"AMZE": "The Amaze World",
"AMZNON": "Amazon (Ondo Tokenized)",
"AMZNX": "Amazon tokenized stock (xStock) USD Price",
"AMZNX": "Amazon tokenized stock (xStock)",
"ANA": "Nirvana ANA",
"ANAL": "AnalCoin",
"ANALOS": "analoS",
@ -845,7 +845,7 @@
"ANIM": "Animalia",
"ANIMA": "Realm Anima",
"ANIME": "Animecoin",
"ANIME35319": "Animecoin USD Price",
"ANIME35319": "Animecoin",
"ANIMECOIN": "Animecoin",
"ANIMEONBASE": "Anime",
"ANIMETOKEN": "Anime Token (animetoken.in)",
@ -1016,7 +1016,7 @@
"ARBUZ": "ARBUZ",
"ARBYS": "Arbys Token",
"ARC": "AI Rig Complex",
"ARC23486": "Arcadeum USD Price",
"ARC23486": "Arcadeum",
"ARCA": "Legend of Arcadia",
"ARCAD": "Arcadeum",
"ARCADE": "ARCADE",
@ -1367,7 +1367,7 @@
"AVGOX": "Broadcom tokenized stock (xStock)",
"AVH": "Animation Vision Cash",
"AVI": "Aviator",
"AVICI": "Avici USD Price",
"AVICI": "Avici",
"AVINOC": "AVINOC",
"AVIVE": "Avive World",
"AVL": "Aston Villa Fan Token",
@ -1390,7 +1390,7 @@
"AWAX": "AWAX",
"AWBTC": "Aave interest bearing WBTC",
"AWC": "Atomic Wallet Coin",
"AWE": "AWE Network USD Price",
"AWE": "AWE Network",
"AWK": "Awkward Monkey Base",
"AWM": "Another World",
"AWNEX": "AWNEX token",
@ -1452,7 +1452,7 @@
"AZUR": "Azuro Protocol",
"AZURE": "Azure Wallet",
"AZY": "Amazy",
"B": "BUILDon USD Price",
"B": "BUILDon",
"B01": "b0rder1ess",
"B1P": "B ONE PAYMENT",
"B2": "B² Network",
@ -1974,7 +1974,7 @@
"BEPE": "Blast Pepe",
"BEPR": "Blockchain Euro Project",
"BEPRO": "BEPRO Network",
"BERA": "Berachain USD Price",
"BERA": "Berachain",
"BERAETH": "Berachain Staked ETH",
"BERASTONE": "StakeStone Berachain Vault Token",
"BERC": "Fair BERC20",
@ -2129,6 +2129,7 @@
"BIKE": "White Bike",
"BIKI": "BIKI",
"BILL": "TillBilly",
"BILL39545": "Billions Network",
"BILLI": "Billi",
"BILLICAT": "BilliCat",
"BILLY": "Billy",
@ -2614,7 +2615,7 @@
"BONDLYV1": "Bondly Finance",
"BONDX": "BondX",
"BONE": "Shibarium Wrapped BONE",
"BONE11865": "Bone ShibaSwap USD Price",
"BONE11865": "Bone ShibaSwap",
"BONEBONE": "Bone",
"BONES": "Moonshots Farm",
"BONESCOIN": "BonesCoin",
@ -3715,7 +3716,7 @@
"CHILL": "ChillPill",
"CHILLAX": "Chillax",
"CHILLCAT": "Chillchat",
"CHILLGUY": "Just a chill guy USD Price",
"CHILLGUY": "Just a chill guy",
"CHILLHOUSE": "Chill House",
"CHIM": "Chimera",
"CHINA": "China Coin",
@ -3965,7 +3966,7 @@
"CO2": "CO2 Token",
"COA": "Alliance Games",
"COAI": "ChainOpera AI",
"COAI38489": "ChainOpera AI USD Price",
"COAI38489": "ChainOpera AI",
"COAL": "BitCoal",
"COB": "Cobinhood",
"COBE": "Castle of Blackwater",
@ -5260,7 +5261,7 @@
"DOFI": "Doge Floki Coin",
"DOG": "Dog (Runes)",
"DOG11557": "The Doge NFT",
"DOG30933": "Dog (Runes) USD Price",
"DOG30933": "Dog (Runes)",
"DOGA": "DOGAMÍ",
"DOGACOIN": "DogaCoin",
"DOGAI": "Dogai",
@ -5527,7 +5528,7 @@
"DRT": "DomRaider",
"DRUGS": "Big Pharmai",
"DRV": "Derive",
"DRV35014": "Derive USD Price",
"DRV35014": "Derive",
"DRX": "DRX Token",
"DRXNE": "Droxne",
"DRZ": "Droidz",
@ -6077,7 +6078,7 @@
"ENRON": "Enron",
"ENRX": "Enrex",
"ENS": "Ethereum Name Service",
"ENSO": "Enso USD Price",
"ENSO": "Enso",
"ENT": "Eternity",
"ENTC": "ENTERBUTTON",
"ENTER": "EnterCoin",
@ -6629,7 +6630,7 @@
"FEY": "Feyorra",
"FF": "Forefront",
"FF1": "Two Prime FF1 Token",
"FF38482": "Falcon Finance USD Price",
"FF38482": "Falcon Finance",
"FFA": "Cryptofifa",
"FFC": "FireflyCoin",
"FFCT": "FortFC",
@ -6678,7 +6679,7 @@
"FIGHTPEPE": "FIGHT PEPE",
"FIGHTRUMP": "FIGHT TRUMP",
"FIGMA": "Figments Club",
"FIGRHELOC": "Figure HELOC USD Price",
"FIGRHELOC": "Figure HELOC",
"FIH": "Fidelity House",
"FIII": "Fiii",
"FIL": "Filecoin",
@ -6955,7 +6956,7 @@
"FORGE": "Forge Finance",
"FORK": "Gastro Advisor Token",
"FORM": "Formation Fi",
"FORM23635": "Four USD Price",
"FORM23635": "Four",
"FORMATION": "Formation FI",
"FORMNET": "Form",
"FORS": "Foresight",
@ -7555,7 +7556,7 @@
"GIFT": "GiftNet",
"GIG": "GigaCoin",
"GIGA": "Gigachad",
"GIGA30063": "Gigachad USD Price",
"GIGA30063": "Gigachad",
"GIGABRAIN": "Gigabrain by virtuals",
"GIGACAT": "GIGACAT",
"GIGACHAD": "GigaChad",
@ -7911,7 +7912,7 @@
"GRID": "Grid+",
"GRIDCOIN": "GridCoin",
"GRIDZ": "GridZone.io",
"GRIFFAIN": "GRIFFAIN USD Price",
"GRIFFAIN": "GRIFFAIN",
"GRIFT": "ORBIT",
"GRIM": "GrimToken",
"GRIMACE": "GrimaceCoin",
@ -8108,7 +8109,7 @@
"GZT": "Golden Zen Token",
"GZX": "GreenZoneX",
"Glo Dollar": "USDGLO",
"H": "Humanity Protocol USD Price",
"H": "Humanity Protocol",
"H1": "Haven1",
"H1DR4": "H1DR4 by Virtuals",
"H2O": "H2O DAO",
@ -8545,7 +8546,7 @@
"HOSTAI": "Host AI",
"HOT": "Holo",
"HOT1": "Holo",
"HOT2682": "Holo USD Price",
"HOT2682": "Holo",
"HOTCROSS": "Hot Cross",
"HOTDOGE": "Hot Doge",
"HOTKEY": "HotKeySwap",
@ -8952,7 +8953,7 @@
"INFX": "Influxcoin",
"ING": "Infinity Games",
"INI": "InitVerse",
"INIT": "Initia USD Price",
"INIT": "Initia",
"INJ": "Injective",
"INK": "Ink",
"INN": "Innova",
@ -9293,7 +9294,7 @@
"JINDO": "JINDOGE",
"JINDOGE": "Jindoge",
"JIO": "JIO Token",
"JITOSOL": "Jito Staked SOL USD Price",
"JITOSOL": "Jito Staked SOL",
"JIZZ": "JizzRocket",
"JIZZLORD": "JizzLord",
"JIZZUS": "JIZZUS CHRIST",
@ -9301,7 +9302,7 @@
"JK": "JK Coin",
"JKC": "JunkCoin",
"JKL": "Jackal Protocol",
"JLP": "Jupiter Perps LP USD Price",
"JLP": "Jupiter Perps LP",
"JLY": "Jellyverse",
"JM": "JustMoney",
"JMC": "Junson Ming Chan Coin",
@ -9457,7 +9458,7 @@
"KAIM": "Kai Meme",
"KAINET": "KAINET",
"KAIRO": "Kairo",
"KAITO": "KAITO USD Price",
"KAITO": "KAITO",
"KAKA": "KAKA NFT World",
"KAKAXA": "KAKAXA",
"KAKI": "Doge KaKi",
@ -9694,7 +9695,7 @@
"KISHU": "Kishu Inu",
"KIT": "DexKit",
"KITA": "KITA INU",
"KITE": "Kite USD Price",
"KITE": "Kite",
"KITEAI": "KITEAI",
"KITEHAI": "Kite",
"KITKAT": "Remember KitKat",
@ -10274,7 +10275,7 @@
"LISUSD": "lisUSD",
"LIT": "Lition",
"LIT1": "Litentry",
"LIT39125": "Lighter USD Price",
"LIT39125": "Lighter",
"LITCOIN": "Litcoin",
"LITE": "Lite USD",
"LITEBTC": "LiteBitcoin",
@ -10362,7 +10363,7 @@
"LOCG": "LOCGame",
"LOCI": "LociCoin",
"LOCK": "Meridian Network",
"LOCKIN": "LOCK IN USD Price",
"LOCKIN": "LOCK IN",
"LOCO": "Loco",
"LOCOM": "Locomotir",
"LOCUS": "Locus Chain",
@ -10370,7 +10371,7 @@
"LOE": "Legends of Elysium",
"LOF": "Land of Fantasy",
"LOFI": "LOFI",
"LOFI34187": "LOFI USD Price",
"LOFI34187": "LOFI",
"LOFIBUZZ": "LOFI",
"LOG": "Wood Coin",
"LOGO": "LOGOS",
@ -10640,7 +10641,7 @@
"M0": "M by M^0",
"M1": "SupplyShock",
"M2O": "M2O Token",
"M35491": "MemeCore USD Price",
"M35491": "MemeCore",
"M3H": "MehVerseCoin",
"M3M3": "M3M3",
"M87": "MESSIER",
@ -11011,7 +11012,7 @@
"MEFAI": "META FINANCIAL AI",
"MEFI": "Meo Finance",
"MEGA": "MegaCryptoPolis",
"MEGA38770": "MegaETH USD Price",
"MEGA38770": "MegaETH",
"MEGABOT": "Megabot",
"MEGAD": "Mega Dice Casino",
"MEGAHERO": "MEGAHERO",
@ -11026,7 +11027,7 @@
"MEL": "MELX",
"MELAN": "Melania Meme",
"MELANIA": "Melania Meme",
"MELANIA35347": "Official Melania Meme USD Price",
"MELANIA35347": "Official Melania Meme",
"MELANIATRUMP": "Melania Trump",
"MELB": "Minelab",
"MELD": "MELD",
@ -11106,7 +11107,7 @@
"MESSI": "MESSI COIN",
"MESSU": "Loinel Messu",
"MET": "Metronome",
"MET38353": "Meteora USD Price",
"MET38353": "Meteora",
"META": "Metadium",
"METAA": "META ARENA",
"METABOT": "Robot Warriors",
@ -11513,7 +11514,7 @@
"MOBYONBASEV1": "Moby v1",
"MOC": "Moss Coin",
"MOCA": "Moca Network",
"MOCA31526": "Moca Network USD Price",
"MOCA31526": "Moca Network",
"MOCHI": "Mochiswap",
"MOCHI14315": "Mochi",
"MOCHICAT": "MochiCat",
@ -11576,7 +11577,7 @@
"MOMO2": "MOMO 2.0",
"MOMO2025": "momo",
"MON": "PocMon",
"MON30495": "Monad USD Price",
"MON30495": "Monad",
"MON30950": "MON",
"MONA": "MonaCoin",
"MONAI": "MONAI",
@ -11631,7 +11632,7 @@
"MOOBIFI": "Staked BIFI",
"MOOCAT": "MooCat",
"MOODENG": "Moo Deng (moodengsol.com)",
"MOODENG33093": "Moo Deng USD Price",
"MOODENG33093": "Moo Deng",
"MOODENGBNB": "MOODENG (moodengbnb.com)",
"MOODENGSBS": "Moo Deng (moodeng.sbs)",
"MOODENGSPACE": "MOO DENG",
@ -12352,7 +12353,7 @@
"NIK": "NIKPLACE",
"NIKO": "NikolAI",
"NIL": "Nillion",
"NIL35702": "Nillion USD Price",
"NIL35702": "Nillion",
"NILA": "MindWave",
"NILE": "Nile",
"NIM": "Nimiq",
@ -13672,7 +13673,7 @@
"PIPL": "PiplCoin",
"PIPO": "Pipo",
"PIPONHL": "PiP",
"PIPPIN": "pippin USD Price",
"PIPPIN": "pippin",
"PIPPKIN": "Pippkin The Horse",
"PIPT": "Power Index Pool Token",
"PIRATE": "PirateCash",
@ -14094,7 +14095,7 @@
"PROLIFIC": "Prolific Game Studio",
"PROM": "Prom",
"PROME": "Prometheus Trading",
"PROMPT": "Wayfinder USD Price",
"PROMPT": "Wayfinder",
"PRON": "Rise Of Nebula",
"PROOF": "PROVER",
"PROP": "Propeller",
@ -14250,7 +14251,7 @@
"PURPE": "Purple Pepe",
"PURPLEBTC": "Purple Bitcoin",
"PURR": "Purr",
"PURR34332": "Purr USD Price",
"PURR34332": "Purr",
"PURRC": "Purrcoin",
"PURSE": "Pundi X PURSE",
"PUS": "Pussy Cat",
@ -14357,6 +14358,7 @@
"QGOLD": "Quorium",
"QGOV": "Q Protocol",
"QI": "QiSwap",
"QI9288": "BENQI",
"QIE": "QI Blockchain",
"QINGWA": "ShangXin QingWa",
"QISWAP": "QiSwap",
@ -14546,7 +14548,7 @@
"RATWIF": "RatWifHat",
"RAV": "Ravelin Finance",
"RAVE": "Ravendex",
"RAVE38967": "RaveDAO USD Price",
"RAVE38967": "RaveDAO",
"RAVELOUS": "Ravelous",
"RAVEN": "Raven Protocol",
"RAVENCOINC": "Ravencoin Classic",
@ -14620,6 +14622,7 @@
"RDT": "Ridotto",
"RDWON": "Redwire (Ondo Tokenized)",
"RDX": "Redux Protocol",
"RE": "Re",
"REA": "Realisto",
"REACH": "/Reach",
"REACT": "Reactive Network",
@ -14706,7 +14709,7 @@
"REINDEER": "Reindeer",
"REK": "Rekt",
"REKT": "Rekt",
"REKT34434": "Rekt USD Price",
"REKT34434": "Rekt",
"REKTV1": "REKT",
"REKTV2": "REKT 2.0",
"REKTV3": "REKT v3 (rekt.game)",
@ -15053,7 +15056,7 @@
"RRT": "Recovery Right Tokens",
"RS": "ReadySwap",
"RSC": "ResearchCoin",
"RSC27054": "ResearchCoin USD Price",
"RSC27054": "ResearchCoin",
"RSETH": "Kelp DAO Restaked ETH",
"RSF": "Royal Sting",
"RSG": "RSG TOKEN",
@ -15202,7 +15205,7 @@
"SAD": "SadCat",
"SAF": "Safcoin",
"SAFE": "SafeCoin",
"SAFE21585": "Safe USD Price",
"SAFE21585": "Safe",
"SAFEBTC": "SafeBTC",
"SAFEBULL": "SafeBull",
"SAFECOIN": "SafeCoin",
@ -15289,7 +15292,7 @@
"SAO": "Sator",
"SAP": "SwapAll",
"SAPE": "SolanaApe",
"SAPIEN": "Sapien USD Price",
"SAPIEN": "Sapien",
"SAPP": "Sapphire",
"SAPPC": "SappChat",
"SAR": "Saren",
@ -15315,7 +15318,7 @@
"SATOX": "Satoxcoin",
"SATOZ": "Satozhi",
"SATS": "SATS (Ordinals)",
"SATS28194": "SATS (Ordinals) USD Price",
"SATS28194": "SATS (Ordinals)",
"SATSALL": "ALL BEST ICO SATOSHI",
"SATT": "SaTT",
"SATX": "SATX",
@ -15514,7 +15517,7 @@
"SENATE": "SENATE",
"SENC": "Sentinel Chain",
"SEND": "Social Send",
"SEND34611": "Suilend USD Price",
"SEND34611": "Suilend",
"SENDCOIN": "Sendcoin",
"SENDOR": "Sendor",
"SENK": "Senk",
@ -16125,7 +16128,7 @@
"SNRG": "Synergy",
"SNRK": "Snark Launch",
"SNS": "Synesis One",
"SNS36468": "Solana Name Service USD Price",
"SNS36468": "Solana Name Service",
"SNST": "Smooth Network Solutions Token",
"SNSY": "Sensay",
"SNT": "Status",
@ -16283,7 +16286,7 @@
"SONOF": "Son of Solana",
"SONOR": "SonorusToken",
"SOON": "SoonVerse",
"SOON36542": "SOON USD Price",
"SOON36542": "SOON",
"SOONAVERSE": "Soonaverse",
"SOONCOIN": "SoonCoin",
"SOONTOKEN": "SOON",
@ -17340,7 +17343,7 @@
"THOR": "THORSwap",
"THOREUM": "Thoreum V3",
"THP": "TurboHigh Performance",
"THQ": "Theoriq Token",
"THQ": "Theoriq",
"THR": "Thorecoin",
"THREE": "Three Protocol Token",
"THRT": "ThriveToken",
@ -17751,7 +17754,7 @@
"TRUMP2024": "Donald Trump",
"TRUMP27872": "MAGA",
"TRUMP3": "Trump MP3",
"TRUMP35336": "OFFICIAL TRUMP USD Price",
"TRUMP35336": "OFFICIAL TRUMP",
"TRUMP47": "47th President of the United States",
"TRUMPA": "TRUMP AI",
"TRUMPAI": "Trump Maga AI",
@ -17890,7 +17893,7 @@
"TURT": "TurtSat",
"TURT28826": "TurtSat",
"TURTLE": "Turtle",
"TURTLE38671": "Turtle USD Price",
"TURTLE38671": "Turtle",
"TUS": "Treasure Under Sea",
"TUSD": "TrueUSD",
"TUSDV1": "True USD v1",
@ -18065,7 +18068,7 @@
"UMAMI": "Umami Finance",
"UMB": "Umbrella Network",
"UMBR": "Umbria Network",
"UMBRA": "Umbra USD Price",
"UMBRA": "Umbra",
"UMC": "Umbrella Coin",
"UMI": "UMI",
"UMID": "Umi Digital",
@ -18216,7 +18219,7 @@
"USCR": "United States Crypto Reserve",
"USD0": "Usual USD",
"USD1": "World Liberty Financial USD",
"USD136148": "World Liberty Financial USD USD Price",
"USD136148": "World Liberty Financial USD",
"USD3": "Web 3 Dollar",
"USDA": "USDA",
"USDACC": "USDA",
@ -18296,7 +18299,7 @@
"USE": "Usechain Token",
"USEDCAR": "A Gently Used 2001 Honda",
"USELESS": "USELESS COIN",
"USELESS36828": "Useless Coin USD Price",
"USELESS36828": "Useless Coin",
"USETH": "USETH",
"USF": "Unslashed Finance",
"USG": "USGold",
@ -18363,7 +18366,7 @@
"UWULEND": "UwU Lend",
"UX": "UX Chain",
"UXD": "UXD Stablecoin",
"UXLINK": "UXLINK USD Price",
"UXLINK": "UXLINK",
"UXLINKV1": "UXLINK v1",
"UXOS": "UXOS",
"UXP": "UXD Protocol",
@ -19177,7 +19180,7 @@
"WLSC": "WESTLAND SMART CITY",
"WLTH": "Common Wealth",
"WLUNA": "Wrapped LUNA Token",
"WLUNC": "Wrapped LUNA Classic USD Price",
"WLUNC": "Wrapped LUNA Classic",
"WLXT": "Wallex Token",
"WM": "WrappedM by M^0",
"WMATIC": "Wrapped Matic",
@ -19509,7 +19512,7 @@
"XCXT": "CoinonatX",
"XD": "Data Transaction Token",
"XDAG": "XDAG",
"XDAI": "xDAI",
"XDAI": "XDAI",
"XDAO": "XDAO",
"XDATA": "Streamr XDATA",
"XDB": "DigitalBits",
@ -19587,7 +19590,7 @@
"XIDR": "XIDR",
"XIL": "Project X",
"XIN": "Mixin",
"XIN2349": "Mixin USD Price",
"XIN2349": "Mixin",
"XING": "Xing Xing",
"XINGXING": "星星",
"XINU": "XINU",
@ -19848,7 +19851,7 @@
"YAXIS": "yAxis",
"YAY": "YAY Games",
"YAYCOIN": "YAYcoin",
"YB": "YieldBasis USD Price",
"YB": "YieldBasis",
"YBC": "YbCoin",
"YBDBD": "YBDBD",
"YBNB": "Yellow BNB 4",
@ -20034,7 +20037,7 @@
"ZBCN": "Zebec Network",
"ZBIT": "zbit",
"ZBT": "ZB Token",
"ZBT38427": "ZEROBASE USD Price",
"ZBT38427": "ZEROBASE",
"ZBU": "Zeebu",
"ZBUV1": "ZEEBU v1",
"ZCC": "ZCC Coin",

2
apps/api/src/helper/country.helper.ts

@ -18,7 +18,7 @@ export function getCountryCodeByName({
}
}
if (name) {
if (name && name.toLowerCase() !== 'other') {
const logger = new Logger('getCountryCodeByName');
logger.warn(`Could not map the country "${name}" to a code`);

22
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -1,3 +1,4 @@
import { getCountryCodeByName } from '@ghostfolio/api/helper/country.helper';
import { getSectorName } from '@ghostfolio/api/helper/sector.helper';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error';
@ -18,12 +19,16 @@ import {
SymbolProfile
} from '@prisma/client';
import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import YahooFinance from 'yahoo-finance2';
import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
private static countriesMapping = {
'Czech Republic': 'CZ',
Turkey: 'TR'
};
private static sectorsMapping: Record<string, SectorName> = {
basic_materials: 'Basic Materials',
communication_services: 'Communication Services',
@ -246,15 +251,14 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
) {
// Add country if asset is stock and country available
try {
const [code] = Object.entries(countries).find(([, country]) => {
return country.name === assetProfile.summaryProfile?.country;
});
const code = getCountryCodeByName({
aliases: YahooFinanceDataEnhancerService.countriesMapping,
name: assetProfile.summaryProfile.country
});
if (code) {
response.countries = [{ code, weight: 1 }];
}
} catch {}
if (code) {
response.countries = [{ code, weight: 1 }];
}
if (assetProfile.summaryProfile?.sector) {
response.sectors = [

8
apps/api/src/services/data-provider/data-provider.service.ts

@ -340,7 +340,13 @@ export class DataProviderService implements OnModuleInit {
from: Date;
granularity: Granularity;
to: Date;
} & AssetProfileIdentifier) {
} & AssetProfileIdentifier): Promise<{
[date: string]: DataProviderHistoricalResponse;
}> {
if (!isValid(from) || !isValid(to)) {
return {};
}
return this.getDataProvider(DataSource[dataSource]).getDividends({
from,
granularity,

3
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -56,7 +56,8 @@ export class FinancialModelingPrepService
private static countriesMapping = {
'Korea (the Republic of)': 'KR',
'Russian Federation': 'RU',
'Taiwan (Province of China)': 'TW'
'Taiwan (Province of China)': 'TW',
Turkey: 'TR'
};
private readonly logger = new Logger(FinancialModelingPrepService.name);

63
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -22,6 +22,7 @@ import {
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { addDays, format, isSameDay } from 'date-fns';
import { ReasonPhrases, StatusCodes } from 'http-status-codes';
import { uniqBy } from 'lodash';
import YahooFinance from 'yahoo-finance2';
import { ChartResultArray } from 'yahoo-finance2/esm/src/modules/chart';
@ -41,6 +42,12 @@ import { SearchQuoteNonYahoo } from 'yahoo-finance2/esm/src/modules/search';
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
private static readonly DELISTED_ERROR_MESSAGE =
'No data found, symbol may be delisted';
private static readonly RATE_LIMIT_ERROR_MESSAGE =
ReasonPhrases.TOO_MANY_REQUESTS;
private readonly logger = new Logger(YahooFinanceService.name);
private readonly yahooFinance = new YahooFinance({
@ -77,10 +84,6 @@ export class YahooFinanceService implements DataProviderInterface {
symbol,
to
}: GetDividendsParams) {
if (isSameDay(from, to)) {
to = addDays(to, 1);
}
try {
const historicalResult = this.convertToDividendResult(
await this.yahooFinance.chart(
@ -91,7 +94,10 @@ export class YahooFinanceService implements DataProviderInterface {
events: 'dividends',
interval: granularity === 'month' ? '1mo' : '1d',
period1: format(from, DATE_FORMAT),
period2: format(to, DATE_FORMAT)
period2: format(
isSameDay(from, to) ? addDays(to, 1) : to,
DATE_FORMAT
)
}
)
);
@ -107,12 +113,26 @@ export class YahooFinanceService implements DataProviderInterface {
return response;
} catch (error) {
this.logger.error(
`Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
const message = `Could not get dividends for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}`;
if (error?.message === YahooFinanceService.DELISTED_ERROR_MESSAGE) {
this.logger.warn(
`${message}: ${YahooFinanceService.DELISTED_ERROR_MESSAGE}`
);
} else if (
(error?.name === 'HTTPError' &&
error?.code === StatusCodes.TOO_MANY_REQUESTS) ||
error?.message?.startsWith(YahooFinanceService.RATE_LIMIT_ERROR_MESSAGE)
) {
this.logger.warn(
`${message}: ${YahooFinanceService.RATE_LIMIT_ERROR_MESSAGE}`
);
} else {
this.logger.error(`${message}: [${error?.name}] ${error?.message}`);
}
return {};
}
@ -125,10 +145,6 @@ export class YahooFinanceService implements DataProviderInterface {
}: GetHistoricalParams): Promise<{
[date: string]: DataProviderHistoricalResponse;
}> {
if (isSameDay(from, to)) {
to = addDays(to, 1);
}
try {
const historicalResult = this.convertToHistoricalResult(
await this.yahooFinance.chart(
@ -138,7 +154,10 @@ export class YahooFinanceService implements DataProviderInterface {
{
interval: '1d',
period1: format(from, DATE_FORMAT),
period2: format(to, DATE_FORMAT)
period2: format(
isSameDay(from, to) ? addDays(to, 1) : to,
DATE_FORMAT
)
}
)
);
@ -155,7 +174,7 @@ export class YahooFinanceService implements DataProviderInterface {
return response;
} catch (error) {
if (error.message === 'No data found, symbol may be delisted') {
if (error?.message === YahooFinanceService.DELISTED_ERROR_MESSAGE) {
throw new AssetProfileDelistedError(
`No data found, ${symbol} (${this.getName()}) may be delisted`
);
@ -164,7 +183,7 @@ export class YahooFinanceService implements DataProviderInterface {
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
)} to ${format(to, DATE_FORMAT)}: [${error?.name}] ${error?.message}`
);
}
}
@ -343,9 +362,11 @@ export class YahooFinanceService implements DataProviderInterface {
private convertToDividendResult(
result: ChartResultArray
): HistoricalDividendsResult {
return result.events.dividends.map(({ amount: dividends, date }) => {
return { date, dividends };
});
return (result.events?.dividends ?? []).map(
({ amount: dividends, date }) => {
return { date, dividends };
}
);
}
private convertToHistoricalResult(

38
libs/common/src/lib/personal-finance-tools.ts

@ -1252,6 +1252,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Know where your money is going',
url: 'https://www.pocketsmith.com'
},
{
categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'portfolio-coach',
languages: ['English'],
name: 'Portfolio Coach',
origin: 'GB',
platforms: ['WEB'],
pricingPerYear: '£120',
slogan: 'Know your wealth. Ask it anything.',
url: 'https://portfoliocoach.co.uk'
},
{
categories: ['DIVIDEND_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: false,
@ -1276,6 +1289,18 @@ export const personalFinanceTools: Product[] = [
slogan: 'Tools for Better Investors',
url: 'https://www.portfoliovisualizer.com'
},
{
categories: ['DIVIDEND_TRACKING', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'portfolioglance',
languages: ['English', 'Polski'],
name: 'PortfolioGlance',
origin: 'PL',
platforms: ['WEB'],
slogan: 'One look. Clear picture of your portfolio.',
url: 'https://www.portfolioglance.com'
},
{
categories: ['STOCK_TRACKING'],
hasFreePlan: true,
@ -1551,6 +1576,19 @@ export const personalFinanceTools: Product[] = [
'Your financial life in a spreadsheet, automatically updated each day',
url: 'https://tiller.com'
},
{
categories: ['CRYPTOCURRENCY', 'DIVIDEND_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'tinywallet',
languages: ['Deutsch', 'English'],
name: 'TinyWallet',
origin: 'DE',
platforms: ['WEB'],
pricingPerYear: '€35',
slogan: 'Your key to empowered wealth management',
url: 'https://www.tinywallet.de'
},
{
founded: 2011,
hasFreePlan: false,

8
package-lock.json

@ -63,7 +63,7 @@
"color": "5.0.3",
"cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0",
"countries-list": "3.4.0",
"countries-list": "3.4.1",
"countup.js": "2.10.1",
"date-fns": "4.4.0",
"dotenv": "17.4.2",
@ -16600,9 +16600,9 @@
}
},
"node_modules/countries-list": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.4.0.tgz",
"integrity": "sha512-SonKkjlEGUNz/Ugj6akOMUl8/F6EBVbLq/nQbDKhZo60EFyNgsVIgDP521J9pGhywikV514Ct4BueIqiOnvBBA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.4.1.tgz",
"integrity": "sha512-sKLGAZD5EAdzQwQ19vOp/9u33MTjmZ0CkdXPYtp/An6cyIlJ/E8TwGf2FcPe1AnzHO+h7nbTfoHyXIi58Hpa+Q==",
"license": "MIT"
},
"node_modules/countup.js": {

2
package.json

@ -107,7 +107,7 @@
"color": "5.0.3",
"cookie-parser": "1.4.7",
"countries-and-timezones": "3.9.0",
"countries-list": "3.4.0",
"countries-list": "3.4.1",
"countup.js": "2.10.1",
"date-fns": "4.4.0",
"dotenv": "17.4.2",

Loading…
Cancel
Save