├── requirements.txt ├── get_vacency_rate.py ├── good_investments.txt ├── get_rental_value.py ├── get_growth.py ├── process_properties.py ├── get_from_domain.py ├── get_sale_history_2.py ├── README.md ├── LICENSE └── investment_finder.py /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | bs4 3 | selenium_stealth 4 | pandas 5 | matplotlib 6 | webdriver_manager 7 | numpy_financial 8 | -------------------------------------------------------------------------------- /get_vacency_rate.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.chrome.service import Service 3 | from selenium.webdriver.common.by import By 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from webdriver_manager.chrome import ChromeDriverManager 6 | from selenium.webdriver.support import expected_conditions as EC 7 | from selenium.webdriver.common.action_chains import ActionChains 8 | 9 | def extract_last_tooltip_value(url): 10 | service = Service(ChromeDriverManager().install()) 11 | driver = webdriver.Chrome(service=service) 12 | 13 | try: 14 | driver.get(url) 15 | 16 | # Wait for the Highcharts graph to load 17 | WebDriverWait(driver, 10).until( 18 | EC.presence_of_element_located((By.CSS_SELECTOR, ".highcharts-tooltip")) 19 | ) 20 | 21 | # Find the Highcharts graph element to hover over 22 | graph_element = driver.find_element(By.CSS_SELECTOR, ".highcharts-container") 23 | 24 | # Hover over the graph element to trigger the tooltip 25 | ActionChains(driver).move_to_element(graph_element).perform() 26 | 27 | # Wait for the tooltip to appear 28 | WebDriverWait(driver, 10).until( 29 | EC.presence_of_element_located((By.CSS_SELECTOR, ".highcharts-tooltip")) 30 | ) 31 | 32 | # Find the last tooltip element 33 | tooltips = driver.find_elements(By.CSS_SELECTOR, ".highcharts-tooltip") 34 | if tooltips: 35 | last_tooltip = tooltips[-1] 36 | tooltip_text = last_tooltip.text 37 | print("Last Tooltip Text:", tooltip_text) 38 | else: 39 | print("No tooltip elements found.") 40 | 41 | except Exception as e: 42 | print("An error occurred:", e) 43 | finally: 44 | driver.quit() 45 | 46 | # Example usage 47 | url = "https://sqmresearch.com.au/graph_vacancy.php?postcode=4000" 48 | extract_last_tooltip_value(url) 49 | -------------------------------------------------------------------------------- /good_investments.txt: -------------------------------------------------------------------------------- 1 | Address: 8/2 yangoora road belmore nsw 2192, URL: https://www.domain.com.au/8-2-yangoora-road-belmore-nsw-2192-2018920536, Investment Score: 6.6632996632996635 2 | Address: 87 main road perth tas 7300, URL: https://www.domain.com.au/87-main-road-perth-tas-7300-2018920528, Investment Score: 6.6595165134490975 3 | Address: 14 hampden street south launceston tas 7249, URL: https://www.domain.com.au/14-hampden-street-south-launceston-tas-7249-2018917372, Investment Score: 6.674160964483545 4 | Address: 12 veulalee avenue trevallyn tas 7250, URL: https://www.domain.com.au/12-veulalee-avenue-trevallyn-tas-7250-2018916717, Investment Score: 6.696969696969697 5 | Address: 17 hampden street south launceston tas 7249, URL: https://www.domain.com.au/17-hampden-street-south-launceston-tas-7249-2018914987, Investment Score: 6.674160964483545 6 | Address: 1b clarke way bassendean wa 6054, URL: https://www.domain.com.au/1b-clarke-way-bassendean-wa-6054-2018914220, Investment Score: 6.7245179063360885 7 | Address: 4/45-49 toongabbie road toongabbie nsw 2146, URL: https://www.domain.com.au/4-45-49-toongabbie-road-toongabbie-nsw-2146-2018909190, Investment Score: 6.6632996632996635 8 | Address: 187 rosevears drive rosevears tas 7277, URL: https://www.domain.com.au/187-rosevears-drive-rosevears-tas-7277-2018885744, Investment Score: 6.6632996632996635 9 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 10 | Address: 31/124 terrace road perth wa 6000, URL: https://www.domain.com.au/31-124-terrace-road-perth-wa-6000-2018805392, Investment Score: 4.03980998367421 11 | Address: 31 windsor street invermay tas 7248, URL: https://www.domain.com.au/31-windsor-street-invermay-tas-7248-2018901665, Investment Score: 6.6632996632996635 12 | Address: 4/15 monastery court longford tas 7301, URL: https://www.domain.com.au/4-15-monastery-court-longford-tas-7301-2018901662, Investment Score: 6.696969696969697 13 | Address: 701 deviot road deviot tas 7275, URL: https://www.domain.com.au/701-deviot-road-deviot-tas-7275-2018901661, Investment Score: 6.6632996632996635 14 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 15 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 16 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 17 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 18 | Address: 209/452 st kilda road melbourne vic 3004, URL: https://www.domain.com.au/209-452-st-kilda-road-melbourne-vic-3004-2018826820, Investment Score: 3.967988341374062 19 | -------------------------------------------------------------------------------- /get_rental_value.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.chrome.service import Service 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.webdriver.support import expected_conditions as EC 6 | from webdriver_manager.chrome import ChromeDriverManager 7 | import time 8 | import re 9 | 10 | def calculate_annual_appreciation_rate(current_price, price_5_years_ago): 11 | try: 12 | annual_rate = ((current_price / price_5_years_ago) ** (1 / 5)) - 1 13 | return annual_rate 14 | except ZeroDivisionError: 15 | return None 16 | 17 | 18 | def address_needs_normalization(address): 19 | # Regular expression pattern to check 'number/number-number' 20 | pattern = r'\d+/\d+-\d+' 21 | 22 | # Use re.match or re.search to check if the pattern exists in the address 23 | return re.search(pattern, address) is not None 24 | 25 | def normalize_address(address): 26 | # Regular expression pattern to find 'number/number-number' 27 | pattern = r'(\d+)/(\d+)-\d+' 28 | 29 | # Replacement pattern to change 'number/number-number' to 'number/number' 30 | replacement = r'\1/\2' 31 | 32 | # Replace using regex 33 | normalized_address = re.sub(pattern, replacement, address) 34 | 35 | return normalized_address 36 | 37 | def submit_address_and_get_data(url, address): 38 | data = {} 39 | normalized_address = normalize_address(address) if address_needs_normalization(address) else address 40 | 41 | with webdriver.Chrome(service=Service(ChromeDriverManager().install())) as driver: 42 | driver.get(url) 43 | # Handle cookie consent or any pop-ups 44 | try: 45 | cookie_consent_button = WebDriverWait(driver, 10).until( 46 | EC.element_to_be_clickable((By.CSS_SELECTOR, "button[class*='accept-cookies'],button[id*='cookie'],div[id*='cookie']")) 47 | ) 48 | cookie_consent_button.click() 49 | except Exception: 50 | pass # No cookie consent or pop-up found 51 | 52 | address_field = driver.find_element(By.ID, "da_optional_address_text") 53 | address_field.send_keys(normalized_address) 54 | 55 | WebDriverWait(driver, 10).until( 56 | EC.visibility_of_element_located((By.ID, "da_optional_address_suggest")) 57 | ) 58 | 59 | suggestion_box = driver.find_element(By.ID, "da_optional_address_suggest") 60 | 61 | if "Address/Suburb not found" in suggestion_box.text: 62 | data["Error"] = "Address not found. Please check the address and try again." 63 | return data 64 | 65 | suggestions = suggestion_box.find_elements(By.CLASS_NAME, "da_optional_address_item") 66 | 67 | if suggestions: 68 | driver.execute_script("arguments[0].click();", suggestions[0]) 69 | 70 | submit_button = driver.find_element(By.ID, "da_optional_address_button") 71 | submit_button.click() 72 | 73 | # Wait for the new page to load 74 | time.sleep(5) 75 | 76 | cards = driver.find_elements(By.CLASS_NAME, "da-card") 77 | for card in cards: 78 | title_parts = card.find_elements(By.CLASS_NAME, "da-card-title") 79 | card_title = " ".join([part.text.strip() for part in title_parts]) 80 | 81 | card_value = card.find_element(By.CLASS_NAME, "da-card-value").text.strip() if card.find_elements(By.CLASS_NAME, "da-card-value") else "N/A" 82 | card_footer = card.find_element(By.CLASS_NAME, "da-card-footer").text.strip() if card.find_elements(By.CLASS_NAME, "da-card-footer") else "N/A" 83 | 84 | formatted_title = ' '.join(card_title.split()) 85 | data[formatted_title] = {"Value": card_value, "Footer": card_footer} 86 | 87 | 88 | return data 89 | 90 | # Example usage 91 | #url = "https://www.pulseproperty.com.au" 92 | #address = "6/13-15 lambert street richmond vic 3121" 93 | #data = submit_address_and_get_data(url, address) 94 | #print(data) 95 | 96 | 97 | -------------------------------------------------------------------------------- /get_growth.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.webdriver.support import expected_conditions as EC 6 | import time 7 | from selenium import webdriver 8 | from selenium.webdriver.chrome.options import Options 9 | from selenium.webdriver.chrome.service import Service 10 | from webdriver_manager.chrome import ChromeDriverManager 11 | import random 12 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 13 | from datetime import datetime 14 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 15 | 16 | # List of user agents 17 | user_agents = [ 18 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", # Google Chrome on Windows 10 19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0", # Mozilla Firefox on Windows 10 20 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", # Apple Safari on macOS 21 | "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.58 Mobile Safari/537.36", # Google Chrome on Android 22 | "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1", # Apple Safari on iOS (iPhone) 23 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36 Edg/100.0.1185.36" # Microsoft Edge on Windows 10 24 | ] 25 | 26 | # Choose a random user agent 27 | random_user_agent = random.choice(user_agents) 28 | 29 | # Chrome options for optimal web scraping 30 | chrome_options = Options() 31 | chrome_options.add_argument("--headless") # Run in headless mode 32 | chrome_options.add_argument("--disable-gpu") # Disable GPU acceleration in headless mode 33 | chrome_options.add_argument("--disable-images") # Disable images 34 | chrome_options.add_argument("--incognito") # Use incognito mode 35 | chrome_options.add_argument("--window-size=1920x1080") # Window size 36 | chrome_options.add_argument("--disable-notifications") # Disable notifications 37 | chrome_options.add_argument("--ignore-certificate-errors") # Ignore SSL errors 38 | chrome_options.add_argument(f"user-agent={random_user_agent}") 39 | 40 | # Initialize the driver 41 | driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) 42 | 43 | driver.get("https://www.domain.com.au/property-profile") 44 | 45 | 46 | def calculate_average_growth(growth_values): 47 | """Calculates the average growth from the given growth values.""" 48 | total_growth = 0 49 | count = 0 50 | 51 | for growth in growth_values.values(): 52 | try: 53 | growth_rate = float(growth.strip('%')) 54 | total_growth += growth_rate 55 | count += 1 56 | except ValueError: 57 | pass # Skip invalid or missing values 58 | 59 | if count > 0: 60 | return total_growth / count 61 | else: 62 | return None 63 | 64 | def extract_growth_values(property_type, room_count): 65 | # Initialize the driver 66 | driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) 67 | driver.get("https://www.domain.com.au/suburb-profile/rose-bay-nsw-2029") 68 | 69 | try: 70 | # Wait for the market trends table to load 71 | WebDriverWait(driver, 10).until( 72 | EC.presence_of_element_located((By.CSS_SELECTOR, 'table.css-15dn4s8')) 73 | ) 74 | 75 | # Find all rows in the table 76 | rows = driver.find_elements(By.CSS_SELECTOR, 'tbody[data-test="insight"] > tr') 77 | 78 | growth_values = {} 79 | for row in rows: 80 | try: 81 | bedrooms = row.find_element(By.CSS_SELECTOR, 'td.css-15k02nu').text.strip() 82 | type_ = row.find_element(By.CSS_SELECTOR, 'td.css-mz9cyk').text.strip() 83 | 84 | # Check if the row matches the property type and room count 85 | if type_ == property_type and bedrooms == str(room_count): 86 | # Click to expand the row for more details 87 | expand_button = row.find_element(By.CSS_SELECTOR, 'button[title="Open"]') 88 | expand_button.click() 89 | 90 | # Wait for the expanded section to load and extract growth values 91 | WebDriverWait(driver, 5).until( 92 | EC.presence_of_element_located((By.CSS_SELECTOR, 'div.suburb-insights')) 93 | ) 94 | growth_data = driver.find_elements(By.CSS_SELECTOR, 'table#suburb-insights__table tbody > tr') 95 | for data in growth_data: 96 | year = data.find_element(By.CSS_SELECTOR, 'td:first-child').text.strip() 97 | growth = data.find_element(By.CSS_SELECTOR, 'td[data-testid="suburb-insights__annual-growth-value"]').text.strip() 98 | growth_values[year] = growth 99 | 100 | break 101 | except NoSuchElementException: 102 | print("Required element not found in a row.") 103 | 104 | return calculate_average_growth(growth_values) 105 | 106 | except Exception as e: 107 | print("An error occurred:", e) 108 | finally: 109 | driver.quit() 110 | 111 | 112 | def get_growth(property_type, room_count): 113 | try: 114 | average_growth_rate = extract_growth_values(property_type, room_count) 115 | if average_growth_rate is not None: 116 | formatted_rate = round(average_growth_rate, 2) 117 | print(f"Average Growth Rate: {formatted_rate}%") 118 | return formatted_rate 119 | else: 120 | print("Average Growth Rate could not be calculated.") 121 | return 0 122 | except Exception as e: 123 | print(f"An error occurred: {e}") 124 | return 0 125 | 126 | print ( get_growth("House", "3") ) 127 | -------------------------------------------------------------------------------- /process_properties.py: -------------------------------------------------------------------------------- 1 | import re 2 | from investment_finder import real_estate_investment_analysis, calculate_additional_metrics_and_score 3 | from get_rental_value import submit_address_and_get_data 4 | from get_sale_history_2 import get_appreciate_rate 5 | import time 6 | import random 7 | import sys 8 | import numpy as np 9 | import numpy_financial as npf 10 | 11 | investment_amount_ability = 50000 12 | 13 | def extract_median_asking_rent(data): 14 | for key, value in data.items(): 15 | if "MEDIAN ASKING RENT" in key: 16 | rent_value = value.get("Value", "") 17 | rent_numbers = re.findall(r'\d+', rent_value) 18 | return ''.join(rent_numbers) if rent_numbers else "Data not available" 19 | return "Data not available" 20 | 21 | def write_investment_details_to_file(address, url, investment_score, file_path="good_investments.txt"): 22 | with open(file_path, "a") as file: 23 | file.write(f"Address: {address}, URL: {url}, Investment Score: {investment_score}\n") 24 | 25 | def calculate_break_even(cash_flows): 26 | cumulative_cash_flow = np.cumsum(cash_flows) 27 | break_even_year = np.where(cumulative_cash_flow >= 0)[0] 28 | return break_even_year[0] + 1 if break_even_year.size > 0 else None 29 | 30 | def calculate_roi_and_irr(total_investment, cash_flows, sale_proceeds): 31 | total_cash_flow = sum(cash_flows) + sale_proceeds 32 | roi = ((total_cash_flow - total_investment) / total_investment) * 100 33 | 34 | # Adding a zero initial cash flow for IRR calculation 35 | irr_cash_flows = [-total_investment] + cash_flows + [sale_proceeds] 36 | irr = npf.irr(irr_cash_flows) * 100 37 | 38 | return roi, irr if not np.isnan(irr) else None 39 | 40 | 41 | 42 | def read_and_print_properties(filename): 43 | try: 44 | with open(filename, 'r') as file: 45 | for line in file: 46 | parts = line.strip().split('|') 47 | if len(parts) >= 6: 48 | price, address, url, room_count, bathroom_count, suburb = parts[:6] 49 | numeric_price = int(price.replace(",", "")) 50 | 51 | try: 52 | data = submit_address_and_get_data("https://www.pulseproperty.com.au", address) 53 | rental_value = extract_median_asking_rent(data) 54 | 55 | if rental_value.isdigit(): 56 | rental_value = int(rental_value) 57 | else: 58 | print("Invalid rental value format. Setting default value to 450.") 59 | rental_value = 450 60 | 61 | rental_value = int(rental_value) * 4 62 | 63 | apprec_rate = get_appreciate_rate(address) 64 | 65 | # Print the values 66 | print("Rppreciation Rate:", apprec_rate["appreciation_rate"]) 67 | print("Mid Price", apprec_rate["property_mid_price"]) 68 | 69 | 70 | 71 | est_purchase_price = price 72 | 73 | if apprec_rate["property_mid_price"] is not None: 74 | est_purchase_price = apprec_rate["property_mid_price"] 75 | 76 | #reduced_price = int(est_purchase_price) - int(investment_amount_ability) 77 | reduced_price = int(est_purchase_price) * 0.8 78 | ''' 79 | print( f"Purchase Price: {est_purchase_price}, Address: {address}, URL: {url}, Rooms: {room_count}, " 80 | f"Baths: {bathroom_count}, Suburb: {suburb}, Loan Amount: {reduced_price}, " 81 | f"Appreciate Rate: {apprec_rate['appreciation_rate']}, Rental Value: {rental_value}") 82 | ''' 83 | 84 | analysis_result = real_estate_investment_analysis( 85 | purchase_price=int(est_purchase_price), 86 | renovation_cost=5000, 87 | loan_amount=int(reduced_price), 88 | interest_rate=5.6, 89 | loan_term=20, 90 | rental_income=rental_value, 91 | sale_year=15, 92 | vacancy_rate=1, 93 | operating_expenses=300, 94 | appreciation_rate=apprec_rate['appreciation_rate'], 95 | location_score=1.5, 96 | market_growth_rate=10 97 | ) 98 | 99 | 100 | # After analysis_result = real_estate_investment_analysis(...) 101 | additional_metrics_and_score = calculate_additional_metrics_and_score(analysis_result) 102 | break_even_year = calculate_break_even(analysis_result["Yearly Cash Flows"]) 103 | roi, irr = calculate_roi_and_irr(analysis_result["Total Investment"], analysis_result["Yearly Cash Flows"], analysis_result["Sale Proceeds"]) 104 | 105 | 106 | print(f"Purchase Price: {est_purchase_price}, Address: {address}, URL: {url}, Rooms: {room_count}, " 107 | f"Baths: {bathroom_count}, Suburb: {suburb}, Loan Amount: {reduced_price}, " 108 | f"Appreciate Rate: {apprec_rate['appreciation_rate']}, Rental Value: {rental_value}") 109 | print("Investment Metrics and Score:", additional_metrics_and_score) 110 | print(f"Break-even Year: Year {break_even_year if break_even_year else 'N/A'}, ROI: {roi:.2f}%, IRR: {'{:.2f}%'.format(irr) if irr is not None else 'N/A'}") 111 | print("Total Investment Required (Including Loan):", analysis_result["Total Investment"]) 112 | print("Capital Investment Required (Out-of-Pocket):", analysis_result["Capital Investment Required"]) 113 | print("\n\n") 114 | 115 | # Check if investment score is greater than or equal to 3 and write to file 116 | if additional_metrics_and_score["Investment Score"] >= 3: 117 | write_investment_details_to_file(address, url, additional_metrics_and_score["Investment Score"]) 118 | 119 | 120 | except Exception as inner_e: 121 | print(f"Error processing property {address}: {inner_e}") 122 | 123 | # Generate a random integer between 4 and 10 124 | random_sleep_time = random.randint(5, 10) 125 | time.sleep(random_sleep_time) 126 | 127 | else: 128 | print("Invalid line format:", line.strip()) 129 | except FileNotFoundError: 130 | print(f"File {filename} not found.") 131 | except Exception as e: 132 | print(f"An error occurred: {e}") 133 | 134 | def main(): 135 | read_and_print_properties("properties_data.txt") 136 | 137 | if __name__ == "__main__": 138 | main() 139 | -------------------------------------------------------------------------------- /get_from_domain.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.chrome.options import Options 3 | from bs4 import BeautifulSoup 4 | import time 5 | import re 6 | import random 7 | from selenium_stealth import stealth 8 | 9 | # List of user agents 10 | user_agents = [ 11 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", # Google Chrome on Windows 10 12 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0", # Mozilla Firefox on Windows 10 13 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", # Apple Safari on macOS 14 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36 Edg/100.0.1185.36" # Microsoft Edge on Windows 10 15 | ] 16 | 17 | # Choose a random user agent 18 | random_user_agent = random.choice(user_agents) 19 | 20 | 21 | def setup_driver(): 22 | chrome_options = Options() 23 | chrome_options.add_argument("--headless") 24 | #chrome_options.add_argument("--incognito") # Use incognito mode 25 | #chrome_options.add_argument("--window-size=1920x1080") # Window size 26 | #chrome_options.add_argument(f"user-agent={random_user_agent}") 27 | # s = Service('path/to/chromedriver') # Replace with the path to your chromedriver 28 | driver = webdriver.Chrome(options=chrome_options) 29 | return driver 30 | ''' 31 | def setup_driver(): 32 | chrome_options = Options() 33 | chrome_options.add_argument("--headless") 34 | #chrome_options.add_argument(f"user-agent={random_user_agent}") 35 | #chrome_options.add_argument("--disable-blink-features=AutomationControlled") 36 | #chrome_options.add_argument("--incognito") # Use incognito mode 37 | #chrome_options.add_argument("--window-size=1920x1080") # Window size 38 | 39 | driver = webdriver.Chrome(options=chrome_options) 40 | 41 | # Stealth settings 42 | stealth(driver, 43 | languages=["en-US", "en"], 44 | vendor="Google Inc.", 45 | platform="Win32", 46 | webgl_vendor="Intel Inc.", 47 | renderer="Intel Iris OpenGL Engine", 48 | fix_hairline=True, 49 | ) 50 | 51 | return driver 52 | ''' 53 | def parse_price(price_str): 54 | # Find all numbers in the string 55 | numbers = re.findall(r'\d+(?:,\d+)*(?:\.\d+)?', price_str) 56 | 57 | # Convert found numbers to integers, handling commas and decimals 58 | prices = sorted([int(num.replace(',', '').split('.')[0]) for num in numbers], reverse=True) 59 | 60 | # Return the highest number if available, otherwise return None 61 | return prices[0] if prices else None 62 | 63 | 64 | 65 | def is_valid_price(price_str, max_price): 66 | # Check if the price string contains a valid numerical price 67 | if 'EOI' in price_str or 'Offers' in price_str or 'Auction' in price_str or 'Enquire' in price_str or 'Contact' in price_str: 68 | return False 69 | 70 | price = parse_price(price_str) 71 | #print(f"######: {price}") 72 | return price is not None and price <= max_price 73 | 74 | 75 | 76 | def strip_url_query(url): 77 | return url.split('?')[0] 78 | 79 | def scrape_property_address(driver, property_url): 80 | stripped_url = strip_url_query(property_url) 81 | driver.get(stripped_url) 82 | 83 | soup = BeautifulSoup(driver.page_source, 'html.parser') 84 | address_wrapper = soup.select_one('div[data-testid="listing-details__button-copy-wrapper"] h1') 85 | #print(f"address: {address_wrapper}") # For debugging 86 | 87 | return address_wrapper.text.strip() if address_wrapper else "Address Not Found" 88 | 89 | import sys 90 | 91 | def scrape_page(driver, url, max_price): 92 | driver.get(url) 93 | time.sleep(3) 94 | soup = BeautifulSoup(driver.page_source, 'html.parser') 95 | results = [] 96 | 97 | # Select all listings 98 | listings = soup.select('div.css-qrqvvg') 99 | 100 | for listing in listings: 101 | # Extract price 102 | #print(listing.prettify()) 103 | # sys.exit("asd") 104 | price_tag = listing.select_one('p[data-testid="listing-card-price"]') 105 | if price_tag: 106 | price_text = price_tag.text.strip() 107 | 108 | if is_valid_price(price_text, max_price): 109 | # Extract URL 110 | 111 | link_element = listing.select_one('a[href]') 112 | 113 | if link_element and 'href' in link_element.attrs: 114 | link = link_element['href'] 115 | full_link = f"https://www.domain.com.au{link}" if not link.startswith('http') else link 116 | 117 | # Extract address if needed 118 | address_wrapper = listing.select_one('h2[data-testid="address-wrapper"]') 119 | if address_wrapper: 120 | address_text = address_wrapper.text.replace('\xa0', ' ').replace(',', '').strip().lower() 121 | else: 122 | address_text = "Address Not Found" 123 | 124 | 125 | # Extract suburb name for each individual listing 126 | suburb_container = listing.select_one('span[data-testid="address-line2"]') 127 | if suburb_container: 128 | first_span = suburb_container.find('span') 129 | suburb_name = first_span.get_text(strip=True) if first_span else "Suburb Not Found" 130 | else: 131 | suburb_name = "Suburb Not Found" 132 | 133 | 134 | print(suburb_name) 135 | 136 | # Extract number of bedrooms and bathrooms 137 | beds = listing.select_one('span[data-testid="property-features-text-container"]:contains("Bed")') 138 | baths = listing.select_one('span[data-testid="property-features-text-container"]:contains("Bath")') 139 | num_beds = beds.text.split(' ')[0] if beds else "N/A" 140 | num_baths = baths.text.split(' ')[0] if baths else "N/A" 141 | 142 | # Extract property type 143 | property_type_div = listing.select_one('div.css-11n8uyu span.css-693528') 144 | # Assuming property_type_div contains the text as extracted earlier 145 | if property_type_div: 146 | property_type_text = property_type_div.get_text().strip() 147 | # Check if the extracted text is "House" 148 | if property_type_text == "House": 149 | property_type = "House" 150 | else: 151 | property_type = "Unit" 152 | else: 153 | property_type = "N/A" 154 | 155 | 156 | 157 | 158 | results.append((parse_price(price_text), full_link, address_text, num_beds, num_baths, property_type, suburb_name)) 159 | 160 | 161 | return results 162 | 163 | 164 | 165 | def main(): 166 | driver = setup_driver() 167 | base_url = "https://www.domain.com.au/sale/" 168 | 169 | # Base query parameters 170 | base_params = { 171 | 'ptype': 'apartment-unit-flat,block-of-units,duplex,free-standing,new-apartments,new-home-designs,new-house-land,pent-house,semi-detached,studio,terrace,town-house,villa', 172 | 'price': '200000-555000', 173 | 'establishedtype': 'established' 174 | } 175 | 176 | max_price = 555000 # Adjust maximum price threshold as needed 177 | page = 1 178 | 179 | try: 180 | while True: 181 | print(f"Scraping page {page}...") 182 | 183 | # Add page number to the parameters 184 | params = base_params.copy() 185 | if page > 1: 186 | params['page'] = page 187 | 188 | full_url = f"{base_url}?{'&'.join([f'{k}={v}' for k, v in params.items()])}" 189 | scraped_data = scrape_page(driver, full_url, max_price) 190 | 191 | if not scraped_data: 192 | print("No more data found, stopping...") 193 | break 194 | 195 | # Write data to file 196 | with open('properties_data.txt', 'a') as file: 197 | for price, link, address, num_beds, num_baths, property_type, suburb_name in scraped_data: 198 | #file.write(f"{price}|{address}|{link}\n") 199 | file.write(f"{price}|{address}|{link}|{num_beds}|{num_baths}|{property_type}|{suburb_name}\n") 200 | 201 | page += 1 202 | 203 | finally: 204 | driver.quit() 205 | 206 | if __name__ == "__main__": 207 | main() -------------------------------------------------------------------------------- /get_sale_history_2.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.webdriver.support import expected_conditions as EC 6 | import time 7 | from selenium import webdriver 8 | from selenium.webdriver.chrome.options import Options 9 | from selenium.webdriver.chrome.service import Service 10 | from webdriver_manager.chrome import ChromeDriverManager 11 | import random 12 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 13 | from datetime import datetime 14 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 15 | from selenium.webdriver.support import expected_conditions as EC 16 | from selenium.common.exceptions import NoSuchElementException, TimeoutException, ElementClickInterceptedException 17 | 18 | 19 | 20 | # List of user agents 21 | user_agents = [ 22 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", # Google Chrome on Windows 10 23 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0", # Mozilla Firefox on Windows 10 24 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", # Apple Safari on macOS 25 | "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.58 Mobile Safari/537.36", # Google Chrome on Android 26 | "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1", # Apple Safari on iOS (iPhone) 27 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36 Edg/100.0.1185.36" # Microsoft Edge on Windows 10 28 | ] 29 | 30 | # Choose a random user agent 31 | random_user_agent = random.choice(user_agents) 32 | 33 | # Chrome options for optimal web scraping 34 | chrome_options = Options() 35 | chrome_options.add_argument("--headless") 36 | chrome_options.add_argument("--disable-gpu") 37 | chrome_options.add_argument("--disable-images") 38 | chrome_options.add_argument("--incognito") 39 | chrome_options.add_argument("--window-size=1920x1080") 40 | chrome_options.add_argument("--disable-notifications") 41 | chrome_options.add_argument("--ignore-certificate-errors") 42 | chrome_options.add_argument(f"user-agent={random_user_agent}") 43 | 44 | 45 | def get_average_appreciation_rate(): 46 | # Historical average annual appreciation rate in Australia 47 | return 2 # You can adjust this based on the most recent data or specific market trends 48 | 49 | def click_view_more_results(driver): 50 | try: 51 | view_more_button = WebDriverWait(driver, 10).until( 52 | EC.presence_of_element_located((By.CSS_SELECTOR, 'button.css-e4xbky')) 53 | ) 54 | if view_more_button.is_displayed() and view_more_button.is_enabled(): 55 | view_more_button.click() 56 | time.sleep(2) 57 | except TimeoutException: 58 | print("Timeout waiting for 'View more results' button.") 59 | except NoSuchElementException: 60 | print("'View more results' button not found.") 61 | except ElementClickInterceptedException: 62 | print("Unable to click 'View more results' button.") 63 | except Exception as e: 64 | print(f"Error clicking 'View more results': {e}") 65 | 66 | 67 | 68 | def convert_price(price_text): 69 | price_text = price_text.replace('$', '').strip().upper() 70 | if 'M' in price_text: 71 | return float(price_text.replace('M', '')) * 1000000 72 | elif 'K' in price_text: 73 | return float(price_text.replace('K', '')) * 1000 74 | else: 75 | return float(price_text) 76 | 77 | 78 | def calculate_yearly_appreciation_rate(closest_sale_price, current_mid_price, years_difference): 79 | if closest_sale_price is not None and years_difference > 0: 80 | return (((current_mid_price - closest_sale_price) / closest_sale_price) / years_difference) * 100 81 | return None 82 | 83 | def get_appreciate_rate(address): 84 | with webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) as driver: 85 | driver.get("https://www.domain.com.au/property-profile") 86 | 87 | # Wait for the popup banner to appear 88 | popup_banner = WebDriverWait(driver, 10).until( 89 | EC.presence_of_element_located((By.CSS_SELECTOR, 'section[data-testid="popupbanner-wrapper"]')) 90 | ) 91 | 92 | # Find the close button and click it 93 | close_button = popup_banner.find_element(By.CSS_SELECTOR, 'button[data-testid="popupbanner-wrapper__close-cta"]') 94 | close_button.click() 95 | 96 | # Wait for the input field to be clickable 97 | WebDriverWait(driver, 10).until( 98 | EC.element_to_be_clickable((By.CSS_SELECTOR, 'input[type="search"]')) 99 | ) 100 | 101 | # Find the input field and send the address 102 | input_field = driver.find_element(By.CSS_SELECTOR, 'input[type="search"]') 103 | input_field.send_keys(address) 104 | 105 | # Press the space bar to trigger the list 106 | input_field.send_keys(Keys.SPACE) 107 | 108 | # Wait for the results to load (you might need to adjust the waiting condition here) 109 | WebDriverWait(driver, 10).until( 110 | EC.presence_of_element_located((By.ID, 'downshift-0-item-0')) 111 | ) 112 | 113 | # Click on the first record to make it work 114 | first_record = driver.find_element(By.ID, 'downshift-0-item-0') 115 | first_record.click() 116 | 117 | # Extracting property entries 118 | try: 119 | 120 | 121 | mid_value_text = None 122 | 123 | try: 124 | # Locating all containers that might contain the 'Mid' value 125 | mid_containers = driver.find_elements(By.XPATH, "//div[contains(@class, 'css-8nlvsz')][div[text()='Mid']]") 126 | 127 | # Iterate through each container found 128 | for container in mid_containers: 129 | currency_element = container.find_element(By.XPATH, ".//div[@data-testid='currency']") 130 | mid_value_text = currency_element.text 131 | break 132 | 133 | if mid_value_text is not None: 134 | print(f"Mid Value Text: {mid_value_text}") 135 | else: 136 | print("Mid value text not found.") 137 | 138 | except Exception as e: 139 | print(f"An error occurred: {e}") 140 | 141 | 142 | if mid_value_text is not None: 143 | #mid_price_text = mid_price_element.text 144 | mid_price = convert_price(mid_value_text) 145 | 146 | # print(f"Mid price: ${mid_value_text}") 147 | 148 | # Current year and the target year (5 years ago) 149 | current_year = datetime.now().year 150 | target_year = current_year - 5 151 | 152 | click_view_more_results(driver) 153 | 154 | 155 | 156 | # Waiting for property entries to load 157 | property_entries = WebDriverWait(driver, 10).until( 158 | EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'li.css-16ezjtx')) 159 | ) 160 | 161 | # Initialize variables 162 | closest_sale_year = None 163 | closest_sale_price = None 164 | smallest_year_difference = float('inf') 165 | 166 | # Iterate through each property entry to extract the information 167 | for entry in property_entries: 168 | category_elements = entry.find_elements(By.CSS_SELECTOR, 'div[data-testid="fe-co-property-timeline-card-category"]') 169 | 170 | # Skip entry if category element is not found 171 | if not category_elements: 172 | continue 173 | 174 | category_text = category_elements[0].text 175 | 176 | # Processing if category is 'SOLD' 177 | if category_text == 'SOLD': 178 | year = int(entry.find_element(By.CSS_SELECTOR, 'div.css-1qi20sy').text) 179 | price_text = entry.find_element(By.CSS_SELECTOR, 'span.css-b27lqk').text 180 | price = convert_price(price_text) 181 | 182 | year_difference = abs(year - target_year) 183 | if year_difference < smallest_year_difference: 184 | smallest_year_difference = year_difference 185 | closest_sale_year = year 186 | closest_sale_price = price 187 | 188 | 189 | if closest_sale_price is not None: 190 | print(f"Selected Sale: Year - {closest_sale_year}, Price - ${closest_sale_price}") 191 | years_difference = current_year - closest_sale_year 192 | if years_difference > 0: 193 | appreciation_rate = calculate_yearly_appreciation_rate(closest_sale_price, mid_price, years_difference) 194 | formatted_rate = round(appreciation_rate, 2) 195 | return {"appreciation_rate": formatted_rate, "property_mid_price": mid_price} 196 | else: 197 | print("No appreciation calculation due to same year sale.") 198 | return {"appreciation_rate": get_average_appreciation_rate(), "property_mid_price": mid_price} 199 | else: 200 | print("No suitable sale found within 5 years.") 201 | 202 | return {"appreciation_rate": get_average_appreciation_rate(), "property_mid_price": mid_price} 203 | 204 | except Exception as e: 205 | print("An error occurred:", e) 206 | mid_price = None 207 | return {"appreciation_rate": get_average_appreciation_rate(), "property_mid_price": mid_price} 208 | 209 | # Example usage# 210 | #rate = get_appreciate_rate("14 blaxland drive illawong nsw 2234") 211 | #print( f"Appreciate Rate: {rate['appreciation_rate']}") 212 | 213 | #rate = get_appreciate_rate("25/11 oryx road cable beach wa 6726") 214 | #print(f"Appreciation rate: {rate}%") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Real Estate Investment Analysis Tool 2 | 3 | This tool is designed to analyze real estate investments by calculating key financial metrics and assessing property details. It consists of several functions, each serving a specific purpose in the investment analysis process. 4 | 5 | ### Disclaimer for Web Scraping 6 | 7 | This tool includes functionality for web scraping from real estate websites. It is important to note that this feature is intended strictly for **educational purposes only**. Users are responsible for adhering to the terms of service of the websites they scrape, and this tool should not be used in a way that violates those terms or any applicable laws. Always obtain permission from website owners before scraping their sites. 8 | 9 | 10 | ### Overview 11 | 12 | - **Imported Modules:** The tool imports various modules like `re` for regular expressions, `numpy` and `numpy_financial` for numerical calculations, and custom modules for specific functionalities like `real_estate_investment_analysis`. 13 | - **Global Variable:** `investment_amount_ability` is set, indicating the maximum investment ability (e.g., $50,000). 14 | 15 | ### Key Functions 16 | 17 | #### `extract_median_asking_rent(data)` 18 | Extracts the median asking rent from a given data dictionary. It searches for a key containing "MEDIAN ASKING RENT" and extracts the numerical value. 19 | 20 | #### `write_investment_details_to_file(address, url, investment_score, file_path="good_investments.txt")` 21 | Writes the details of an investment property to a file. It records the address, URL, and investment score of the property. 22 | 23 | #### `calculate_break_even(cash_flows)` 24 | Calculates the break-even year based on yearly cash flows. It uses cumulative cash flow calculations to find the first year where the investment breaks even. 25 | 26 | #### `calculate_roi_and_irr(total_investment, cash_flows, sale_proceeds)` 27 | Calculates the Return on Investment (ROI) and Internal Rate of Return (IRR) for an investment. It uses total investment, yearly cash flows, and sale proceeds for these calculations. 28 | 29 | #### `read_and_print_properties(filename)` 30 | Reads property data from a file, processes each property, and prints detailed analysis. This includes fetching rental values, appreciation rates, and performing a comprehensive investment analysis. 31 | 32 | #### `calculate_stamp_duty(property_value)` 33 | Calculates the stamp duty based on property value, specifically for NSW jurisdiction. This function uses a tier-based calculation method. 34 | 35 | #### `real_estate_investment_analysis(...)` 36 | Performs an in-depth investment analysis. It takes into account various factors like purchase price, loan amount, rental income, etc., and calculates essential investment metrics. 37 | 38 | #### `calculate_additional_metrics_and_score(results)` 39 | Processes the results from the basic analysis and calculates additional investment metrics like Debt Service Coverage Ratio (DSCR), Gross Rent Multiplier (GRM), and Break-Even Point (BEP). 40 | 41 | #### `advanced_investment_analysis(results, risk_factor, exit_strategy_score, local_market_score)` 42 | Enhances the basic investment score by considering additional factors such as risk and local market conditions. 43 | 44 | ### Usage 45 | 46 | 1. **Populate the real estat data:** run the get_domain script first 47 | 2. **Analysis Execution:** Run the process_properties.py 48 | 3. **Output:** The results, including ROI, IRR, and investment scores, are printed out for each property. Properties with a high investment score are written to a separate file. 49 | 50 | ### Note 51 | 52 | - The tool is currently configured for NSW stamp duty calculations and needs adjustments for other jurisdictions. 53 | - The investment amount ability is a configurable global variable that can be adjusted based on user preference. 54 | 55 | 56 | ## Installation and Setup 57 | 58 | Follow these steps to set up your Python environment for running the scripts included in this repository. These instructions assume that Python 3 is already installed on your system. 59 | 60 | ### Creating a Virtual Environment 61 | 62 | A virtual environment is a self-contained directory that contains a Python installation for a particular version of Python, plus a number of additional packages. Creating a virtual environment allows you to manage dependencies for different projects separately. 63 | 64 | 1. **Create the Virtual Environment:** 65 | python3 -m venv .venv 66 | 67 | This command creates a new directory named `.venv` in your current directory, which will contain the Python interpreter and libraries. 68 | 69 | 2. **Activate the Virtual Environment:** 70 | - On macOS and Linux: 71 | ``` 72 | . .venv/bin/activate 73 | ``` 74 | - On Windows: 75 | ``` 76 | .venv\Scripts\activate 77 | ``` 78 | Activating the virtual environment will change your shell’s prompt to show the name of the environment and modify the environment so that running `python` will get you that particular version and installation of Python. 79 | 80 | ### Installing Dependencies 81 | 82 | Once the virtual environment is activated, you can install the required dependencies. 83 | 84 | 1. **Install Required Packages:** 85 | ``` 86 | pip install -r requirements.txt 87 | ``` 88 | 89 | This command will install all the Python packages listed in the `requirements.txt` file. These packages are necessary for the scripts to run properly. 90 | 91 | ### Deactivating the Virtual Environment 92 | 93 | After you finish working in the virtual environment, you can deactivate it by running: 94 | deactivate 95 | 96 | This command will revert your Python environment back to normal. 97 | 98 | ### Note 99 | 100 | - Ensure you have the `requirements.txt` file in the same directory where you are running these commands. 101 | - If you encounter any issues during installation, make sure your Python and `pip` are up-to-date. 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ## process_properties.py 110 | 111 | `process_properties.py` is a Python script designed for analyzing and evaluating real estate investment opportunities. It integrates various functionalities to calculate investment metrics, such as ROI and IRR, and assesses the potential of real estate properties as profitable investments. 112 | 113 | ### Features 114 | 115 | - **Data Extraction and Analysis:** Extracts rental data, calculates appreciation rates, and evaluates investment potential based on various financial metrics. 116 | - **Investment Decision Making:** Determines the break-even year and calculates return on investment (ROI) and internal rate of return (IRR). 117 | - **Output Management:** Prints detailed property analysis to the console and writes promising investment details to a file. 118 | 119 | ### How to Use 120 | 121 | 1. **Set Up:** Ensure all dependencies are installed, including `numpy`, `numpy_financial`, and other required modules. 122 | 2. **Data Input:** Prepare a text file (`properties_data.txt`) with property details in the specified format. 123 | 3. **Running the Script:** Execute the script using Python. The script will read the data file, process each property, and output investment analysis results. 124 | 4. **Output Review:** Review the console output for immediate insights and check `good_investments.txt` for properties with an investment score greater than or equal to 3. 125 | 126 | ### Example Output 127 | 128 | The script outputs various investment metrics, including appreciation rate, purchase price, rental value, break-even year, ROI, and IRR. Example console output for a property might look like: 129 | 130 | 131 | ## get_from_domain.py 132 | 133 | `get_from_domain.py` is a web scraping script specifically tailored to extract real estate listings from the Domain website. It leverages tools like Selenium and BeautifulSoup to navigate the website, parse data, and extract detailed information about properties listed for sale. 134 | 135 | ### Features 136 | 137 | - **Dynamic Web Scraping:** Utilizes Selenium to interact with the Domain website dynamically, accommodating for JavaScript-rendered content. 138 | - **Data Parsing:** Employs BeautifulSoup for extracting and parsing relevant data from the web pages. 139 | - **Customizable Search Parameters:** Supports defining custom search parameters like price range, property type, and others. 140 | - **User-Agent Randomization:** Randomizes user-agent strings to mimic different browsers, helping to reduce the risk of being detected as a bot. 141 | - **Data Extraction:** Gathers comprehensive details about each property, including price, address, URL, number of bedrooms and bathrooms, property type, and suburb. 142 | 143 | ### How to Use 144 | 145 | 1. **Setup:** Ensure Python environment is set up with Selenium, BeautifulSoup, and other required dependencies. 146 | 2. **Driver Configuration:** Set up the Selenium WebDriver to match your system and browser configuration. 147 | 3. **Running the Script:** Execute the script. It will start scraping data from Domain based on the specified search parameters. 148 | 4. **Data Output:** The script writes the extracted property data into `properties_data.txt`, appending new listings to the file. 149 | 150 | 151 | 152 | ## investment_finder.py 153 | 154 | The `investment_finder.py` script is a comprehensive tool designed for analyzing real estate investments. It includes functions for calculating various financial metrics essential for assessing the profitability and viability of real estate investments. 155 | 156 | ### Features 157 | 158 | - **Stamp Duty Calculation:** Calculates stamp duty for properties, currently configured for NSW jurisdiction. 159 | - **Real Estate Investment Analysis:** Conducts a detailed investment analysis considering factors like purchase price, renovation cost, loan amount, and others. 160 | - **Additional Metrics Calculation:** Calculates additional investment metrics such as Debt Service Coverage Ratio (DSCR), Gross Rent Multiplier (GRM), and Break-Even Point (BEP). 161 | - **Advanced Investment Analysis:** Further refines the investment analysis by incorporating risk factors, exit strategy score, and local market score. 162 | 163 | ### Usage 164 | 165 | 1. **Setup:** Ensure Python environment is set up with necessary libraries including `pandas`, `numpy`, and `matplotlib`. 166 | 2. **Data Input:** Input required parameters like purchase price, loan amount, rental income, etc., for investment analysis. 167 | 3. **Executing Functions:** Use the script’s functions to calculate stamp duty, conduct basic and advanced investment analysis, and calculate additional investment metrics. 168 | 4. **Output Review:** Review the output for each function to gain insights into the investment's potential, including ROI, cash-on-cash return, and investment score. 169 | 170 | ### Example Usage 171 | 172 | ```python 173 | # Example of basic investment analysis 174 | basic_investment_results = real_estate_investment_analysis( 175 | purchase_price=500000, # Purchase price of the property 176 | renovation_cost=20000, # Cost of renovations 177 | loan_amount=400000, # Loan amount 178 | interest_rate=4.5, # Interest rate of the loan 179 | loan_term=25, # Term of the loan in years 180 | rental_income=2500, # Monthly rental income 181 | sale_year=15, # Year of property sale 182 | vacancy_rate=5, # Vacancy rate percentage 183 | operating_expenses=500, # Monthly operating expenses 184 | appreciation_rate=2, # Annual property appreciation rate 185 | location_score=2, # Location desirability score 186 | market_growth_rate=3 # Annual market growth rate 187 | ) 188 | 189 | # Print investment results 190 | print("Investment Metrics and Score:", basic_investment_results) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /investment_finder.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | #This is only NSW Stamp duty. You'll need to update the script to handle different states or for your jurisdiction. 6 | def calculate_stamp_duty(property_value): 7 | # Function to calculate stamp duty based on property value 8 | if property_value <= 16000: 9 | return max(1.25 * property_value / 100, 10) 10 | elif property_value <= 35000: 11 | return 200 + 1.50 * (property_value - 16000) / 100 12 | elif property_value <= 93000: 13 | return 485 + 1.75 * (property_value - 35000) / 100 14 | elif property_value <= 351000: 15 | return 1500 + 3.50 * (property_value - 93000) / 100 16 | elif property_value <= 1168000: 17 | return 10530 + 4.50 * (property_value - 351000) / 100 18 | else: 19 | return 47295 + 5.50 * (property_value - 1168000) / 100 20 | 21 | 22 | def real_estate_investment_analysis(purchase_price, renovation_cost, loan_amount, 23 | interest_rate, loan_term, rental_income, 24 | operating_expenses, vacancy_rate, appreciation_rate, 25 | sale_year, location_score, market_growth_rate): 26 | # Validating input data 27 | if purchase_price <= 0 or loan_amount <= 0 or rental_income <= 0: 28 | return "Invalid input data" 29 | 30 | monthly_interest_rate = interest_rate / 12 / 100 31 | number_of_payments = loan_term * 12 32 | 33 | # Monthly mortgage payment calculation 34 | monthly_mortgage = loan_amount * (monthly_interest_rate * (1 + monthly_interest_rate)**number_of_payments) / ((1 + monthly_interest_rate)**number_of_payments - 1) 35 | 36 | yearly_cash_flows, property_values, remaining_loan_balances = [], [], [] 37 | current_loan_balance = loan_amount 38 | property_value = purchase_price + renovation_cost 39 | 40 | # Stamp duty and total initial investment calculation 41 | stamp_duty = calculate_stamp_duty(purchase_price) 42 | total_investment = purchase_price + renovation_cost + stamp_duty 43 | capital_investment_required = total_investment - loan_amount 44 | 45 | for year in range(1, sale_year + 1): 46 | property_value *= (1 + (appreciation_rate + market_growth_rate) / 100) 47 | adjusted_rental_income = rental_income * 12 * (1 - vacancy_rate / 100) 48 | net_operating_income = adjusted_rental_income - (operating_expenses * 12) 49 | annual_mortgage_payment = monthly_mortgage * 12 50 | cash_flow = net_operating_income - annual_mortgage_payment 51 | yearly_cash_flows.append(cash_flow) 52 | property_values.append(property_value) 53 | interest_for_year = current_loan_balance * interest_rate / 100 54 | principal_paid_for_year = annual_mortgage_payment - interest_for_year 55 | current_loan_balance -= principal_paid_for_year 56 | remaining_loan_balances.append(max(current_loan_balance, 0)) 57 | 58 | total_cash_flow = sum(yearly_cash_flows) 59 | sale_proceeds = property_values[-1] - remaining_loan_balances[-1] 60 | roi = ((sale_proceeds + total_cash_flow - total_investment) / total_investment) * 100 * location_score 61 | cash_on_cash_return = (yearly_cash_flows[0] / capital_investment_required) * 100 62 | 63 | # Corrected DSCR, GRM, BEP calculations 64 | annual_rental_income = rental_income * 12 65 | dscr = net_operating_income / annual_mortgage_payment if annual_mortgage_payment > 0 else float('-inf') 66 | grm = purchase_price / annual_rental_income if annual_rental_income > 0 else float('inf') 67 | bep = (operating_expenses * 12) / adjusted_rental_income if adjusted_rental_income > 0 else float('inf') 68 | 69 | # Valid investment check 70 | valid_investment = dscr > 1 and grm > 0 and bep >= 0 and bep <= 1 71 | 72 | # Investment score based on normalized metrics 73 | normalized_dscr = (dscr - 1) if dscr > 1 else 0 74 | normalized_grm = 1 / grm if grm > 0 else 0 75 | normalized_bep = 1 - bep if bep >= 0 and bep <= 1 else 0 76 | investment_score = (normalized_dscr + normalized_grm + normalized_bep) / 3 * 10 if valid_investment else 0 77 | 78 | return { 79 | "Yearly Cash Flows": yearly_cash_flows, 80 | "Property Values": property_values, 81 | "Remaining Loan Balances": remaining_loan_balances, 82 | "Sale Proceeds": sale_proceeds, 83 | "ROI (%)": roi, 84 | "Cash on Cash Return (%)": cash_on_cash_return, 85 | "Stamp Duty": stamp_duty, 86 | "Total Investment": total_investment, 87 | "Capital Investment Required": capital_investment_required, 88 | "Interest Rate": interest_rate, 89 | "Renovation Cost": renovation_cost, 90 | "Operating Expenses": operating_expenses, 91 | "DSCR": dscr, 92 | "GRM": grm, 93 | "BEP": bep, 94 | "Investment Score": investment_score, 95 | "Valid Investment": valid_investment 96 | } 97 | 98 | def calculate_additional_metrics_and_score(results): 99 | # Extract necessary values from the results dictionary 100 | renovation_cost = results["Renovation Cost"] 101 | purchase_price = results["Total Investment"] - results["Stamp Duty"] - renovation_cost 102 | loan_amount = results["Remaining Loan Balances"][0] 103 | interest_rate = results["Interest Rate"] 104 | operating_expenses = results["Operating Expenses"] 105 | annual_rental_income = results["Yearly Cash Flows"][0] + (loan_amount * (interest_rate / 12 / 100) * 12) 106 | 107 | dscr = results["DSCR"] 108 | grm = results["GRM"] 109 | bep = results["BEP"] 110 | 111 | # Adjusting weights for DSCR, GRM, and BEP 112 | weight_dscr = 0.5 # Assuming DSCR is most critical 113 | weight_grm = 0.3 114 | weight_bep = 0.2 115 | 116 | # Assuming valid investment criteria are met 117 | valid_investment = dscr > 1 and grm > 0 and bep >= 0 and bep <= 1 118 | 119 | # Updated normalization and scoring 120 | normalized_dscr = min((results["DSCR"] - 1) / (5 - 1), 1) if results["DSCR"] > 1 else 0 121 | normalized_grm = min((12 - results["GRM"]) / (12 - 1), 1) if results["GRM"] >= 1 and results["GRM"] <= 12 else 0 122 | normalized_bep = 1 - results["BEP"] if results["BEP"] >= 0 and results["BEP"] <= 1 else 0 123 | 124 | # Weighted score calculation 125 | investment_score = (normalized_dscr * weight_dscr + normalized_grm * weight_grm + normalized_bep * weight_bep) * 10 126 | investment_score = min(investment_score, 10) 127 | 128 | return { 129 | "DSCR": dscr, 130 | "GRM": grm, 131 | "BEP": bep, 132 | "Investment Score": investment_score, 133 | "Valid Investment": valid_investment 134 | } 135 | 136 | def advanced_investment_analysis(results, risk_factor, exit_strategy_score, local_market_score): 137 | # Extract the basic investment score 138 | basic_investment_score = results["Investment Score"] 139 | 140 | # Adjust the basic score based on additional factors 141 | adjusted_score = basic_investment_score * (1 - risk_factor) * exit_strategy_score * local_market_score 142 | 143 | return adjusted_score 144 | 145 | ''' 146 | good_investment_results = real_estate_investment_analysis( 147 | purchase_price=250000, # Lower purchase price 148 | renovation_cost=15000, # Modest renovation cost 149 | loan_amount=200000, # Lower loan amount (80% of purchase price) 150 | interest_rate=3.0, # More favorable interest rate 151 | loan_term=30, # Standard loan term 152 | rental_income=3500, # Higher rental income 153 | sale_year=10, # Selling after 10 years 154 | vacancy_rate=3, # Lower vacancy rate 155 | operating_expenses=400, # Lower operating expenses 156 | appreciation_rate=3, # Annual property appreciation rate 157 | location_score=1.5, # Good location score 158 | market_growth_rate=2 # Market growth rate 159 | ) 160 | 161 | 162 | ################################################################################################ 163 | # 164 | # 165 | # TEST DATA - YOU CAN USE THIS TO TEST THE MODEL 166 | # 167 | # 168 | # 169 | # Calculate additional metrics and score 170 | good_additional_metrics_and_score = calculate_additional_metrics_and_score(good_investment_results) 171 | print("Investment Metrics and Score:", good_additional_metrics_and_score) 172 | print("Total Investment Required (Including Loan):", good_investment_results["Total Investment"]) 173 | print("Capital Investment Required (Out-of-Pocket):", good_investment_results["Capital Investment Required"]) 174 | 175 | 176 | # Define your real_estate_investment_analysis and calculate_additional_metrics_and_score functions here 177 | 178 | def print_investment_results(name, results): 179 | metrics_and_score = calculate_additional_metrics_and_score(results) 180 | print(f"{name} Metrics and Score:", metrics_and_score) 181 | print("Total Investment Required (Including Loan):", results["Total Investment"]) 182 | print("Capital Investment Required (Out-of-Pocket):", results["Capital Investment Required"]) 183 | print() 184 | 185 | 186 | 187 | 188 | # Investment scenarios with their respective parameters 189 | investment_scenarios = { 190 | "Terrible Investment": { 191 | "purchase_price": 500000, 192 | "renovation_cost": 50000, 193 | "loan_amount": 450000, 194 | "interest_rate": 6.0, 195 | "loan_term": 15, 196 | "rental_income": 1500, 197 | "sale_year": 5, 198 | "vacancy_rate": 10, 199 | "operating_expenses": 1000, 200 | "appreciation_rate": 1, 201 | "location_score": 0.8, 202 | "market_growth_rate": 1 203 | }, 204 | "Below Average Investment": { 205 | "purchase_price": 350000, 206 | "renovation_cost": 25000, 207 | "loan_amount": 280000, 208 | "interest_rate": 4.5, 209 | "loan_term": 30, 210 | "rental_income": 2200, 211 | "sale_year": 10, 212 | "vacancy_rate": 7, 213 | "operating_expenses": 600, 214 | "appreciation_rate": 1.5, 215 | "location_score": 0.9, 216 | "market_growth_rate": 1.5 217 | }, 218 | "Average Investment": { 219 | "purchase_price": 300000, 220 | "renovation_cost": 20000, 221 | "loan_amount": 240000, 222 | "interest_rate": 4.0, 223 | "loan_term": 30, 224 | "rental_income": 2500, 225 | "sale_year": 10, 226 | "vacancy_rate": 5, 227 | "operating_expenses": 500, 228 | "appreciation_rate": 2, 229 | "location_score": 1.0, 230 | "market_growth_rate": 2 231 | }, 232 | "Above-Average Investment": { 233 | "purchase_price": 250000, 234 | "renovation_cost": 15000, 235 | "loan_amount": 200000, 236 | "interest_rate": 3.5, 237 | "loan_term": 30, 238 | "rental_income": 3200, 239 | "sale_year": 10, 240 | "vacancy_rate": 4, 241 | "operating_expenses": 400, 242 | "appreciation_rate": 3, 243 | "location_score": 1.2, 244 | "market_growth_rate": 2.5 245 | }, 246 | "Top Investment": { 247 | "purchase_price": 200000, 248 | "renovation_cost": 10000, 249 | "loan_amount": 160000, 250 | "interest_rate": 2.5, 251 | "loan_term": 30, 252 | "rental_income": 3500, 253 | "sale_year": 10, 254 | "vacancy_rate": 2, 255 | "operating_expenses": 300, 256 | "appreciation_rate": 4, 257 | "location_score": 1.5, 258 | "market_growth_rate": 3 259 | }, 260 | "Terrible Investment": { 261 | "purchase_price": 500000, 262 | "renovation_cost": 50000, 263 | "loan_amount": 450000, 264 | "interest_rate": 6.0, 265 | "loan_term": 15, 266 | "rental_income": 1500, 267 | "sale_year": 5, 268 | "vacancy_rate": 10, 269 | "operating_expenses": 1000, 270 | "appreciation_rate": 1, 271 | "location_score": 0.8, 272 | "market_growth_rate": 1 273 | }, 274 | "Below Average Investment": { 275 | "purchase_price": 350000, 276 | "renovation_cost": 25000, 277 | "loan_amount": 280000, 278 | "interest_rate": 4.5, 279 | "loan_term": 30, 280 | "rental_income": 2200, 281 | "sale_year": 10, 282 | "vacancy_rate": 7, 283 | "operating_expenses": 600, 284 | "appreciation_rate": 1.5, 285 | "location_score": 0.9, 286 | "market_growth_rate": 1.5 287 | }, 288 | "Average Investment": { 289 | "purchase_price": 300000, 290 | "renovation_cost": 20000, 291 | "loan_amount": 240000, 292 | "interest_rate": 4.0, 293 | "loan_term": 30, 294 | "rental_income": 2500, 295 | "sale_year": 10, 296 | "vacancy_rate": 5, 297 | "operating_expenses": 500, 298 | "appreciation_rate": 2, 299 | "location_score": 1.0, 300 | "market_growth_rate": 2 301 | }, 302 | "Above-Average Investment": { 303 | "purchase_price": 250000, 304 | "renovation_cost": 15000, 305 | "loan_amount": 200000, 306 | "interest_rate": 3.5, 307 | "loan_term": 30, 308 | "rental_income": 3200, 309 | "sale_year": 10, 310 | "vacancy_rate": 4, 311 | "operating_expenses": 400, 312 | "appreciation_rate": 3, 313 | "location_score": 1.2, 314 | "market_growth_rate": 2.5 315 | }, 316 | "Top Investment": { 317 | "purchase_price": 200000, 318 | "renovation_cost": 10000, 319 | "loan_amount": 160000, 320 | "interest_rate": 2.5, 321 | "loan_term": 30, 322 | "rental_income": 3500, 323 | "sale_year": 10, 324 | "vacancy_rate": 2, 325 | "operating_expenses": 300, 326 | "appreciation_rate": 4, 327 | "location_score": 1.5, 328 | "market_growth_rate": 3 329 | }, 330 | # Example 6: Moderate Investment 331 | "Moderate Investment": { 332 | "purchase_price": 320000, 333 | "renovation_cost": 25000, 334 | "loan_amount": 260000, 335 | "interest_rate": 5.8, 336 | "loan_term": 20, 337 | "rental_income": 2600, 338 | "sale_year": 12, 339 | "vacancy_rate": 6, 340 | "operating_expenses": 550, 341 | "appreciation_rate": 2.5, 342 | "location_score": 1.1, 343 | "market_growth_rate": 10 344 | }, 345 | 346 | # Example 7: Steady Growth Investment 347 | "Steady Growth Investment": { 348 | "purchase_price": 280000, 349 | "renovation_cost": 20000, 350 | "loan_amount": 225000, 351 | "interest_rate": 5.8, 352 | "loan_term": 25, 353 | "rental_income": 2400, 354 | "sale_year": 15, 355 | "vacancy_rate": 5, 356 | "operating_expenses": 450, 357 | "appreciation_rate": 3, 358 | "location_score": 1.2, 359 | "market_growth_rate": 10 360 | }, 361 | 362 | # Example 8: High Yield Investment 363 | "High Yield Investment": { 364 | "purchase_price": 400000, 365 | "renovation_cost": 30000, 366 | "loan_amount": 350000, 367 | "interest_rate": 5.8, 368 | "loan_term": 15, 369 | "rental_income": 3200, 370 | "sale_year": 10, 371 | "vacancy_rate": 4, 372 | "operating_expenses": 600, 373 | "appreciation_rate": 4, 374 | "location_score": 1.3, 375 | "market_growth_rate": 10 376 | }, 377 | 378 | # Example 9: Low Cost Investment 379 | "Low Cost Investment": { 380 | "purchase_price": 180000, 381 | "renovation_cost": 10000, 382 | "loan_amount": 150000, 383 | "interest_rate": 5.8, 384 | "loan_term": 30, 385 | "rental_income": 1800, 386 | "sale_year": 8, 387 | "vacancy_rate": 7, 388 | "operating_expenses": 350, 389 | "appreciation_rate": 2, 390 | "location_score": 0.9, 391 | "market_growth_rate": 10 392 | }, 393 | 394 | # Example 10: Expansive Investment 395 | "Expansive Investment": { 396 | "purchase_price": 450000, 397 | "renovation_cost": 40000, 398 | "loan_amount": 400000, 399 | "interest_rate": 5.8, 400 | "loan_term": 20, 401 | "rental_income": 3000, 402 | "sale_year": 12, 403 | "vacancy_rate": 3, 404 | "operating_expenses": 700, 405 | "appreciation_rate": 3.5, 406 | "location_score": 1.4, 407 | "market_growth_rate": 10 408 | }, 409 | # Example 11: Balanced Growth Investment 410 | "Balanced Growth Investment": { 411 | "purchase_price": 310000, 412 | "renovation_cost": 18000, 413 | "loan_amount": 248000, 414 | "interest_rate": 5.8, 415 | "loan_term": 25, 416 | "rental_income": 2900, 417 | "sale_year": 10, 418 | "vacancy_rate": 5, 419 | "operating_expenses": 480, 420 | "appreciation_rate": 3.2, 421 | "location_score": 1.1, 422 | "market_growth_rate": 10 423 | }, 424 | 425 | # Example 12: High-Potential Investment 426 | "High-Potential Investment": { 427 | "purchase_price": 360000, 428 | "renovation_cost": 22000, 429 | "loan_amount": 290000, 430 | "interest_rate": 5.8, 431 | "loan_term": 30, 432 | "rental_income": 3100, 433 | "sale_year": 12, 434 | "vacancy_rate": 4, 435 | "operating_expenses": 520, 436 | "appreciation_rate": 4.5, 437 | "location_score": 1.3, 438 | "market_growth_rate": 10 439 | }, 440 | 441 | # Example 13: Long-Term Stable Investment 442 | "Long-Term Stable Investment": { 443 | "purchase_price": 270000, 444 | "renovation_cost": 15000, 445 | "loan_amount": 216000, 446 | "interest_rate": 5.8, 447 | "loan_term": 20, 448 | "rental_income": 2500, 449 | "sale_year": 15, 450 | "vacancy_rate": 6, 451 | "operating_expenses": 420, 452 | "appreciation_rate": 2.8, 453 | "location_score": 1.0, 454 | "market_growth_rate": 10 455 | }, 456 | 457 | # Example 14: Urban Core Investment 458 | "Urban Core Investment": { 459 | "purchase_price": 420000, 460 | "renovation_cost": 35000, 461 | "loan_amount": 378000, 462 | "interest_rate": 5.8, 463 | "loan_term": 25, 464 | "rental_income": 3400, 465 | "sale_year": 8, 466 | "vacancy_rate": 3, 467 | "operating_expenses": 600, 468 | "appreciation_rate": 4, 469 | "location_score": 1.4, 470 | "market_growth_rate": 10 471 | }, 472 | 473 | # Example 15: Niche Market Investment 474 | "Niche Market Investment": { 475 | "purchase_price": 200000, 476 | "renovation_cost": 12000, 477 | "loan_amount": 180000, 478 | "interest_rate": 5.8, 479 | "loan_term": 30, 480 | "rental_income": 1900, 481 | "sale_year": 7, 482 | "vacancy_rate": 8, 483 | "operating_expenses": 320, 484 | "appreciation_rate": 2.2, 485 | "location_score": 0.9, 486 | "market_growth_rate": 10 487 | }, 488 | 489 | # Example 16: Speculative Investment 490 | "Speculative Investment": { 491 | "purchase_price": 500000, 492 | "renovation_cost": 60000, 493 | "loan_amount": 450000, 494 | "interest_rate": 5.8, 495 | "loan_term": 15, 496 | "rental_income": 2800, 497 | "sale_year": 5, 498 | "vacancy_rate": 10, 499 | "operating_expenses": 800, 500 | "appreciation_rate": 5, 501 | "location_score": 1.5, 502 | "market_growth_rate": 10 503 | }, 504 | 505 | # Example 17: Suburban Family Investment 506 | "Suburban Family Investment": { 507 | "purchase_price": 330000, 508 | "renovation_cost": 20000, 509 | "loan_amount": 264000, 510 | "interest_rate": 5.8, 511 | "loan_term": 20, 512 | "rental_income": 2700, 513 | "sale_year": 12, 514 | "vacancy_rate": 5, 515 | "operating_expenses": 500, 516 | "appreciation_rate": 3.5, 517 | "location_score": 1.2, 518 | "market_growth_rate": 10 519 | }, 520 | 521 | # Example 18: Retirement Investment 522 | "Retirement Investment": { 523 | "purchase_price": 240000, 524 | "renovation_cost": 18000, 525 | "loan_amount": 192000, 526 | "interest_rate": 5.8, 527 | "loan_term": 30, 528 | "rental_income": 2000, 529 | "sale_year": 15, 530 | "vacancy_rate": 7, 531 | "operating_expenses": 400, 532 | "appreciation_rate": 2.5, 533 | "location_score": 1.0, 534 | "market_growth_rate": 10 535 | }, 536 | 537 | # Example 19: Growth Focused Investment 538 | "Growth Focused Investment": { 539 | "purchase_price": 400000, 540 | "renovation_cost": 30000, 541 | "loan_amount": 360000, 542 | "interest_rate": 5.8, 543 | "loan_term": 25, 544 | "rental_income": 3300, 545 | "sale_year": 10, 546 | "vacancy_rate": 4, 547 | "operating_expenses": 550, 548 | "appreciation_rate": 4.2, 549 | "location_score": 1.3, 550 | "market_growth_rate": 10 551 | }, 552 | 553 | # Example 20: Small Town Stable Investment 554 | "Small Town Stable Investment": { 555 | "purchase_price": 180000, 556 | "renovation_cost": 10000, 557 | "loan_amount": 162000, 558 | "interest_rate": 5.8, 559 | "loan_term": 30, 560 | "rental_income": 1600, 561 | "sale_year": 10, 562 | "vacancy_rate": 6, 563 | "operating_expenses": 300, 564 | "appreciation_rate": 2, 565 | "location_score": 0.8, 566 | "market_growth_rate": 10 567 | }, 568 | "Score 7 Investment": { 569 | "purchase_price": 230000, 570 | "renovation_cost": 20000, 571 | "loan_amount": 184000, # 80% of purchase price 572 | "interest_rate": 4.0, 573 | "loan_term": 30, 574 | "rental_income": 3200, # High rental income for lower GRM 575 | "sale_year": 10, 576 | "vacancy_rate": 3, 577 | "operating_expenses": 350, # Lower operating expenses for higher DSCR 578 | "appreciation_rate": 3.5, 579 | "location_score": 1.2, 580 | "market_growth_rate": 3 581 | }, 582 | 583 | # Investment for Score ~8 584 | "Score 8 Investment": { 585 | "purchase_price": 220000, 586 | "renovation_cost": 15000, 587 | "loan_amount": 176000, # 80% of purchase price 588 | "interest_rate": 3.5, 589 | "loan_term": 30, 590 | "rental_income": 3300, # Higher rental income 591 | "sale_year": 10, 592 | "vacancy_rate": 2, 593 | "operating_expenses": 300, # Lower operating expenses 594 | "appreciation_rate": 4, 595 | "location_score": 1.3, 596 | "market_growth_rate": 3.5 597 | }, 598 | 599 | # Investment for Score ~9 600 | "Score 9 Investment": { 601 | "purchase_price": 210000, 602 | "renovation_cost": 10000, 603 | "loan_amount": 168000, # 80% of purchase price 604 | "interest_rate": 3.0, 605 | "loan_term": 30, 606 | "rental_income": 3400, # Very high rental income 607 | "sale_year": 10, 608 | "vacancy_rate": 1, 609 | "operating_expenses": 250, # Very low operating expenses 610 | "appreciation_rate": 4.5, 611 | "location_score": 1.4, 612 | "market_growth_rate": 4 613 | }, 614 | 615 | # Investment for Score ~10 616 | "Score 10 Investment": { 617 | "purchase_price": 200000, 618 | "renovation_cost": 5000, 619 | "loan_amount": 160000, # 80% of purchase price 620 | "interest_rate": 2.5, 621 | "loan_term": 30, 622 | "rental_income": 3500, # Exceptionally high rental income 623 | "sale_year": 10, 624 | "vacancy_rate": 0, 625 | "operating_expenses": 200, # Minimal operating expenses 626 | "appreciation_rate": 5, 627 | "location_score": 1.5, 628 | "market_growth_rate": 4.5 629 | } 630 | } 631 | 632 | # Processing each investment scenario 633 | for name, params in investment_scenarios.items(): 634 | results = real_estate_investment_analysis(**params) 635 | print_investment_results(name, results) 636 | 637 | ''' 638 | 639 | 640 | 641 | --------------------------------------------------------------------------------