🤣
class ViewController: UIViewController {
@IBOutlet weak var tickerTextField: UITextField!
@IBOutlet weak var stochasticLabel: UILabel!
@IBOutlet weak var ichimokuLabel: UILabel!
@IBOutlet weak var bollingerBandsLabel: UILabel!
@IBOutlet weak var movingAveragesLabel: UILabel!
@IBOutlet weak var obvLabel: UILabel!
@IBOutlet weak var fibonacciLabel: UILabel!
@IBOutlet weak var rsiLabel: UILabel!
@IBOutlet weak var adxLabel: UILabel!
@IBOutlet weak var adlLabel: UILabel!
@IBOutlet weak var macdLabel: UILabel!
let errorMessageLabel = UILabel()
let closeButton = UIButton(type: .system)
let apiKey = "1RA8WZLKFJNH7B8H"
var symbol = "AAPL" // Default to Apple, can be updated via search
let baseUrl = "https://www.alphavantage.co/query"
var isFetchingData = false
var previousObv: Double? // To store the previous OBV value
// Declare your arrays for price and volume data
var closingPrices: [Double] = []
var highPrices: [Double] = []
var lowPrices: [Double] = []
var volumes: [Double] = []
// Moving Averages
var shortTermMA: Double = 0.0
var longTermMA: Double = 0.0
// MACD-related data
var macd: Double = 0.0
var signalLine: Double = 0.0
var macdBuySignal: Bool = false
var macdSellSignal: Bool = false
var macdHoldSignal: Bool = false
// RSI-related data
var rsi: Double = 0.0
var rsiBuySignal: Bool = false
var rsiSellSignal: Bool = false
var rsiHoldSignal: Bool = false
// OBV-related data
var obv: Double = 0.0
var obvBuySignal: Bool = false
var obvSellSignal: Bool = false
var obvHoldSignal: Bool = false
// ADL-related data
var adl: Double = 0.0
var adlBuySignal: Bool = false
var adlSellSignal: Bool = false
var adlHoldSignal: Bool = false
// Bollinger Bands-related data
var upperBand: Double = 0.0
var middleBand: Double = 0.0
var lowerBand: Double = 0.0
var bbBuySignal: Bool = false
var bbSellSignal: Bool = false
var bbHoldSignal: Bool = false
// ADX-related data
var adx: Double = 0.0
var adxBuySignal: Bool = false
var adxSellSignal: Bool = false
var adxHoldSignal: Bool = false
// Fibonacci Retracement-related data
var fibBuySignal: Bool = false
var fibSellSignal: Bool = false
var fibHoldSignal: Bool = false
// Ichimoku Cloud-related data
var ichimokuBuySignal: Bool = false
var ichimokuSellSignal: Bool = false
var ichimokuHoldSignal: Bool = false
// Stochastic-related data
var stochasticK: Double = 0.0
var stochasticD: Double = 0.0
override func viewDidLoad() {
super.viewDidLoad()
setupErrorMessageLabel()
setupCloseButton()
}
func setupErrorMessageLabel() {
errorMessageLabel.translatesAutoresizingMaskIntoConstraints = false
errorMessageLabel.numberOfLines = 0
errorMessageLabel.lineBreakMode = .byWordWrapping
errorMessageLabel.textColor = .red
errorMessageLabel.textAlignment = .center
errorMessageLabel.isHidden = true
errorMessageLabel.backgroundColor = .white
errorMessageLabel.layer.cornerRadius = 10
errorMessageLabel.layer.masksToBounds = true
view.addSubview(errorMessageLabel)
// Set constraints for the error message label to be centered and square
NSLayoutConstraint.activate([
errorMessageLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
errorMessageLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
errorMessageLabel.widthAnchor.constraint(equalToConstant: 300), // Set a fixed width (square size)
errorMessageLabel.heightAnchor.constraint(equalTo: errorMessageLabel.widthAnchor) // Make it square
])
}
func setupCloseButton() {
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.setTitle("X", for: .normal)
closeButton.addTarget(self, action: #selector(hideErrorMessage), for: .touchUpInside)
closeButton.isHidden = true
view.addSubview(closeButton)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: errorMessageLabel.topAnchor, constant: 8),
closeButton.trailingAnchor.constraint(equalTo: errorMessageLabel.trailingAnchor, constant: -8)
])
}
func updateErrorMessage(_ message: String) {
errorMessageLabel.text = message
errorMessageLabel.isHidden = false
closeButton.isHidden = false
view.bringSubviewToFront(errorMessageLabel)
view.bringSubviewToFront(closeButton)
}
@objc func hideErrorMessage() {
errorMessageLabel.isHidden = true
closeButton.isHidden = true
}
// Function triggered by search button tap
@IBAction func searchButtonTapped(_ sender: UIButton) {
guard let symbolText = tickerTextField.text, !symbolText.isEmpty else {
showError(message: "Please enter a valid stock symbol.")
return
}
symbol = symbolText.uppercased()
if isFetchingData {
return // Prevent multiple fetch requests while one is in progress
}
fetchStockData(ticker: newTicker) { [weak self] stockData, error in
guard let self = self else { return }
// Handle error
if let error = error {
// Assuming `processError` is a method that displays errors in your app.
self.processError(message: error.localizedDescription) // Use your actual error handling method here
} else if let stockData = stockData {
// Process the received stock data
self.processStockData(stockData) // Make sure `processStockData` is the correct method to process data
}
}
// Function to fetch stock data from Alpha Vantage
func fetchStockData(ticker: String, completion: @escaping ([String: DailyAdjustedData]?, Error?) -> Void) {
let urlString = "\(baseUrl)?function=TIME_SERIES_DAILY_ADJUSTED&symbol=\(ticker)&apikey=\(apiKey)"
// Print the URL string for debugging
print("URL String: \(urlString)")
guard let url = URL(string: urlString) else {
completion(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
return
}
isFetchingData = true // Ensure this is set to true to avoid multiple fetch requests
URLSession.shared.dataTask(with: url) { data, response, error in
defer { self.isFetchingData = false } // Ensure it's reset to false when the task finishes
if let error = error {
print("Network error: \(error)")
completion(nil, error)
return
}
// Check for HTTP response status code
if let httpResponse = response as? HTTPURLResponse {
print("HTTP Status Code: \(httpResponse.statusCode)")
guard (200...299).contains(httpResponse.statusCode) else {
completion(nil, NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Server returned an error"]))
return
}
}
guard let data = data else {
completion(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data received."]))
return
}
// Print the raw JSON response for debugging
if let jsonString = String(data: data, encoding: .utf8) {
print("Raw JSON Response: \(jsonString)")
}
let decoder = JSONDecoder()
do {
let response = try decoder.decode(TimeSeriesDailyAdjusted.self, from: data)
if let errorMessage = response.errorMessage {
completion(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: errorMessage]))
} else {
completion(response.timeSeries, nil)
self.cacheStockData(stockData: response.timeSeries)
}
} catch let decodingError as DecodingError {
self.handleDecodingError(decodingError)
completion(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to decode response."]))
} catch {
print("Unexpected error: \(error)")
completion(nil, error)
}
}.resume()
}
func handleDecodingError(_ error: DecodingError) {
switch error {
case .dataCorrupted(let context):
print("Data corrupted: \(context.debugDescription)")
case .keyNotFound(let key, let context):
print("Key \(key) not found: \(context.debugDescription)")
case .typeMismatch(let type, let context):
print("Type mismatch for type \(type): \(context.debugDescription)")
case .valueNotFound(let type, let context):
print("Value not found for type \(type): \(context.debugDescription)")
@unknown default:
print("Unknown decoding error: \(error)")
}
}
// Function to process fetched stock data and calculate indicators
func processStockData(_ stockData: [String: DailyAdjustedData]) {
// Ensure there is data to process
guard !stockData.isEmpty else {
print("No stock data available to process.")
return
}
// Convert optional String? values to Double, defaulting to 0.0 if conversion fails
let closingPrices = stockData.values.compactMap { Double($0.close) }
let highPrices = stockData.values.compactMap { Double($0.high) }
let lowPrices = stockData.values.compactMap { Double($0.low) }
let volumes = stockData.values.compactMap { Double($0.volume) }
_ = stockData.values.compactMap { Double($0.open) }
let adjustedClosingPrices = stockData.values.compactMap { Double($0.adjustedClose) }
// Check if closingPrices and adjustedClosingPrices are consistent
guard closingPrices.count == adjustedClosingPrices.count else {
print("Mismatch in closing prices and adjusted closing prices count.")
return
}
// Sort data by date (if needed)
_ = stockData.sorted { $0.key < $1.key }
// Proceed with your calculations
let shortTermMA = calculateMovingAverage(prices: closingPrices, period: 50)
let longTermMA = calculateMovingAverage(prices: closingPrices, period: 200)
updateMovingAveragesLabel(shortTermMA: shortTermMA, longTermMA: longTermMA)
// MACD Calculation
let (macd, signalLine, macdBuySignal, macdSellSignal, macdHoldSignal) = calculateMacd(prices: closingPrices)
updateMacdLabel(macd: macd, signalLine: signalLine, buySignal: macdBuySignal, sellSignal: macdSellSignal, holdSignal: macdHoldSignal)
// RSI Calculation
let (rsi, rsiBuySignal, rsiSellSignal, rsiHoldSignal) = calculateRsi(prices: closingPrices)
updateRsiLabel(rsi: rsi, buySignal: rsiBuySignal, sellSignal: rsiSellSignal, holdSignal: rsiHoldSignal)
// OBV Calculation
let (obv, _, _, _) = calculateObv(prices: closingPrices, volumes: volumes)
let previousPrice = closingPrices.dropLast().last ?? 0.0
updateObvLabel(obv: obv, price: closingPrices.last ?? 0.0, previousPrice: previousPrice)
// ADL Calculation
let (adl, _, _, _) = calculateAdl(prices: closingPrices, highPrices: highPrices, lowPrices: lowPrices, volumes: volumes)
updateAdlLabel(adl: adl)
// Bollinger Bands Calculation
let (upperBand, _, lowerBand, _, _, _) = calculateBollingerBandsWithSignals(prices: closingPrices)
updateBollingerBandsLabel(currentPrice: closingPrices.last ?? 0.0, upperBand: upperBand, lowerBand: lowerBand)
// ADX Calculation
let (adx, _, _, _) = calculateAdxWithSignals(prices: closingPrices, highPrices: highPrices, lowPrices: lowPrices)
updateAdxLabel(adx: adx)
// Fibonacci Retracement Calculation
let (fibRetracementLevel, _, _, _) = calculateFibonacciRetracementWithSignals(prices: closingPrices)
updateFibonacciLabel(price: closingPrices.last ?? 0.0, fibRetracementLevel: fibRetracementLevel)
// Ichimoku Cloud Calculation
let (_, _, _) = calculateIchimokuCloudWithSignals(highPrices: highPrices, lowPrices: lowPrices, closePrices: closingPrices)
updateIchimokuLabel(currentPrice: closingPrices.last ?? 0.0, tenkanSen: 0.0, kijunSen: 0.0, priceAboveCloud: true) // Adjust accordingly
// Stochastic Calculation
let (stochasticK, stochasticD) = calculateStochastic(prices: closingPrices, highPrices: highPrices, lowPrices: lowPrices)
updateStochasticLabel(stochasticK: stochasticK, stochasticD: stochasticD)
}
func showError(message: String) {
DispatchQueue.main.async {
self.errorMessageLabel.isHidden = false
self.errorMessageLabel.text = message
}
}
struct CachedStockData: Codable {
let data: [String: DailyAdjustedData]
let timestamp: Date
}
// Global variable to track when new data is fetched
var isNewDataFetched = false
func loadCachedStockData() -> [String: DailyAdjustedData]? {
guard let savedData = UserDefaults.standard.object(forKey: "cachedStockData") as? Data else { return nil }
let decoder = JSONDecoder()
do {
let loadedData = try decoder.decode([String: DailyAdjustedData].self, from: savedData)
// Check if the market is open (or if a certain time has passed since last data fetch)
if let lastUpdated = UserDefaults.standard.object(forKey: "lastUpdated") as? Date {
let currentTime = Date()
let timeDifference = currentTime.timeIntervalSince(lastUpdated)
// If data was fetched more than X minutes ago, allow a re-fetch
if timeDifference < 60 * 10 { // 10 minutes, adjust as needed
isNewDataFetched = false // No new data, don't trigger recalculations
return loadedData // Use cached data
}
}
// Set flag when new data is fetched
isNewDataFetched = true
return loadedData // Use cached data if no recent update found
} catch {
print("Failed to decode stock data: \(error)")
return nil
}
}
func cacheStockData(stockData: [String: DailyAdjustedData]) {
let encoder = JSONEncoder()
do {
// Include a timestamp to track when the data was cached
let dataWithTimestamp = CachedStockData(data: stockData, timestamp: Date())
let encoded = try encoder.encode(dataWithTimestamp)
UserDefaults.standard.set(encoded, forKey: "cachedStockData")
UserDefaults.standard.set(Date(), forKey: "lastUpdated") // Store the timestamp of the cache
// Set flag to indicate new data was fetched and cached
isNewDataFetched = true
} catch {
print("Failed to encode stock data: \(error)")
}
}
// Function to check if recalculations are needed
func shouldRecalculateIndicators() -> Bool {
return isNewDataFetched // Only recalculate if new data was fetched
}
func resetData() {
// Resetting price data
closingPrices = []
highPrices = []
lowPrices = []
volumes = []
// Resetting moving averages
shortTermMA = 0.0
longTermMA = 0.0
// Resetting MACD-related data
macd = 0.0
signalLine = 0.0
macdBuySignal = false
macdSellSignal = false
macdHoldSignal = false
// Resetting RSI-related data
rsi = 0.0
rsiBuySignal = false
rsiSellSignal = false
rsiHoldSignal = false
// Resetting OBV-related data
obv = 0.0
obvBuySignal = false
obvSellSignal = false
obvHoldSignal = false
// Resetting ADL-related data
adl = 0.0
adlBuySignal = false
adlSellSignal = false
adlHoldSignal = false
// Resetting Bollinger Bands-related data
upperBand = 0.0
middleBand = 0.0
lowerBand = 0.0
bbBuySignal = false
bbSellSignal = false
bbHoldSignal = false
// Resetting ADX-related data
adx = 0.0
adxBuySignal = false
adxSellSignal = false
adxHoldSignal = false
// Resetting Fibonacci Retracement-related data
fibBuySignal = false
fibSellSignal = false
fibHoldSignal = false
// Resetting Ichimoku Cloud-related data
ichimokuBuySignal = false
ichimokuSellSignal = false
ichimokuHoldSignal = false
// Resetting Stochastic-related data
stochasticK = 0.0
stochasticD = 0.0
}
func handleTickerChange(newTicker: String, completion: @escaping ([String: DailyAdjustedData]?, Error?) -> Void) {
resetData() // Reset any previous data
fetchStockData(ticker: newTicker) { data, error in
if let error = error {
completion(nil, error) // Pass error to completion handler
} else if let data = data {
completion(data, nil) // Pass the fetched data to completion handler
}
}
}
@IBAction func tickerInputChanged(_ sender: UITextField) {
guard let newTicker = sender.text else { return }
handleTickerChange(newTicker: newTicker) { data, error in
if let error = error {
print("Error fetching data: \(error)")
// Handle error (e.g., show alert)
} else if let data = data {
print("Fetched data: \(data)")
// Process the fetched data (e.g., update UI)
}
}
}
// **Helper Functions for Stock Indicators**
func calculateStochastic(prices: [Double], highPrices: [Double], lowPrices: [Double], period: Int = 14) -> (stochasticK: Double, stochasticD: Double) {
guard shouldRecalculateIndicators() else { return (0.0, 0.0) }
guard prices.count >= period, highPrices.count >= period, lowPrices.count >= period else {
return (stochasticK: 0.0, stochasticD: 0.0) // Return 0 if there aren't enough data points
}
// Calculate the highest high and the lowest low over the period
let recentHighs = highPrices.suffix(period)
let recentLows = lowPrices.suffix(period)
let recentCloses = prices.suffix(period)
let highestHigh = recentHighs.max() ?? 0.0
let lowestLow = recentLows.min() ?? 0.0
let currentClose = recentCloses.last ?? 0.0
// Calculate %K (the Fast Stochastic)
let stochasticK = (highestHigh - lowestLow) != 0 ? (currentClose - lowestLow) / (highestHigh - lowestLow) * 100 : 0.0
// Calculate %D (the Slow Stochastic) as the moving average of %K over the last 3 periods
let kValues = recentCloses.enumerated().map { (i, close) -> Double in
guard i < recentHighs.count, i < recentLows.count else { return 0.0 }
let high = recentHighs[i]
let low = recentLows[i]
return (high - low) != 0 ? (close - low) / (high - low) * 100 : 0.0
}
// Ensure at least 3 values are available for calculating %D
let dPeriod = min(3, kValues.count)
let stochasticD = dPeriod > 0 ? kValues.suffix(dPeriod).reduce(0, +) / Double(dPeriod) : 0.0
return (stochasticK, stochasticD)
}
func calculateMovingAverage(prices: [Double], period: Int) -> Double {
guard shouldRecalculateIndicators() else { return 0.0 }
guard prices.count >= period else { return 0.0 }
let slice = prices.suffix(period)
let sum = slice.reduce(0, +)
return sum / Double(period)
}
func calculateMacd(prices: [Double], shortTermPeriod: Int = 12, longTermPeriod: Int = 26, signalPeriod: Int = 9) -> (macd: Double, signalLine: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, 0.0, false, false, true) }
guard prices.count >= longTermPeriod else { return (0.0, 0.0, false, false, true) }
// Calculate Exponential Moving Averages (EMAs)
let shortTermEma = calculateEma(prices: prices, period: shortTermPeriod)
let longTermEma = calculateEma(prices: prices, period: longTermPeriod)
// Calculate MACD Line
let macd = shortTermEma - longTermEma
// Calculate Signal Line (9-period EMA of MACD)
let signalLine = calculateEma(prices: [macd], period: signalPeriod)
// Buy, Sell, or Hold signal based on MACD and Signal Line crossover
let buySignal = macd > signalLine // Buy signal when MACD crosses above Signal Line
let sellSignal = macd < signalLine // Sell signal when MACD crosses below Signal Line
let holdSignal = macd == signalLine // Hold signal when MACD equals Signal Line (no crossover)
return (macd, signalLine, buySignal, sellSignal, holdSignal)
}
func calculateEma(prices: [Double], period: Int) -> Double {
guard prices.count >= period else { return 0.0 }
// Calculate the smoothing factor for EMA
let smoothingFactor = 2.0 / (Double(period) + 1)
// Calculate the Exponential Moving Average (EMA)
var ema = prices.prefix(period).reduce(0, +) / Double(period) // Initial EMA (SMA of the first period)
for price in prices.dropFirst(period) {
ema = (price - ema) * smoothingFactor + ema
}
return ema
}
func calculateRsi(prices: [Double], period: Int = 14) -> (rsi: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, false, false, true) }
guard prices.count > period else { return (0.0, false, false, true) }
var gains: [Double] = []
var losses: [Double] = []
// Calculate gains and losses
for i in 1.. 0 {
gains.append(change)
losses.append(0.0)
} else if change < 0 {
losses.append(-change)
gains.append(0.0)
} else {
gains.append(0.0)
losses.append(0.0)
}
}
// Calculate average gain and loss
let avgGain = gains.suffix(period).reduce(0, +) / Double(period)
let avgLoss = losses.suffix(period).reduce(0, +) / Double(period)
// Guard against division by zero
guard avgLoss != 0 else { return (100.0, false, true, false) }
// Calculate Relative Strength (RS) and RSI
let rs = avgGain / avgLoss
let rsi = 100 - (100 / (1 + rs))
// Buy, Sell, or Hold signal based on RSI value
let buySignal = rsi < 30 // Buy signal when RSI is under 30 (oversold)
let sellSignal = rsi > 70 // Sell signal when RSI is above 70 (overbought)
let holdSignal = rsi >= 30 && rsi <= 70 // Hold signal when RSI is between 30 and 70 (neutral)
return (rsi, buySignal, sellSignal, holdSignal)
}
func calculateObv(prices: [Double], volumes: [Double]) -> (obv: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, false, false, true) }
guard prices.count == volumes.count, prices.count > 1 else { return (0.0, false, false, true) }
var obv = 0.0
var previousObv = 0.0
var buySignal = false
var sellSignal = false
var holdSignal = false
// Calculate OBV
for i in 1.. prices[i - 1] {
obv += volumes[i] // Add volume on up days
} else if prices[i] < prices[i - 1] {
obv -= volumes[i] // Subtract volume on down days
}
}
// Calculate Buy/Sell/Hold signals based on OBV trends
if obv > previousObv && prices.last ?? 0.0 > prices[prices.count - 2] {
buySignal = true // OBV and price are both increasing (buy signal)
} else if obv < previousObv && prices.last ?? 0.0 < prices[prices.count - 2] {
sellSignal = true // OBV and price are both decreasing (sell signal)
} else {
holdSignal = true // No clear trend (hold signal)
}
// Update previous OBV for next iteration
previousObv = obv
return (obv, buySignal, sellSignal, holdSignal)
}
func calculateAdl(prices: [Double], highPrices: [Double], lowPrices: [Double], volumes: [Double]) -> (adl: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, false, false, true) }
guard prices.count == highPrices.count, prices.count == lowPrices.count, prices.count == volumes.count else { return (0.0, false, false, true) }
var adl = 0.0
var previousAdl = 0.0
var buySignal = false
var sellSignal = false
var holdSignal = false
// Calculate ADL
for i in 1.. previousAdl && prices.last ?? 0.0 > prices[prices.count - 2] {
buySignal = true // ADL and price are both increasing (buy signal)
} else if adl < previousAdl && prices.last ?? 0.0 < prices[prices.count - 2] {
sellSignal = true // ADL and price are both decreasing (sell signal)
} else {
holdSignal = true // No clear trend (hold signal)
}
// Update previous ADL for next iteration
previousAdl = adl
return (adl, buySignal, sellSignal, holdSignal)
}
func calculateBollingerBandsWithSignals(prices: [Double], period: Int = 20) -> (upperBand: Double, middleBand: Double, lowerBand: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, 0.0, 0.0, false, false, true) }
// Ensure there are enough data points
guard prices.count >= period else { return (0.0, 0.0, 0.0, false, false, true) }
// Calculate the moving average (middle band)
let movingAvg = calculateMovingAverage(prices: prices, period: period)
// Calculate the standard deviation
let squaredDiff = prices.map { pow($0 - movingAvg, 2) }
let variance = squaredDiff.reduce(0, +) / Double(prices.count)
let standardDeviation = sqrt(variance)
// Calculate the upper and lower bands
let upperBand = movingAvg + (standardDeviation * 2)
let lowerBand = movingAvg - (standardDeviation * 2)
// Determine the buy, sell, or hold signal
var buySignal = false
var sellSignal = false
var holdSignal = false
let lastPrice = prices.last ?? 0.0
if lastPrice <= lowerBand {
buySignal = true // Price is at or below the lower band (possible oversold condition)
} else if lastPrice >= upperBand {
sellSignal = true // Price is at or above the upper band (possible overbought condition)
} else {
holdSignal = true // Price is within the bands (no clear trend)
}
return (upperBand, movingAvg, lowerBand, buySignal, sellSignal, holdSignal)
}
func calculateAdxWithSignals(prices: [Double], highPrices: [Double], lowPrices: [Double], period: Int = 14) -> (adx: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, false, false, true) }
// Ensure there is enough data for the period
guard prices.count > period, highPrices.count > period, lowPrices.count > period else {
return (0.0, false, false, true) // Insufficient data
}
var trValues: [Double] = []
var positiveDM: [Double] = []
var negativeDM: [Double] = []
for i in 1.. prevLow - low ? max(high - prevHigh, 0) : 0
positiveDM.append(posDM)
// Negative Directional Movement
let negDM = prevLow - low > high - prevHigh ? max(prevLow - low, 0) : 0
negativeDM.append(negDM)
}
// Smooth the True Range, +DM and -DM
let smoothedTR = smoothArray(trValues, period: period)
let smoothedPositiveDM = smoothArray(positiveDM, period: period)
let smoothedNegativeDM = smoothArray(negativeDM, period: period)
var plusDI: [Double] = []
var minusDI: [Double] = []
var dx: [Double] = []
for i in 0..= 25 {
// Strong trend
if lastPlusDI > lastMinusDI {
buySignal = true
} else if lastMinusDI > lastPlusDI {
sellSignal = true
}
} else if adx < 20 {
// Weak or no trend
holdSignal = true
}
return (adx, buySignal, sellSignal, holdSignal)
}
func smoothArray(_ array: [Double], period: Int) -> [Double] {
var smoothedArray: [Double] = []
var smoothedValue = array.prefix(period).reduce(0, +) / Double(period)
smoothedArray.append(smoothedValue)
for i in period.. (fibRetracementLevel: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (0.0, false, false, true) }
// Ensure we have enough data points for calculation
guard let maxPrice = prices.max(), let minPrice = prices.min(), maxPrice != minPrice else {
return (0.0, false, false, true) // Return no signals if there is insufficient data or the max and min are equal
}
// Calculate the Fibonacci retracement levels
let difference = maxPrice - minPrice
let level23_6 = maxPrice - (difference * 0.236)
let level38_2 = maxPrice - (difference * 0.382)
let level50 = maxPrice - (difference * 0.5)
let level61_8 = maxPrice - (difference * 0.618)
// Retrieve the last price to compare against the Fibonacci levels
let lastPrice = prices.last ?? 0.0
var buySignal = false
var sellSignal = false
var holdSignal = false
var fibRetracementLevel = 0.0 // Set this to 0 initially
// Buy Signal Logic: Price at a retracement level (typically 23.6%, 38.2%, or 50%) and shows signs of bouncing
if lastPrice <= level38_2 && lastPrice > level50 {
buySignal = true
fibRetracementLevel = level38_2 // Use the retracement level that triggered the buy signal
}
// Sell Signal Logic: Price reaches 61.8% and shows signs of reversal downwards
if lastPrice >= level61_8 {
sellSignal = true
fibRetracementLevel = level61_8 // Use the retracement level that triggered the sell signal
}
// Hold Signal Logic: Price is between levels with no clear signal
if lastPrice > level23_6 && lastPrice < level61_8 {
holdSignal = true
fibRetracementLevel = level50 // Use level50 as the hold signal retracement level
}
// Return the retracement level and signals
return (fibRetracementLevel, buySignal, sellSignal, holdSignal)
}
func calculateIchimokuCloudWithSignals(highPrices: [Double], lowPrices: [Double], closePrices: [Double]) -> (buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
guard shouldRecalculateIndicators() else { return (false, false, true) }
// Ensure sufficient data points
guard highPrices.count >= 26, lowPrices.count >= 26, closePrices.count > 0 else {
return (false, false, true) // Cannot compute with insufficient data
}
// Calculate Tenkan-sen (Conversion Line) and Kijun-sen (Base Line)
let tenkanSen = calculateMovingAverage(prices: highPrices, period: 9)
let kijunSen = calculateMovingAverage(prices: highPrices, period: 26)
// Calculate Senkou Span A and Senkou Span B (Leading spans)
let senkouSpanA = (tenkanSen + kijunSen) / 2
let senkouSpanB = calculateMovingAverage(prices: highPrices, period: 52)
// Calculate Chikou Span (Lagging Line)
_ = closePrices.last ?? 0.0
// Define Buy, Sell, and Hold conditions
var buySignal = false
var sellSignal = false
var holdSignal = false
// Check if Tenkan-sen crosses above Kijun-sen and price is above the cloud
if tenkanSen > kijunSen && closePrices.last ?? 0.0 > senkouSpanA {
buySignal = true
}
// Check if Tenkan-sen crosses below Kijun-sen and price is below the cloud
if tenkanSen < kijunSen && closePrices.last ?? 0.0 < senkouSpanB {
sellSignal = true
}
// If price is within the cloud or no crossovers, hold the position
if closePrices.last ?? 0.0 >= senkouSpanB && closePrices.last ?? 0.0 <= senkouSpanA {
holdSignal = true
}
// Return the signals for buy, sell, or hold
return (buySignal, sellSignal, holdSignal)
}
func updateLabelColor(_ label: UILabel, status: String) {
DispatchQueue.main.async {
switch status.lowercased() {
case "buy":
label.textColor = .green
case "sell":
label.textColor = .red
case "hold":
label.textColor = .orange
default:
label.textColor = .white // Original color, assuming white text on a black background
}
}
}
func updateMovingAveragesLabel(shortTermMA: Double, longTermMA: Double) {
let status = shortTermMA > longTermMA ? "buy" : (shortTermMA < longTermMA ? "sell" : "hold")
updateLabelColor(movingAveragesLabel, status: status)
}
func updateMacdLabel(macd: Double, signalLine: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
// Update the MACD and Signal Line values
print("MACD: \(macd), Signal Line: \(signalLine)")
// Handle Buy, Sell, or Hold signals
if buySignal {
print("Buy Signal")
} else if sellSignal {
print("Sell Signal")
} else if holdSignal {
print("Hold Signal")
}
}
func updateRsiLabel(rsi: Double, buySignal: Bool, sellSignal: Bool, holdSignal: Bool) {
// Update the RSI value
print("RSI: \(rsi)")
// Handle Buy, Sell, or Hold signals
if buySignal {
print("Buy Signal (RSI is below 30, oversold)")
} else if sellSignal {
print("Sell Signal (RSI is above 70, overbought)")
} else if holdSignal {
print("Hold Signal (RSI is between 30 and 70, neutral)")
}
}
func updateObvLabel(obv: Double, price: Double, previousPrice: Double) {
let status: String
if let previousObv = previousObv {
status = (obv > previousObv && price > previousPrice) ? "buy" : (obv < previousObv && price < previousPrice ? "sell" : "hold")
} else {
status = "hold"
}
updateLabelColor(obvLabel, status: status)
previousObv = obv
}
func updateStochasticLabel(stochasticK: Double, stochasticD: Double) {
let status = (stochasticK > 80) ? "sell" : (stochasticK < 20 ? "buy" : "hold")
updateLabelColor(stochasticLabel, status: status)
}
func updateAdlLabel(adl: Double) {
let status = (adl > 0) ? "buy" : (adl < 0 ? "sell" : "hold")
updateLabelColor(adlLabel, status: status)
}
func updateBollingerBandsLabel(currentPrice: Double, upperBand: Double, lowerBand: Double) {
let status = (currentPrice > upperBand) ? "sell" : (currentPrice < lowerBand ? "buy" : "hold")
updateLabelColor(bollingerBandsLabel, status: status)
}
func updateAdxLabel(adx: Double) {
let strongTrendThreshold = 25.0
let status = (adx > strongTrendThreshold) ? "buy" : (adx < strongTrendThreshold ? "sell" : "hold")
updateLabelColor(adxLabel, status: status)
}
func updateFibonacciLabel(price: Double, fibRetracementLevel: Double) {
let status = (price > fibRetracementLevel) ? "buy" : (price < fibRetracementLevel ? "sell" : "hold")
updateLabelColor(fibonacciLabel, status: status)
}
func updateIchimokuLabel(currentPrice: Double, tenkanSen: Double, kijunSen: Double, priceAboveCloud: Bool) {
let status: String
if priceAboveCloud {
status = tenkanSen > kijunSen ? "buy" : "hold"
} else {
status = currentPrice < min(tenkanSen, kijunSen) ? "sell" : "hold"
}
updateLabelColor(ichimokuLabel, status: status)
}
// Decodable structs for handling Alpha Vantage API response
struct TimeSeriesDailyAdjusted: Decodable {
let timeSeries: [String: DailyAdjustedData]
let errorMessage: String?
enum CodingKeys: String, CodingKey {
case timeSeries = "Time Series (Daily)"
case errorMessage = "Error Message"
}
}
struct DailyAdjustedData: Codable {
let open: String
let high: String
let low: String
let close: String
let adjustedClose: String
let volume: String
let dividendAmount: String
let splitCoefficient: String
// Coding keys to match the JSON response if necessary
enum CodingKeys: String, CodingKey {
case open = "1. open"
case high = "2. high"
case low = "3. low"
case close = "4. close"
case adjustedClose = "5. adjusted close"
case volume = "6. volume"
case dividendAmount = "7. dividend amount"
case splitCoefficient = "8. split coefficient"
}
}}