Skip to content
This repository has been archived by the owner on Aug 12, 2024. It is now read-only.

Commit

Permalink
Remove some stuff and begin impl of the TradingView API
Browse files Browse the repository at this point in the history
  • Loading branch information
Cach30verfl0w committed Jun 17, 2024
1 parent cdc8bf8 commit 8272a13
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 324 deletions.
21 changes: 6 additions & 15 deletions cash3fl0w-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,15 @@ kotlin {
}
}

/*listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "Cash3Fl0w"
isStatic = true
}
}*/

sourceSets {

androidMain.dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.biometric)
implementation(libs.decompose)
implementation(libs.koin.android)
}
iosMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
commonMain.dependencies {
// Add kmp-advcrypto
Expand All @@ -62,7 +49,11 @@ kotlin {
// Other
implementation(libs.kotlinx.serialization.json)
implementation(libs.okio)
implementation(libs.qrkit)

// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,26 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slid
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import de.cacheoverflow.cashflow.api.tradingview.EnumMarket
import de.cacheoverflow.cashflow.api.tradingview.TradingViewAPI
import de.cacheoverflow.cashflow.security.ISecurityProvider
import de.cacheoverflow.cashflow.ui.DefaultColorScheme
import de.cacheoverflow.cashflow.ui.HomeScreen
import de.cacheoverflow.cashflow.ui.components.Modal
import de.cacheoverflow.cashflow.ui.components.ModalType
import de.cacheoverflow.cashflow.ui.settings.Settings
import de.cacheoverflow.cashflow.ui.components.OptionalAuthLock
import de.cacheoverflow.cashflow.ui.components.RootComponent
import de.cacheoverflow.cashflow.ui.settings.AuthSettings
import de.cacheoverflow.cashflow.ui.settings.DataTransfer
import de.cacheoverflow.cashflow.utils.DI
import de.cacheoverflow.cashflow.utils.defaultCoroutineScope
import de.cacheoverflow.cashflow.utils.deviceRootedPromptText
import de.cacheoverflow.cashflow.utils.securityWarning
import de.cacheoverflow.cashflow.utils.unlockAccountInfo
import de.cacheoverflow.cashflow.utils.unlockAccountInfoSubtitle
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.koin.dsl.module

interface IErrorHandler {
Expand All @@ -54,8 +58,6 @@ fun AppView(
when (val instance = it.instance) {
is RootComponent.Child.MainMenu -> HomeScreen(instance.component)
is RootComponent.Child.Settings -> Settings(instance.component)
is RootComponent.Child.AuthSettings -> AuthSettings(instance.component)
is RootComponent.Child.DataTransfer -> DataTransfer(instance.component)
}
}
}
Expand All @@ -64,20 +66,25 @@ fun AppView(
fun App(root: RootComponent) {
MaterialTheme(colorScheme = DefaultColorScheme) {
val childStack by root.childStack.subscribeAsState()
OptionalAuthLock(
title = unlockAccountInfo(),
subtitle = unlockAccountInfoSubtitle(),
authCancelled = { AppView(childStack) }
) {
if (DI.inject<ISecurityProvider>().isDeviceRooted()) {
// TODO: Don't show menu if already clicked away?
val rootedModalVisible = remember { mutableStateOf(true) }
Modal(rootedModalVisible, securityWarning(), ModalType.WARNING) {
Text(deviceRootedPromptText())

val json = Json { ignoreUnknownKeys = true }
defaultCoroutineScope.launch {
TradingViewAPI(HttpClient {
install(ContentNegotiation) {
json(json, contentType = ContentType.Any)
}
}
}).scanMarket(EnumMarket.AMERICA)
}

AppView(childStack)
// TODO: Lock AppView behind authentication lock
if (DI.inject<ISecurityProvider>().isDeviceRooted()) {
// TODO: Don't show menu if already clicked away?
val rootedModalVisible = remember { mutableStateOf(true) }
Modal(rootedModalVisible, securityWarning(), ModalType.WARNING) {
Text(deviceRootedPromptText())
}
}

AppView(childStack)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* This project is licensed with GNU General Public License 2.0
* Copyright (c) 2024 Cach30verfl0w <[email protected]>
*/

package de.cacheoverflow.cashflow.api.tradingview

enum class EnumMarket {

BONDS,
CFD,
COIN,
CRYPTO,
ECONOMICS2,
FOREX,
FUTURES,
OPTIONS,
AMERICA,
ARGENTINA,
AUSTRALIA,
AUSTRIA,
BAHRAIN,
BANGLADESH,
BELGIUM,
BRAZIL,
CANADA,
CHILE,
CHINA,
COLOMBIA,
CYPRUS,
CZECH,
DENMARK,
EGYPT,
ESTONIA,
FINLAND,
FRANCE,
GERMANY,
GREECE,
HONGKONG,
HUNGARY,
ICELAND,
INDIA,
INDONESIA,
ISRAEL,
ITALY,
JAPAN,
KENYA,
KOREA,
KSA,
KUWAIT,
LATVIA,
LITHUANIA,
LUXEMBOURG,
MALAYSIA,
MEXICO,
MOROCCO,
NETHERLANDS,
NEWZEALAND,
NIGERIA,
NORWAY,
PAKISTAN,
PERU,
PHILIPPINES,
POLAND,
PORTUGAL,
QATAR,
ROMANIA,
RSA,
RUSSIA,
SERBIA,
SINGAPORE,
SLOVAKIA,
SPAIN,
SRILANKA,
SWEDEN,
SWITZERLAND,
TAIWAN,
THAILAND,
TUNISIA,
TURKEY,
UAE,
UK,
VENEZUELA,
VIETNAM

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* This project is licensed with GNU General Public License 2.0
* Copyright (c) 2024 Cach30verfl0w <[email protected]>
*/

package de.cacheoverflow.cashflow.api.tradingview

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.http.HttpMethod
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.serializer

/**
* This class represents the scan filter. This filter can be used to filter out companies in the
* market scan to acquire only companies with more specific parameters.
*
* @author Cedric Hammes
* @since 17/06/2024
*/
@Serializable(with = ScanFilterSerializer::class)
data class ScanFilter(val left: String, val operation: String, val type: String, val right: Any)

/**
* This is a specific serializer for the [ScanFilter] data class. This allows to serialize this
* structure with the parameter typed with [Any]. This class is only being used internally.
*
* @author Cedric Hammes
* @since 17/06/2024
*/
private class ScanFilterSerializer: KSerializer<ScanFilter> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Packet") {
element("left", serialDescriptor<String>())
element("operation", serialDescriptor<String>())
element("right", buildClassSerialDescriptor("Any"))
}

@Suppress("UNCHECKED_CAST")
private val dataTypeSerializers: Map<String, KSerializer<Any>> = mapOf(
"String" to serializer<String>(),
"Int" to serializer<Int>()
).mapValues { (_, v) -> v as KSerializer<Any> }

override fun serialize(encoder: Encoder, value: ScanFilter)
= encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.left)
encodeStringElement(descriptor, 1, value.operation)
encodeSerializableElement(
descriptor,
2,
checkNotNull(dataTypeSerializers[value.type]),
value.right
)
}

override fun deserialize(decoder: Decoder): ScanFilter {
// We don't need to deserialize them anyway
throw UnsupportedOperationException("Deserialization of ScanFilters is not implemented")
}
}

/**
* This data class contains information that is used in the scan markets request. This parameters
* specify the columns returned to the user and the filters applied.
*
* @author Cedric Hammes
* @since 17/06/2024
*/
@Serializable
data class ScanRequestParameters(
val columns: List<String>,
val filter: List<ScanFilter>? = null,
val range: IntArray = intArrayOf(0, 100)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ScanRequestParameters

if (columns != other.columns) return false
if (filter != other.filter) return false
if (!range.contentEquals(other.range)) return false

return true
}

override fun hashCode(): Int {
var result = columns.hashCode()
result = 31 * result + (filter?.hashCode() ?: 0)
result = 31 * result + range.contentHashCode()
return result
}
}

/**
* This is a simple API wrapper for the TradingView API. TradingView is a social media network and
* analysis platform for traders and investors. I got some values for this library form other libs
* around.
*
* This API is used by a background service to acquire a list of companies in a specific market to
* allow the user to choose between them and acquire notifications about news on the market and
* more. We also give this app the ability to track market prices etc.
*
* @see https://github.com/shner-elmo/TradingView-Screener/tree/master
* @see https://en.wikipedia.org/wiki/TradingView
* @see https://www.tradingview.com/
*
* @author Cedric Hammes
* @since 17/06/2024
*/
class TradingViewAPI(private val client: HttpClient) {

/**
* This method sends a request to TradingView to receive a list of companies in the specified
* market with some information about them like market capitalization or revenue.
*
* @param market The name of the market represented by the [EnumMarket] enum
* @return A list of companies with information about them
*
* @author Cedric Hammes
* @since 17/06/2024
*/
suspend fun scanMarket(market: EnumMarket, columns: List<String> = defaultColumns) {
val url = "https://scanner.tradingview.com/${market.toString().lowercase()}/scan"
val listData = checkNotNull(Json.parseToJsonElement(client.request(url) {
setBody(Json.encodeToString(ScanRequestParameters(columns)))
method = HttpMethod.Post
}.body<String>()).jsonObject["data"]).jsonArray

for (company in listData) {
// Get exchange and company short
val companyObject = company.jsonObject
val (exchange, companyShort) = checkNotNull(companyObject["s"]).jsonPrimitive.toString()
.split(":").let { Pair(it[0], it[1]) }

// Handle data
val companyData = checkNotNull(companyObject["d"]).jsonArray
for (dataField in companyData) {
val data = dataField.jsonPrimitive
// TODO: Handle data
}
}
}

companion object {
private val defaultColumns = arrayListOf(
"description",
"market_cap_basic", // Market capitalization
"country",
"industry",
"net_income",
"dps_common_stock_prim_issue_fy", // Dividends per Share
"earnings_per_share_basic_ttm", // Earnings per Share (EPS)
"total_revenue"
)
}

}
Loading

0 comments on commit 8272a13

Please sign in to comment.