Compare commits
12 Commits
e0b6eab7cf
...
1f83b6abf9
Author | SHA1 | Date | |
---|---|---|---|
1f83b6abf9 | |||
10324d5799 | |||
ad7ab6db86 | |||
ca9dd95461 | |||
fae9cc8154 | |||
e6d7020fc8 | |||
32c1458a9c | |||
22fad337ae | |||
454850c6b3 | |||
ea611f0cdc | |||
d31855785d | |||
f089a04ef4 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,3 +10,6 @@ stock.db
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Fresh build directory
|
||||||
|
_fresh/
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
# Stock
|
# Stock
|
||||||
|
|
||||||
|
[![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev)
|
||||||
|
|
||||||
주식 데이터 수집 및 선별하는 파이썬 코드입니다.
|
주식 데이터 수집 및 선별하는 파이썬 코드입니다.
|
||||||
|
24
app.py
24
app.py
@ -1,24 +0,0 @@
|
|||||||
import flask
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Stock web server")
|
|
||||||
parser.add_argument("--port", type=int, default=12001, help="port number")
|
|
||||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="host address")
|
|
||||||
parser.add_argument("--debug", action="store_true", help="debug mode")
|
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
|
||||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
|
||||||
|
|
||||||
@app.route("/dist/<m>")
|
|
||||||
def distServe(m:str):
|
|
||||||
return flask.send_from_directory("dist", m)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
import pages
|
|
||||||
return flask.render_template("index.html", pages = pages.GenLists)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
args = parser.parse_args()
|
|
||||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
|
||||||
|
|
11
db/db.ts
Normal file
11
db/db.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||||
|
import { DB as Sqlite } from "sqlite";
|
||||||
|
import { DenoSqliteDialect } from "./deno-sqlite-dialect.ts";
|
||||||
|
import { Database } from "./type.ts";
|
||||||
|
|
||||||
|
export const db = new Kysely<Database>({
|
||||||
|
dialect: new DenoSqliteDialect({
|
||||||
|
database: new Sqlite("stock.db")
|
||||||
|
}),
|
||||||
|
plugins: [new ParseJSONResultsPlugin()]
|
||||||
|
});
|
27
db/deno-sqlite-dialect-config.ts
Normal file
27
db/deno-sqlite-dialect-config.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/// The MIT License (MIT)
|
||||||
|
/// Copyright (c) 2023 Alex Gleason
|
||||||
|
/// Copyright (c) 2022 Sami Koskimäki
|
||||||
|
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||||
|
|
||||||
|
import type { SqliteDialectConfig } from 'kysely';
|
||||||
|
|
||||||
|
/** Type compatible with both [dyedgreen/deno-sqlite](https://github.com/dyedgreen/deno-sqlite) and [denodrivers/sqlite3](https://github.com/denodrivers/sqlite3). */
|
||||||
|
type DenoSqlite =
|
||||||
|
& {
|
||||||
|
close(): void;
|
||||||
|
changes: number;
|
||||||
|
lastInsertRowId: number;
|
||||||
|
}
|
||||||
|
& ({
|
||||||
|
queryEntries(sql: string, params: any): unknown[];
|
||||||
|
} | {
|
||||||
|
prepare(sql: string): {
|
||||||
|
all(...params: any): unknown[];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DenoSqliteDialectConfig extends Omit<SqliteDialectConfig, 'database'> {
|
||||||
|
database: DenoSqlite | (() => Promise<DenoSqlite>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { DenoSqlite, DenoSqliteDialectConfig };
|
49
db/deno-sqlite-dialect.ts
Normal file
49
db/deno-sqlite-dialect.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/// The MIT License (MIT)
|
||||||
|
/// Copyright (c) 2023 Alex Gleason
|
||||||
|
/// Copyright (c) 2022 Sami Koskimäki
|
||||||
|
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DatabaseIntrospector,
|
||||||
|
type Dialect,
|
||||||
|
type DialectAdapter,
|
||||||
|
type Driver,
|
||||||
|
Kysely,
|
||||||
|
type QueryCompiler,
|
||||||
|
SqliteAdapter,
|
||||||
|
SqliteIntrospector,
|
||||||
|
SqliteQueryCompiler,
|
||||||
|
} from 'kysely';
|
||||||
|
|
||||||
|
import { DenoSqliteDriver } from './kysely-sqlite-driver.ts';
|
||||||
|
|
||||||
|
import type { DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
|
||||||
|
|
||||||
|
class DenoSqliteDialect implements Dialect {
|
||||||
|
readonly #config: DenoSqliteDialectConfig;
|
||||||
|
|
||||||
|
constructor(config: DenoSqliteDialectConfig) {
|
||||||
|
this.#config = Object.freeze({ ...config });
|
||||||
|
}
|
||||||
|
|
||||||
|
createDriver(): Driver {
|
||||||
|
return new DenoSqliteDriver(this.#config);
|
||||||
|
}
|
||||||
|
|
||||||
|
createQueryCompiler(): QueryCompiler {
|
||||||
|
return new SqliteQueryCompiler();
|
||||||
|
}
|
||||||
|
|
||||||
|
createAdapter(): DialectAdapter {
|
||||||
|
return new SqliteAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||||
|
return new SqliteIntrospector(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DenoSqliteDialect };
|
||||||
|
|
||||||
|
|
112
db/kysely-sqlite-driver.ts
Normal file
112
db/kysely-sqlite-driver.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/// The MIT License (MIT)
|
||||||
|
/// Copyright (c) 2023 Alex Gleason
|
||||||
|
/// Copyright (c) 2022 Sami Koskimäki
|
||||||
|
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||||
|
|
||||||
|
import { CompiledQuery, type DatabaseConnection, type Driver, type QueryResult } from 'kysely';
|
||||||
|
|
||||||
|
import type { DenoSqlite, DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
|
||||||
|
|
||||||
|
class DenoSqliteDriver implements Driver {
|
||||||
|
readonly #config: DenoSqliteDialectConfig;
|
||||||
|
readonly #connectionMutex = new ConnectionMutex();
|
||||||
|
|
||||||
|
#db?: DenoSqlite;
|
||||||
|
#connection?: DatabaseConnection;
|
||||||
|
|
||||||
|
constructor(config: DenoSqliteDialectConfig) {
|
||||||
|
this.#config = Object.freeze({ ...config });
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
this.#db = typeof this.#config.database === 'function' ? await this.#config.database() : this.#config.database;
|
||||||
|
|
||||||
|
this.#connection = new DenoSqliteConnection(this.#db);
|
||||||
|
|
||||||
|
if (this.#config.onCreateConnection) {
|
||||||
|
await this.#config.onCreateConnection(this.#connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireConnection(): Promise<DatabaseConnection> {
|
||||||
|
// SQLite only has one single connection. We use a mutex here to wait
|
||||||
|
// until the single connection has been released.
|
||||||
|
await this.#connectionMutex.lock();
|
||||||
|
return this.#connection!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw('begin'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw('commit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||||
|
await connection.executeQuery(CompiledQuery.raw('rollback'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async releaseConnection(): Promise<void> {
|
||||||
|
this.#connectionMutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
this.#db?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DenoSqliteConnection implements DatabaseConnection {
|
||||||
|
readonly #db: DenoSqlite;
|
||||||
|
|
||||||
|
constructor(db: DenoSqlite) {
|
||||||
|
this.#db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
executeQuery<O>({ sql, parameters }: CompiledQuery): Promise<QueryResult<O>> {
|
||||||
|
const rows = 'queryEntries' in this.#db
|
||||||
|
? this.#db.queryEntries(sql, parameters)
|
||||||
|
: this.#db.prepare(sql).all(...parameters);
|
||||||
|
|
||||||
|
const { changes, lastInsertRowId } = this.#db;
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
rows: rows as O[],
|
||||||
|
numAffectedRows: BigInt(changes),
|
||||||
|
insertId: BigInt(lastInsertRowId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore require-yield
|
||||||
|
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||||
|
throw new Error('Sqlite driver doesn\'t support streaming');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionMutex {
|
||||||
|
#promise?: Promise<void>;
|
||||||
|
#resolve?: () => void;
|
||||||
|
|
||||||
|
async lock(): Promise<void> {
|
||||||
|
while (this.#promise) {
|
||||||
|
await this.#promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#promise = new Promise((resolve) => {
|
||||||
|
this.#resolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock(): void {
|
||||||
|
const resolve = this.#resolve;
|
||||||
|
|
||||||
|
this.#promise = undefined;
|
||||||
|
this.#resolve = undefined;
|
||||||
|
|
||||||
|
resolve?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DenoSqliteDriver };
|
64
db/type.ts
Normal file
64
db/type.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Code" TEXT,
|
||||||
|
"Date" TEXT,
|
||||||
|
"Close" INTEGER NOT NULL,
|
||||||
|
"Diff" INTEGER NOT NULL,
|
||||||
|
"Open" INTEGER NOT NULL,
|
||||||
|
"High" INTEGER NOT NULL,
|
||||||
|
"Low" INTEGER NOT NULL,
|
||||||
|
"Volume" INTEGER NOT NULL,
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface StockTable {
|
||||||
|
Code: string;
|
||||||
|
Date: string;
|
||||||
|
Close: number;
|
||||||
|
Diff: number;
|
||||||
|
Open: number;
|
||||||
|
High: number;
|
||||||
|
Low: number;
|
||||||
|
Volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KRXCorpTable{
|
||||||
|
Name: string;
|
||||||
|
/**
|
||||||
|
* PK
|
||||||
|
*/
|
||||||
|
Code: string;
|
||||||
|
|
||||||
|
Sector: string;
|
||||||
|
Product: string;
|
||||||
|
ListingDay: string;
|
||||||
|
ClosingMonth: string;
|
||||||
|
Representative: string;
|
||||||
|
Homepage: string;
|
||||||
|
AddressArea: string;
|
||||||
|
LastUpdate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KOSPITable{
|
||||||
|
Name: string;
|
||||||
|
/**
|
||||||
|
* PK
|
||||||
|
*/
|
||||||
|
Code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KOSDAQTable{
|
||||||
|
Name: string;
|
||||||
|
/**
|
||||||
|
* PK
|
||||||
|
*/
|
||||||
|
Code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Database {
|
||||||
|
stock: StockTable;
|
||||||
|
KRXCorp: KRXCorpTable;
|
||||||
|
KOSPI: KOSPITable;
|
||||||
|
KOSDAQ: KOSDAQTable;
|
||||||
|
}
|
37
deno.json
37
deno.json
@ -2,31 +2,28 @@
|
|||||||
"lock": false,
|
"lock": false,
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
||||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
"prod_start": "deno run -A main.ts",
|
||||||
},
|
"update": "deno run -A -r https://fresh.deno.dev/update .",
|
||||||
"lint": {
|
"build": "deno run -A dev.ts build",
|
||||||
"rules": {
|
"preview": "deno run -A main.ts"
|
||||||
"tags": [
|
|
||||||
"fresh",
|
|
||||||
"recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
||||||
"imports": {
|
"imports": {
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.3.1/",
|
"$fresh/": "https://deno.land/x/fresh@1.5.2/",
|
||||||
"preact": "https://esm.sh/preact@10.15.1",
|
"preact": "https://esm.sh/preact@10.18.1",
|
||||||
"preact/": "https://esm.sh/preact@10.15.1/",
|
"preact/": "https://esm.sh/preact@10.18.1/",
|
||||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
|
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
|
||||||
"auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0",
|
"auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0",
|
||||||
"auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/",
|
"auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/",
|
||||||
"twind": "https://esm.sh/twind@0.16.19",
|
"twind": "https://esm.sh/twind@0.16.19",
|
||||||
"twind/": "https://esm.sh/twind@0.16.19/",
|
"twind/": "https://esm.sh/twind@0.16.19/",
|
||||||
"$std/": "https://deno.land/std@0.193.0/"
|
"$std/": "https://deno.land/std@0.203.0/",
|
||||||
|
"kysely": "npm:kysely@^0.26.3",
|
||||||
|
"kysely/helpers/sqlite": "npm:kysely@^0.26.3/helpers/sqlite",
|
||||||
|
"sqlite": "https://deno.land/x/sqlite@v3.8/mod.ts"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
|
||||||
"jsx": "react-jsx",
|
"exclude": ["**/_fresh/*"]
|
||||||
"jsxImportSource": "preact"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
44
fresh.gen.ts
44
fresh.gen.ts
@ -1,36 +1,42 @@
|
|||||||
// DO NOT EDIT. This file is generated by fresh.
|
// DO NOT EDIT. This file is generated by Fresh.
|
||||||
// This file SHOULD be checked into source version control.
|
// This file SHOULD be checked into source version control.
|
||||||
// This file is automatically updated during development when running `dev.ts`.
|
// This file is automatically updated during development when running `dev.ts`.
|
||||||
|
|
||||||
import * as $0 from "./routes/_404.tsx";
|
import * as $0 from "./routes/_404.tsx";
|
||||||
import * as $1 from "./routes/_app.tsx";
|
import * as $1 from "./routes/_app.tsx";
|
||||||
import * as $2 from "./routes/api/joke.ts";
|
import * as $2 from "./routes/api/corps/[index].ts";
|
||||||
import * as $3 from "./routes/api/kosdaq.ts";
|
import * as $3 from "./routes/api/corps/index.ts";
|
||||||
import * as $4 from "./routes/api/kospi.ts";
|
import * as $4 from "./routes/api/joke.ts";
|
||||||
import * as $5 from "./routes/api/pages/[name].ts";
|
import * as $5 from "./routes/api/kosdaq.ts";
|
||||||
import * as $6 from "./routes/api/pages/index.ts";
|
import * as $6 from "./routes/api/kospi.ts";
|
||||||
import * as $7 from "./routes/greet/[name].tsx";
|
import * as $7 from "./routes/api/pages/[name].ts";
|
||||||
import * as $8 from "./routes/index.tsx";
|
import * as $8 from "./routes/api/pages/index.ts";
|
||||||
import * as $9 from "./routes/pages/[name].tsx";
|
import * as $9 from "./routes/greet/[name].tsx";
|
||||||
|
import * as $10 from "./routes/index.tsx";
|
||||||
|
import * as $11 from "./routes/pages/[name].tsx";
|
||||||
import * as $$0 from "./islands/Counter.tsx";
|
import * as $$0 from "./islands/Counter.tsx";
|
||||||
import * as $$1 from "./islands/StockList.tsx";
|
import * as $$1 from "./islands/Search.tsx";
|
||||||
|
import * as $$2 from "./islands/StockList.tsx";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
routes: {
|
routes: {
|
||||||
"./routes/_404.tsx": $0,
|
"./routes/_404.tsx": $0,
|
||||||
"./routes/_app.tsx": $1,
|
"./routes/_app.tsx": $1,
|
||||||
"./routes/api/joke.ts": $2,
|
"./routes/api/corps/[index].ts": $2,
|
||||||
"./routes/api/kosdaq.ts": $3,
|
"./routes/api/corps/index.ts": $3,
|
||||||
"./routes/api/kospi.ts": $4,
|
"./routes/api/joke.ts": $4,
|
||||||
"./routes/api/pages/[name].ts": $5,
|
"./routes/api/kosdaq.ts": $5,
|
||||||
"./routes/api/pages/index.ts": $6,
|
"./routes/api/kospi.ts": $6,
|
||||||
"./routes/greet/[name].tsx": $7,
|
"./routes/api/pages/[name].ts": $7,
|
||||||
"./routes/index.tsx": $8,
|
"./routes/api/pages/index.ts": $8,
|
||||||
"./routes/pages/[name].tsx": $9,
|
"./routes/greet/[name].tsx": $9,
|
||||||
|
"./routes/index.tsx": $10,
|
||||||
|
"./routes/pages/[name].tsx": $11,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/Counter.tsx": $$0,
|
"./islands/Counter.tsx": $$0,
|
||||||
"./islands/StockList.tsx": $$1,
|
"./islands/Search.tsx": $$1,
|
||||||
|
"./islands/StockList.tsx": $$2,
|
||||||
},
|
},
|
||||||
baseUrl: import.meta.url,
|
baseUrl: import.meta.url,
|
||||||
};
|
};
|
||||||
|
53
gen.py
53
gen.py
@ -5,7 +5,6 @@ import sqlite3
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
from render import *
|
from render import *
|
||||||
import db as database
|
import db as database
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import tqdm
|
import tqdm
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ def isMACDCrossSignal(signal: pd.Series, macd: pd.Series, nday: int, order=1) ->
|
|||||||
signal.iloc[nday+order] < macd.iloc[nday+order])
|
signal.iloc[nday+order] < macd.iloc[nday+order])
|
||||||
|
|
||||||
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
|
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
|
||||||
return abs(a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < threshold
|
return abs(a.iloc[nday] - b.iloc[nday + 1]) / b.iloc[nday + 1] < threshold
|
||||||
|
|
||||||
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
|
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
|
||||||
"""a is bigger than b"""
|
"""a is bigger than b"""
|
||||||
@ -145,7 +144,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
|||||||
d5 = d(5)
|
d5 = d(5)
|
||||||
d20 = d(20)
|
d20 = d(20)
|
||||||
d25 = d(25)
|
d25 = d(25)
|
||||||
d30 = d(30)
|
|
||||||
d45 = d(45)
|
d45 = d(45)
|
||||||
d60 = d(60)
|
d60 = d(60)
|
||||||
d120 = d(120)
|
d120 = d(120)
|
||||||
@ -156,8 +154,14 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
|||||||
|
|
||||||
bollinger_upperband = d25 + 2* d_std25
|
bollinger_upperband = d25 + 2* d_std25
|
||||||
|
|
||||||
a = [d5, d20, d45, d60]
|
a = [d5, d20, d45]
|
||||||
for nday in ndays:
|
for nday in ndays:
|
||||||
|
if openv[nday] <= d240[nday] and d240[nday] <= close[nday] and d240[nday + 1] < d240[nday]:
|
||||||
|
collector.collect("양봉사이240일선증가", corp, stock.index[nday])
|
||||||
|
|
||||||
|
if d5[nday + 1] < d5[nday] and d5[nday + 2] > d5[nday + 1] and d20[nday + 1] < d20[nday]:
|
||||||
|
collector.collect("5일선반등120선증가", corp, stock.index[nday])
|
||||||
|
|
||||||
if openv[nday] <= d20[nday] and d20[nday] <= close[nday]:
|
if openv[nday] <= d20[nday] and d20[nday] <= close[nday]:
|
||||||
collector.collect("양봉사이20일선", corp, stock.index[nday])
|
collector.collect("양봉사이20일선", corp, stock.index[nday])
|
||||||
|
|
||||||
@ -207,6 +211,9 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
|||||||
if (D240BiggerThanYesterDay):
|
if (D240BiggerThanYesterDay):
|
||||||
collector.collect("240일 증가", corp, stock.index[nday])
|
collector.collect("240일 증가", corp, stock.index[nday])
|
||||||
|
|
||||||
|
if max([d[nday] for d in (d20, d60, d120)]) < min(close[nday], openv[nday]):
|
||||||
|
collector.collect("떠있음", corp, stock.index[nday])
|
||||||
|
|
||||||
if (d60[nday + 1] < d60[nday]):
|
if (d60[nday + 1] < d60[nday]):
|
||||||
collector.collect("정배열60", corp, stock.index[nday])
|
collector.collect("정배열60", corp, stock.index[nday])
|
||||||
if (d20[nday + 1] < d20[nday]):
|
if (d20[nday + 1] < d20[nday]):
|
||||||
@ -217,6 +224,11 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
|||||||
d120[nday + 1] <= d120[nday]):
|
d120[nday + 1] <= d120[nday]):
|
||||||
collector.collect("모두 정배열", corp, stock.index[nday])
|
collector.collect("모두 정배열", corp, stock.index[nday])
|
||||||
|
|
||||||
|
if(d120[nday + 1] <= d120[nday] and
|
||||||
|
d120[nday + 1] < d240[nday] and
|
||||||
|
d120[nday] >= d240[nday]):
|
||||||
|
collector.collect("120선240선추월", corp, stock.index[nday])
|
||||||
|
|
||||||
if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]):
|
if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]):
|
||||||
collector.collect("d20d5돌파", corp, stock.index[nday])
|
collector.collect("d20d5돌파", corp, stock.index[nday])
|
||||||
|
|
||||||
@ -248,8 +260,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
|||||||
#rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1]
|
#rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
|
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
|
||||||
parser.add_argument("--format", "-f", choices=["json", "html"], default="html",
|
|
||||||
help="출력 포맷을 지정합니다. 기본값은 html입니다.")
|
|
||||||
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
|
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
|
||||||
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.")
|
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.")
|
||||||
parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true')
|
parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true')
|
||||||
@ -268,10 +278,6 @@ if __name__ == "__main__":
|
|||||||
if args.corp:
|
if args.corp:
|
||||||
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
|
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
|
||||||
|
|
||||||
env = Environment(
|
|
||||||
loader=PackageLoader('render', 'templates'),
|
|
||||||
autoescape=select_autoescape(['html', 'xml'])
|
|
||||||
)
|
|
||||||
collector = OutputCollector()
|
collector = OutputCollector()
|
||||||
prepareCollector(collector)
|
prepareCollector(collector)
|
||||||
|
|
||||||
@ -281,25 +287,10 @@ if __name__ == "__main__":
|
|||||||
dataStore.clearCache()
|
dataStore.clearCache()
|
||||||
|
|
||||||
for k,v in collector.data.items():
|
for k,v in collector.data.items():
|
||||||
if args.format == "json":
|
data = json.dumps(v.toDict(), ensure_ascii=False)
|
||||||
data = json.dumps(v.toDict(), indent=4, ensure_ascii=False)
|
if args.printStdout:
|
||||||
if args.printStdout:
|
print(k)
|
||||||
print(k)
|
print(data)
|
||||||
print(data)
|
|
||||||
else:
|
|
||||||
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
|
|
||||||
f.write(data)
|
|
||||||
else:
|
else:
|
||||||
template = env.get_template("Lists.html")
|
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
|
||||||
|
f.write(data)
|
||||||
days = v.corpListByDate.keys()
|
|
||||||
days = list(days)
|
|
||||||
days.sort(reverse=True)
|
|
||||||
days = days[:5]
|
|
||||||
|
|
||||||
html = template.render(collected=v, title=k, days=days, lastUpdate=datetime.date.today().isoformat())
|
|
||||||
if args.printStdout:
|
|
||||||
print(html)
|
|
||||||
else:
|
|
||||||
with open(os.path.join(args.dir, k + ".html"), "w", encoding="UTF-8") as f:
|
|
||||||
f.write(html)
|
|
5
islands/Search.tsx
Normal file
5
islands/Search.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function Search(){
|
||||||
|
return <div>
|
||||||
|
<div>div</div>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,9 +1,18 @@
|
|||||||
import { Button } from "../components/Button.tsx";
|
import { Button } from "../components/Button.tsx";
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { Signal, useSignal } from "@preact/signals";
|
import { Signal, useSignal } from "@preact/signals";
|
||||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||||
import { mapValues } from "$std/collections/map_values.ts";
|
import { mapValues } from "$std/collections/map_values.ts";
|
||||||
|
import { useAsync } from "../util/util.ts";
|
||||||
|
import {
|
||||||
|
Coperation,
|
||||||
|
CorpSimple,
|
||||||
|
fetchKosdaqList,
|
||||||
|
fetchKospiList,
|
||||||
|
fetchPageInfo,
|
||||||
|
PageCorpsInfo,
|
||||||
|
} from "../util/api.ts";
|
||||||
|
|
||||||
interface StockProps {
|
interface StockProps {
|
||||||
pageName: string;
|
pageName: string;
|
||||||
@ -31,94 +40,70 @@ function ToggleButton(props: ToggleButtonProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryStatus<T> = {
|
function StockListByDate(
|
||||||
type: "loading";
|
{ prevSet, rows, name }: {
|
||||||
} | {
|
prevSet: Set<string>;
|
||||||
type: "complete";
|
rows: Coperation[];
|
||||||
data: T;
|
name: string;
|
||||||
} | {
|
},
|
||||||
type: "error";
|
) {
|
||||||
err: Error;
|
const lastCount = useRef(rows.length);
|
||||||
};
|
const curCount = rows.length;
|
||||||
|
const parent = useRef<HTMLDivElement>(null);
|
||||||
function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
|
const controller = useRef<
|
||||||
const state = useSignal({
|
{
|
||||||
type: "loading",
|
isEnabled: () => boolean;
|
||||||
} as QueryStatus<T>);
|
disable: () => void;
|
||||||
|
enable: () => void;
|
||||||
|
} | undefined
|
||||||
|
>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
console.log("animation mount on ", name);
|
||||||
const data = await fn();
|
const { default: autoAnimate } = await import(
|
||||||
state.value = {
|
"https://esm.sh/@formkit/auto-animate@0.7.0"
|
||||||
type: "complete",
|
);
|
||||||
data: data,
|
if (parent.current) {
|
||||||
};
|
const cntr = autoAnimate(parent.current);
|
||||||
} catch (err) {
|
controller.current = cntr;
|
||||||
state.value = {
|
|
||||||
type: "error",
|
|
||||||
err: err,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, [parent]);
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Coperation {
|
useLayoutEffect(() => {
|
||||||
Name: string;
|
if (controller.current) {
|
||||||
Code: string;
|
if (Math.abs(curCount - lastCount.current) > 200) {
|
||||||
Sector: string;
|
console.log("disable animation", curCount, "from", lastCount.current);
|
||||||
Product: string;
|
controller.current.disable();
|
||||||
ListingDay: string;
|
} else {
|
||||||
ClosingMonth: string;
|
console.log("enable animation", curCount, "from", lastCount.current);
|
||||||
Representative: string;
|
controller.current.enable();
|
||||||
Homepage: string;
|
}
|
||||||
AddressArea: string;
|
lastCount.current = curCount;
|
||||||
LastUpdate: string;
|
}
|
||||||
}
|
}, [parent, rows]);
|
||||||
|
|
||||||
interface PageCorpsInfo {
|
return (
|
||||||
name: string;
|
<div ref={parent}>
|
||||||
description: string;
|
<h2 class="text-lg">{name}</h2>
|
||||||
corpListByDate: Record<string, Coperation[]>;
|
{rows.map((row) => {
|
||||||
}
|
const firstOccur = !prevSet.has(row.Code);
|
||||||
|
return (
|
||||||
interface CorpSimple {
|
<div
|
||||||
code: string;
|
key={row.Code}
|
||||||
name: string;
|
class={[
|
||||||
}
|
"bg-white",
|
||||||
|
firstOccur ? "text-[#ff5454] underline" : "text-black",
|
||||||
function StockListByDate({prevSet, rows, name}:{prevSet:Set<string>,
|
].join(" ")}
|
||||||
rows:Coperation[],
|
>
|
||||||
name: string}){
|
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
|
||||||
const parent = useRef<HTMLDivElement>(null);
|
{row.Name}
|
||||||
useEffect(()=>{
|
</a>
|
||||||
(async ()=>{
|
</div>
|
||||||
console.log("animation mount on ",name);
|
);
|
||||||
const {default:autoAnimate} = await import("https://esm.sh/@formkit/auto-animate@0.7.0");
|
})}
|
||||||
parent.current && autoAnimate(parent.current)
|
</div>
|
||||||
})();
|
);
|
||||||
},[parent]);
|
|
||||||
|
|
||||||
return <div ref={parent}>
|
|
||||||
<h2 class="text-lg">{name}</h2>
|
|
||||||
{rows.map((row) => {
|
|
||||||
const firstOccur = !prevSet.has(row.Code);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={row.Code}
|
|
||||||
class={[
|
|
||||||
"bg-white",
|
|
||||||
firstOccur ? "text-[#ff5454]" : "text-black",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
|
|
||||||
{row.Name}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StockList({ data }: { data: PageCorpsInfo }) {
|
function StockList({ data }: { data: PageCorpsInfo }) {
|
||||||
@ -135,14 +120,13 @@ function StockList({ data }: { data: PageCorpsInfo }) {
|
|||||||
const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
|
const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
|
||||||
const rows = corpListByDate[x];
|
const rows = corpListByDate[x];
|
||||||
return (
|
return (
|
||||||
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows}></StockListByDate>
|
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type FilterInfoOption = {
|
type FilterInfoOption = {
|
||||||
list: {
|
list: {
|
||||||
items: CorpSimple[];
|
items: CorpSimple[];
|
||||||
@ -155,7 +139,7 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
|
|||||||
const checkMap = new Map<string, boolean>();
|
const checkMap = new Map<string, boolean>();
|
||||||
for (const l of filterList.list) {
|
for (const l of filterList.list) {
|
||||||
for (const i of l.items) {
|
for (const i of l.items) {
|
||||||
checkMap.set(i.code, l.include);
|
checkMap.set(i.Code, l.include);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return info.filter((x) => {
|
return info.filter((x) => {
|
||||||
@ -169,17 +153,13 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StockListUI(props: StockProps) {
|
export default function StockListUI(props: StockProps) {
|
||||||
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(async () => {
|
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() =>
|
||||||
const res = await Promise.all([
|
Promise.all([
|
||||||
fetch("/api/pages/" + encodeURIComponent(props.pageName)),
|
fetchPageInfo(props.pageName),
|
||||||
fetch("/api/kospi"),
|
fetchKospiList(),
|
||||||
fetch("/api/kosdaq"),
|
fetchKosdaqList(),
|
||||||
]);
|
])
|
||||||
const corpsInfo = await res[0].json() as PageCorpsInfo;
|
);
|
||||||
const kospi = await res[1].json();
|
|
||||||
const kosdaq = await res[2].json();
|
|
||||||
return [corpsInfo, kospi, kosdaq];
|
|
||||||
});
|
|
||||||
const viewKospi = useSignal(true);
|
const viewKospi = useSignal(true);
|
||||||
const viewKosdaq = useSignal(false);
|
const viewKosdaq = useSignal(false);
|
||||||
const viewOtherwise = useSignal(false);
|
const viewOtherwise = useSignal(false);
|
||||||
@ -203,34 +183,47 @@ export default function StockListUI(props: StockProps) {
|
|||||||
<p>File Loading Failed</p>
|
<p>File Loading Failed</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: <StockList data={applyFilter(sig.value.data[0], sig.value.data[1], sig.value.data[2])}></StockList>}
|
: (
|
||||||
|
<StockList
|
||||||
|
data={applyFilter(
|
||||||
|
sig.value.data[0],
|
||||||
|
sig.value.data[1],
|
||||||
|
sig.value.data[2],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
function applyFilter(data: PageCorpsInfo, kospi: CorpSimple[], kosdaq: CorpSimple[]): PageCorpsInfo{
|
function applyFilter(
|
||||||
const filter = getFilters(kospi,kosdaq);
|
data: PageCorpsInfo,
|
||||||
|
kospi: CorpSimple[],
|
||||||
|
kosdaq: CorpSimple[],
|
||||||
|
): PageCorpsInfo {
|
||||||
|
const filter = getFilters(kospi, kosdaq);
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[])=>{
|
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[]) => {
|
||||||
return filterInfo(it, filter);
|
return filterInfo(it, filter);
|
||||||
})
|
}),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
function getFilters(kospi: CorpSimple[], kosdaq: CorpSimple[]): FilterInfoOption{
|
function getFilters(
|
||||||
|
kospi: CorpSimple[],
|
||||||
|
kosdaq: CorpSimple[],
|
||||||
|
): FilterInfoOption {
|
||||||
return {
|
return {
|
||||||
otherwise: viewOtherwise.value,
|
otherwise: viewOtherwise.value,
|
||||||
list: [{
|
list: [{
|
||||||
include: viewKospi.value,
|
include: viewKospi.value,
|
||||||
items: kospi
|
items: kospi,
|
||||||
},
|
}, {
|
||||||
{
|
include: viewKosdaq.value,
|
||||||
include: viewKosdaq.value,
|
items: kosdaq,
|
||||||
items: kosdaq
|
}],
|
||||||
}
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
main.ts
8
main.ts
@ -9,9 +9,15 @@ import "$std/dotenv/load.ts";
|
|||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
|
|
||||||
import twindPlugin from "$fresh/plugins/twind.ts";
|
import twindPlugin from "$fresh/plugins/twindv1.ts";
|
||||||
import twindConfig from "./twind.config.ts";
|
import twindConfig from "./twind.config.ts";
|
||||||
|
|
||||||
|
console.log("start");
|
||||||
|
|
||||||
|
Deno.addSignalListener("SIGINT", () => {
|
||||||
|
Deno.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
await start(manifest, {
|
await start(manifest, {
|
||||||
port: 12001,
|
port: 12001,
|
||||||
plugins: [twindPlugin(twindConfig)]
|
plugins: [twindPlugin(twindConfig)]
|
||||||
|
15
pages.ts
15
pages.ts
@ -26,18 +26,25 @@ function watchFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
Deno.addSignalListener("SIGINT", () => {
|
const closeHandler = () => {
|
||||||
watcherRef.close();
|
watcherRef.close();
|
||||||
});
|
};
|
||||||
|
Deno.addSignalListener("SIGINT", closeHandler);
|
||||||
|
return ()=>{
|
||||||
|
Deno.removeSignalListener("SIGINT", closeHandler);
|
||||||
|
closeHandler();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pages_meta: PageDescription[] = [];
|
let pages_meta: PageDescription[] = [];
|
||||||
let mtime = 0;
|
let mtime = 0;
|
||||||
|
let lastest_disposer = () => {};
|
||||||
export async function get_pages_meta(): Promise<[PageDescription[],number]>{
|
export async function get_pages_meta(): Promise<[PageDescription[],number]>{
|
||||||
if (pages_meta) {
|
if (pages_meta.length == 0) {
|
||||||
pages_meta = await readPagesDescription();
|
pages_meta = await readPagesDescription();
|
||||||
mtime = Date.now();
|
mtime = Date.now();
|
||||||
watchFile(PAGES_PATH, async () => {
|
lastest_disposer();
|
||||||
|
lastest_disposer = watchFile(PAGES_PATH, async () => {
|
||||||
pages_meta = await readPagesDescription();
|
pages_meta = await readPagesDescription();
|
||||||
mtime = Date.now();
|
mtime = Date.now();
|
||||||
});
|
});
|
||||||
|
16
pages.yaml
16
pages.yaml
@ -36,11 +36,11 @@
|
|||||||
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
|
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
|
||||||
시그널을 구하기 위한 이동 평균은 4일입니다.
|
시그널을 구하기 위한 이동 평균은 4일입니다.
|
||||||
- name: 뭉침
|
- name: 뭉침
|
||||||
description: d5, d20, d45, d60 만난것 종가 5% 이내
|
description: d5, d20, d45 만난것 종가 5% 이내
|
||||||
- name: 뭉침01
|
- name: 뭉침01
|
||||||
description: d5, d20, d45, d60 만난것 종가 1% 이내
|
description: d5, d20, d45 만난것 종가 1% 이내
|
||||||
- name: 뭉침03
|
- name: 뭉침03
|
||||||
description: d5, d20, d45, d60 만난것 종가 3% 이내
|
description: d5, d20, d45 만난것 종가 3% 이내
|
||||||
|
|
||||||
- name: 45일선 반등
|
- name: 45일선 반등
|
||||||
description: 45일 선반등
|
description: 45일 선반등
|
||||||
@ -64,3 +64,13 @@
|
|||||||
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
|
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
|
||||||
- name: 양봉사이20일선
|
- name: 양봉사이20일선
|
||||||
description: Open과 Close 사이 20일 선
|
description: Open과 Close 사이 20일 선
|
||||||
|
- name: 양봉사이240일선증가
|
||||||
|
description: Open과 Close 사이 240일 선. 240일 선 증가
|
||||||
|
- name: 떠있음
|
||||||
|
description: |
|
||||||
|
양봉, 음봉이 20일선, 60일선, 120선보다 떠있으면
|
||||||
|
- name: 5일선반등120선증가
|
||||||
|
description: 5일선이 반등 120 선 증가
|
||||||
|
- name: 120선240선추월
|
||||||
|
description: |
|
||||||
|
120선이 상승해서 240일 선을 뚫을때
|
35
routes/api/corps/[index].ts
Normal file
35
routes/api/corps/[index].ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Handlers } from "$fresh/server.ts";
|
||||||
|
import { db } from "../../../db/db.ts";
|
||||||
|
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
async GET(req, ctx): Promise<Response> {
|
||||||
|
const headers = new Headers({
|
||||||
|
"content-type": "application/json"
|
||||||
|
});
|
||||||
|
const index = ctx.params.index;
|
||||||
|
const corp = await db.selectFrom("KRXCorp")
|
||||||
|
.selectAll([
|
||||||
|
"KRXCorp"
|
||||||
|
])
|
||||||
|
.select(eb=> [
|
||||||
|
jsonArrayFrom(eb.selectFrom("stock")
|
||||||
|
.select([
|
||||||
|
"stock.Close",
|
||||||
|
"stock.Open",
|
||||||
|
"stock.Low",
|
||||||
|
"stock.High",
|
||||||
|
"stock.Date",
|
||||||
|
"stock.Volume",
|
||||||
|
])
|
||||||
|
.where("Code", "=", index)
|
||||||
|
.orderBy("Date", "desc")
|
||||||
|
.limit(100)
|
||||||
|
).as("prices")]
|
||||||
|
)
|
||||||
|
.where("Code", "=", index)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(corp ?? null), {headers});
|
||||||
|
},
|
||||||
|
}
|
21
routes/api/corps/index.ts
Normal file
21
routes/api/corps/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Handlers } from "$fresh/server.ts";
|
||||||
|
import { db } from "../../../db/db.ts";
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
async GET(req, _ctx): Promise<Response> {
|
||||||
|
const headers = new Headers({
|
||||||
|
"content-type": "application/json"
|
||||||
|
});
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const q = url.searchParams.get("q");
|
||||||
|
const name = url.searchParams.get("name");
|
||||||
|
const corps = await db.selectFrom("KRXCorp")
|
||||||
|
.selectAll([
|
||||||
|
"KRXCorp"
|
||||||
|
])
|
||||||
|
.$if(!!q, qb=> qb.where("Name", "like", "%"+q+"%"))
|
||||||
|
.$if(!!name, qb => qb.where("Name", "=", name))
|
||||||
|
.execute();
|
||||||
|
return new Response(JSON.stringify(corps), {headers});
|
||||||
|
},
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import {DB} from "https://deno.land/x/sqlite/mod.ts";
|
import { db } from "../../db/db.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(req, _ctx): Promise<Response> {
|
async GET(req, _ctx): Promise<Response> {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
});
|
});
|
||||||
const db = new DB("stock.db");
|
const rows = await db.selectFrom("KOSDAQ")
|
||||||
const conn = db.query("SELECT Code,Name FROM KOSDAQ");
|
.select([
|
||||||
const body = conn.map(row=>({
|
"Code",
|
||||||
code: row[0],
|
"Name"
|
||||||
name: row[1]
|
])
|
||||||
}))
|
.execute();
|
||||||
return new Response(JSON.stringify(body), {headers});
|
return new Response(JSON.stringify(rows), {headers});
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -1,17 +1,17 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import {DB} from "https://deno.land/x/sqlite/mod.ts";
|
import { db } from "../../db/db.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(req, _ctx): Promise<Response> {
|
async GET(req, _ctx): Promise<Response> {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
});
|
});
|
||||||
const db = new DB("stock.db");
|
const rows = await db.selectFrom("KOSPI")
|
||||||
const conn = db.query("SELECT Code,Name FROM KOSPI");
|
.select([
|
||||||
const body = conn.map(row=>({
|
"Code",
|
||||||
code: row[0],
|
"Name"
|
||||||
name: row[1]
|
])
|
||||||
}))
|
.execute();
|
||||||
return new Response(JSON.stringify(body), {headers});
|
return new Response(JSON.stringify(rows), {headers});
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -25,16 +25,18 @@ export const handler: Handlers = {
|
|||||||
const mtime = stat.mtime ?? new Date(0);
|
const mtime = stat.mtime ?? new Date(0);
|
||||||
const body = await Deno.readTextFile(path);
|
const body = await Deno.readTextFile(path);
|
||||||
headers.set("last-modified", mtime.toUTCString());
|
headers.set("last-modified", mtime.toUTCString());
|
||||||
|
console.log(mtime);
|
||||||
|
// headers.set("cache-control", "max-age=600");
|
||||||
|
|
||||||
const ifModifiedSinceValue = req.headers.get("if-modified-since");
|
// const ifModifiedSinceValue = req.headers.get("if-modified-since");
|
||||||
if ( ifModifiedSinceValue &&
|
// if ( ifModifiedSinceValue &&
|
||||||
mtime.getTime() != new Date(ifModifiedSinceValue).getTime()
|
// mtime.getTime() <= new Date(ifModifiedSinceValue).getTime()
|
||||||
){
|
// ){
|
||||||
return new Response(null, {
|
// return new Response(null, {
|
||||||
status: Status.NotModified,
|
// status: Status.NotModified,
|
||||||
statusText: STATUS_TEXT[Status.NotModified]
|
// statusText: STATUS_TEXT[Status.NotModified]
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
return new Response(body, {headers});
|
return new Response(body, {headers});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,25 +1,21 @@
|
|||||||
import { Head } from "$fresh/runtime.ts";
|
import { Head } from "$fresh/runtime.ts";
|
||||||
import { useSignal } from "@preact/signals";
|
import { get_pages_meta, PageDescription } from "../pages.ts";
|
||||||
import {Button} from "../components/Button.tsx";
|
|
||||||
import { PageDescription, get_pages_meta } from "../pages.ts";
|
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(_req, ctx){
|
async GET(_req, ctx) {
|
||||||
const [pages,_] = await get_pages_meta();
|
const [pages, _] = await get_pages_meta();
|
||||||
return await ctx.render(pages);
|
return await ctx.render(pages);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Home({data}: PageProps<PageDescription[]>) {
|
export default function Home({ data }: PageProps<PageDescription[]>) {
|
||||||
const count = useSignal(3);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>stock-front</title>
|
<title>stock-front</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
|
||||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="my-6"
|
class="my-6"
|
||||||
@ -31,14 +27,16 @@ export default function Home({data}: PageProps<PageDescription[]>) {
|
|||||||
<h1 class="text-4xl font-bold">Stock</h1>
|
<h1 class="text-4xl font-bold">Stock</h1>
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{data.map((x) => (
|
||||||
data.map(x=><li class="my-2">
|
<li class="my-2">
|
||||||
<a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${encodeURIComponent(x.name)}`}>
|
<a
|
||||||
|
class="p-2 block hover:bg-gray-300 bg-white rounded"
|
||||||
|
href={`/pages/${encodeURIComponent(x.name)}`}
|
||||||
|
>
|
||||||
{x.name}
|
{x.name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)
|
))}
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import { PageProps } from "$fresh/server.ts";
|
import { PageProps, Handlers } from "$fresh/server.ts";
|
||||||
import { Head } from "$fresh/runtime.ts";
|
import { Head } from "$fresh/runtime.ts";
|
||||||
|
import { get_pages_meta, PageDescription } from "../../pages.ts";
|
||||||
import StockList from "../../islands/StockList.tsx";
|
import StockList from "../../islands/StockList.tsx";
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
async GET(_req, ctx) {
|
||||||
|
const [pages, _] = await get_pages_meta();
|
||||||
|
const name = ctx.params.name;
|
||||||
|
const page = pages.filter(x=> x.name === name);
|
||||||
|
if (page.length === 0) {
|
||||||
|
return await ctx.renderNotFound();
|
||||||
|
}
|
||||||
|
return await ctx.render(page[0]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function Pages(props: PageProps) {
|
export default function Pages(props: PageProps<PageDescription>) {
|
||||||
return <>
|
return <>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Stock: {props.params.name}</title>
|
<title>Stock: {props.params.name}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
|
||||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="my-6"
|
class="my-6"
|
||||||
@ -18,6 +30,7 @@ export default function Pages(props: PageProps) {
|
|||||||
alt="stock graph"
|
alt="stock graph"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-4xl">{props.params.name}</h1>
|
<h1 class="text-4xl">{props.params.name}</h1>
|
||||||
|
<p>{props.data.description}</p>
|
||||||
<StockList pageName={props.params.name}></StockList>
|
<StockList pageName={props.params.name}></StockList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Stock</title>
|
|
||||||
<style>
|
|
||||||
body{
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(to right, #2b2b2b, #3d1844);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.table_item:nth-child(2n){
|
|
||||||
background: #a7a7a7;
|
|
||||||
}
|
|
||||||
.table_item:nth-child(2n+1){
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.table_item:hover{
|
|
||||||
background: #8d8d8d;
|
|
||||||
}
|
|
||||||
.container{
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 24px auto;
|
|
||||||
background: #f0f0f0;
|
|
||||||
color: black;
|
|
||||||
box-shadow: 0px 0px 5px 0px white;
|
|
||||||
text-decoration: none;
|
|
||||||
grid-template-columns: repeat({{ 5 }}, 1fr);
|
|
||||||
}
|
|
||||||
.container a:link, a:visited{
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
.data_header{
|
|
||||||
border-bottom: 1px solid #a7a7a7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="margin: auto; max-width: 750px;">
|
|
||||||
<h1>{{title}} Stock List</h1>
|
|
||||||
<h3>{{lastUpdate}}</h3>
|
|
||||||
<section class="description">
|
|
||||||
{{collected.description}}
|
|
||||||
</section>
|
|
||||||
<section class="container">
|
|
||||||
{% for day in days|reverse %}
|
|
||||||
<div class="data_header">{{ day }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% for day in days|reverse %}
|
|
||||||
{% set corplist = collected.corpListByDate[day] %}
|
|
||||||
<div>{% for item in corplist %}
|
|
||||||
<div class="table_item"><a href="https://stockplus.com/m/stocks/KOREA-A{{ item.Code }}">{{ item.Name }}({{item.Code}})</a></div>{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Stock</title>
|
|
||||||
<style>
|
|
||||||
body{
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(to right, #2b2b2b, #3d1844);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.container{
|
|
||||||
display: grid;
|
|
||||||
background: #f0f0f0;
|
|
||||||
color: black;
|
|
||||||
box-shadow: 0px 0px 5px 0px white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.container a:link, a:visited{
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
.data_header{
|
|
||||||
border-bottom: 1px solid #a7a7a7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="margin: auto; max-width: 750px;">
|
|
||||||
<h1>Main</h1>
|
|
||||||
<div class="container">
|
|
||||||
{% for p in pages %}
|
|
||||||
<a href="/dist/{{p.name}}.html">{{p.name}}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,5 +1,10 @@
|
|||||||
import { Options } from "$fresh/plugins/twind.ts";
|
import { defineConfig, Preset } from "https://esm.sh/@twind/core@1.1.3";
|
||||||
|
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
|
||||||
|
import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.7";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
...defineConfig({
|
||||||
|
presets: [presetTailwind() as Preset, presetAutoprefix()],
|
||||||
|
}),
|
||||||
selfURL: import.meta.url,
|
selfURL: import.meta.url,
|
||||||
} as Options;
|
}
|
||||||
|
38
util/api.ts
Normal file
38
util/api.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
export interface Coperation {
|
||||||
|
Name: string;
|
||||||
|
Code: string;
|
||||||
|
Sector: string;
|
||||||
|
Product: string;
|
||||||
|
ListingDay: string;
|
||||||
|
ClosingMonth: string;
|
||||||
|
Representative: string;
|
||||||
|
Homepage: string;
|
||||||
|
AddressArea: string;
|
||||||
|
LastUpdate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageCorpsInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
corpListByDate: Record<string, Coperation[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CorpSimple {
|
||||||
|
Code: string;
|
||||||
|
Name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPageInfo(pageName: string): Promise<PageCorpsInfo>{
|
||||||
|
const res = await fetch("/api/pages/" + encodeURIComponent(pageName));
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchKospiList(): Promise<CorpSimple[]>{
|
||||||
|
const res = await fetch("/api/kospi");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchKosdaqList(): Promise<CorpSimple[]> {
|
||||||
|
const res = await fetch("/api/kosdaq");
|
||||||
|
return await res.json();
|
||||||
|
}
|
35
util/util.ts
Normal file
35
util/util.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Signal, useSignal } from "@preact/signals";
|
||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
export type QueryStatus<T> = {
|
||||||
|
type: "loading";
|
||||||
|
} | {
|
||||||
|
type: "complete";
|
||||||
|
data: T;
|
||||||
|
} | {
|
||||||
|
type: "error";
|
||||||
|
err: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
|
||||||
|
const state = useSignal({
|
||||||
|
type: "loading",
|
||||||
|
} as QueryStatus<T>);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fn();
|
||||||
|
state.value = {
|
||||||
|
type: "complete",
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
state.value = {
|
||||||
|
type: "error",
|
||||||
|
err: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
return state;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user