Compare commits

..

No commits in common. "1f83b6abf97564197b5c3eb4932291668f06b5e8" and "e0b6eab7cf77c526b78c7461b762bab3856072d2" have entirely different histories.

28 changed files with 362 additions and 668 deletions

3
.gitignore vendored
View File

@ -10,6 +10,3 @@ stock.db
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# Fresh build directory
_fresh/

View File

@ -1,5 +1,3 @@
# Stock # Stock
[![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev)
주식 데이터 수집 및 선별하는 파이썬 코드입니다. 주식 데이터 수집 및 선별하는 파이썬 코드입니다.

24
app.py Normal file
View File

@ -0,0 +1,24 @@
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)

View File

@ -1,11 +0,0 @@
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()]
});

View File

@ -1,27 +0,0 @@
/// 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 };

View File

@ -1,49 +0,0 @@
/// 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 };

View File

@ -1,112 +0,0 @@
/// 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 };

View File

@ -1,64 +0,0 @@
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;
}

View File

@ -2,28 +2,31 @@
"lock": false, "lock": false,
"tasks": { "tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts", "start": "deno run -A --watch=static/,routes/ dev.ts",
"prod_start": "deno run -A main.ts", "update": "deno run -A -r https://fresh.deno.dev/update ."
"update": "deno run -A -r https://fresh.deno.dev/update .", },
"build": "deno run -A dev.ts build", "lint": {
"preview": "deno run -A main.ts" "rules": {
"tags": [
"fresh",
"recommended"
]
}
}, },
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"imports": { "imports": {
"$fresh/": "https://deno.land/x/fresh@1.5.2/", "$fresh/": "https://deno.land/x/fresh@1.3.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/": "https://esm.sh/preact@10.15.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2", "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
"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.203.0/", "$std/": "https://deno.land/std@0.193.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": { "jsx": "react-jsx", "jsxImportSource": "preact" }, "compilerOptions": {
"exclude": ["**/_fresh/*"] "jsx": "react-jsx",
"jsxImportSource": "preact"
}
} }

View File

@ -1,42 +1,36 @@
// 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/corps/[index].ts"; import * as $2 from "./routes/api/joke.ts";
import * as $3 from "./routes/api/corps/index.ts"; import * as $3 from "./routes/api/kosdaq.ts";
import * as $4 from "./routes/api/joke.ts"; import * as $4 from "./routes/api/kospi.ts";
import * as $5 from "./routes/api/kosdaq.ts"; import * as $5 from "./routes/api/pages/[name].ts";
import * as $6 from "./routes/api/kospi.ts"; import * as $6 from "./routes/api/pages/index.ts";
import * as $7 from "./routes/api/pages/[name].ts"; import * as $7 from "./routes/greet/[name].tsx";
import * as $8 from "./routes/api/pages/index.ts"; import * as $8 from "./routes/index.tsx";
import * as $9 from "./routes/greet/[name].tsx"; import * as $9 from "./routes/pages/[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/Search.tsx"; import * as $$1 from "./islands/StockList.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/corps/[index].ts": $2, "./routes/api/joke.ts": $2,
"./routes/api/corps/index.ts": $3, "./routes/api/kosdaq.ts": $3,
"./routes/api/joke.ts": $4, "./routes/api/kospi.ts": $4,
"./routes/api/kosdaq.ts": $5, "./routes/api/pages/[name].ts": $5,
"./routes/api/kospi.ts": $6, "./routes/api/pages/index.ts": $6,
"./routes/api/pages/[name].ts": $7, "./routes/greet/[name].tsx": $7,
"./routes/api/pages/index.ts": $8, "./routes/index.tsx": $8,
"./routes/greet/[name].tsx": $9, "./routes/pages/[name].tsx": $9,
"./routes/index.tsx": $10,
"./routes/pages/[name].tsx": $11,
}, },
islands: { islands: {
"./islands/Counter.tsx": $$0, "./islands/Counter.tsx": $$0,
"./islands/Search.tsx": $$1, "./islands/StockList.tsx": $$1,
"./islands/StockList.tsx": $$2,
}, },
baseUrl: import.meta.url, baseUrl: import.meta.url,
}; };

53
gen.py
View File

@ -5,6 +5,7 @@ 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
@ -91,7 +92,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 + 1]) / b.iloc[nday + 1] < threshold return abs(a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < 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"""
@ -144,6 +145,7 @@ 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)
@ -154,14 +156,8 @@ 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] a = [d5, d20, d45, d60]
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])
@ -211,9 +207,6 @@ 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]):
@ -224,11 +217,6 @@ 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])
@ -260,6 +248,8 @@ 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')
@ -278,6 +268,10 @@ 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)
@ -287,10 +281,25 @@ if __name__ == "__main__":
dataStore.clearCache() dataStore.clearCache()
for k,v in collector.data.items(): for k,v in collector.data.items():
data = json.dumps(v.toDict(), ensure_ascii=False) if args.format == "json":
if args.printStdout: data = json.dumps(v.toDict(), indent=4, ensure_ascii=False)
print(k) if args.printStdout:
print(data) print(k)
print(data)
else:
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
f.write(data)
else: else:
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f: template = env.get_template("Lists.html")
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)

View File

@ -1,5 +0,0 @@
export default function Search(){
return <div>
<div>div</div>
</div>
}

View File

@ -1,18 +1,9 @@
import { Button } from "../components/Button.tsx"; import { Button } from "../components/Button.tsx";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; import { useEffect, 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;
@ -40,70 +31,94 @@ function ToggleButton(props: ToggleButtonProps) {
); );
} }
function StockListByDate( type QueryStatus<T> = {
{ prevSet, rows, name }: { type: "loading";
prevSet: Set<string>; } | {
rows: Coperation[]; type: "complete";
name: string; data: T;
}, } | {
) { type: "error";
const lastCount = useRef(rows.length); err: Error;
const curCount = rows.length; };
const parent = useRef<HTMLDivElement>(null);
const controller = useRef< function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
{ const state = useSignal({
isEnabled: () => boolean; type: "loading",
disable: () => void; } as QueryStatus<T>);
enable: () => void;
} | undefined
>();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
console.log("animation mount on ", name); try {
const { default: autoAnimate } = await import( const data = await fn();
"https://esm.sh/@formkit/auto-animate@0.7.0" state.value = {
); type: "complete",
if (parent.current) { data: data,
const cntr = autoAnimate(parent.current); };
controller.current = cntr; } catch (err) {
state.value = {
type: "error",
err: err,
};
} }
})(); })();
}, [parent]); }, []);
return state;
}
useLayoutEffect(() => { interface Coperation {
if (controller.current) { Name: string;
if (Math.abs(curCount - lastCount.current) > 200) { Code: string;
console.log("disable animation", curCount, "from", lastCount.current); Sector: string;
controller.current.disable(); Product: string;
} else { ListingDay: string;
console.log("enable animation", curCount, "from", lastCount.current); ClosingMonth: string;
controller.current.enable(); Representative: string;
} Homepage: string;
lastCount.current = curCount; AddressArea: string;
} LastUpdate: string;
}, [parent, rows]); }
return ( interface PageCorpsInfo {
<div ref={parent}> name: string;
<h2 class="text-lg">{name}</h2> description: string;
{rows.map((row) => { corpListByDate: Record<string, Coperation[]>;
const firstOccur = !prevSet.has(row.Code); }
return (
<div interface CorpSimple {
key={row.Code} code: string;
class={[ name: string;
"bg-white", }
firstOccur ? "text-[#ff5454] underline" : "text-black",
].join(" ")} function StockListByDate({prevSet, rows, name}:{prevSet:Set<string>,
> rows:Coperation[],
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}> name: string}){
{row.Name} const parent = useRef<HTMLDivElement>(null);
</a> useEffect(()=>{
</div> (async ()=>{
); console.log("animation mount on ",name);
})} const {default:autoAnimate} = await import("https://esm.sh/@formkit/auto-animate@0.7.0");
</div> parent.current && autoAnimate(parent.current)
); })();
},[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 }) {
@ -120,13 +135,14 @@ 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 key={x} name={x} prevSet={prevSet} rows={rows}></StockListByDate>
); );
})} })}
</div> </div>
); );
} }
type FilterInfoOption = { type FilterInfoOption = {
list: { list: {
items: CorpSimple[]; items: CorpSimple[];
@ -139,7 +155,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) => {
@ -153,13 +169,17 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
} }
export default function StockListUI(props: StockProps) { export default function StockListUI(props: StockProps) {
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() => const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(async () => {
Promise.all([ const res = await Promise.all([
fetchPageInfo(props.pageName), fetch("/api/pages/" + encodeURIComponent(props.pageName)),
fetchKospiList(), fetch("/api/kospi"),
fetchKosdaqList(), fetch("/api/kosdaq"),
]) ]);
); 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);
@ -183,47 +203,34 @@ 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( function applyFilter(data: PageCorpsInfo, kospi: CorpSimple[], kosdaq: CorpSimple[]): PageCorpsInfo{
data: PageCorpsInfo, const filter = getFilters(kospi,kosdaq);
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( function getFilters(kospi: CorpSimple[], kosdaq: CorpSimple[]): FilterInfoOption{
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, {
items: kosdaq, include: viewKosdaq.value,
}], items: kosdaq
}; }
]
}
} }
} }

View File

@ -9,15 +9,9 @@ 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/twindv1.ts"; import twindPlugin from "$fresh/plugins/twind.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)]

View File

@ -26,25 +26,18 @@ function watchFile(
} }
} }
})(); })();
const closeHandler = () => { Deno.addSignalListener("SIGINT", () => {
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.length == 0) { if (pages_meta) {
pages_meta = await readPagesDescription(); pages_meta = await readPagesDescription();
mtime = Date.now(); mtime = Date.now();
lastest_disposer(); watchFile(PAGES_PATH, async () => {
lastest_disposer = watchFile(PAGES_PATH, async () => {
pages_meta = await readPagesDescription(); pages_meta = await readPagesDescription();
mtime = Date.now(); mtime = Date.now();
}); });

View File

@ -36,11 +36,11 @@
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고 macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
시그널을 구하기 위한 이동 평균은 4일입니다. 시그널을 구하기 위한 이동 평균은 4일입니다.
- name: 뭉침 - name: 뭉침
description: d5, d20, d45 만난것 종가 5% 이내 description: d5, d20, d45, d60 만난것 종가 5% 이내
- name: 뭉침01 - name: 뭉침01
description: d5, d20, d45 만난것 종가 1% 이내 description: d5, d20, d45, d60 만난것 종가 1% 이내
- name: 뭉침03 - name: 뭉침03
description: d5, d20, d45 만난것 종가 3% 이내 description: d5, d20, d45, d60 만난것 종가 3% 이내
- name: 45일선 반등 - name: 45일선 반등
description: 45일 선반등 description: 45일 선반등
@ -64,13 +64,3 @@
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일 선을 뚫을때

View File

@ -1,35 +0,0 @@
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});
},
}

View File

@ -1,21 +0,0 @@
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});
},
}

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../db/db.ts"; import {DB} from "https://deno.land/x/sqlite/mod.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 rows = await db.selectFrom("KOSDAQ") const db = new DB("stock.db");
.select([ const conn = db.query("SELECT Code,Name FROM KOSDAQ");
"Code", const body = conn.map(row=>({
"Name" code: row[0],
]) name: row[1]
.execute(); }))
return new Response(JSON.stringify(rows), {headers}); return new Response(JSON.stringify(body), {headers});
}, },
} }

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../db/db.ts"; import {DB} from "https://deno.land/x/sqlite/mod.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 rows = await db.selectFrom("KOSPI") const db = new DB("stock.db");
.select([ const conn = db.query("SELECT Code,Name FROM KOSPI");
"Code", const body = conn.map(row=>({
"Name" code: row[0],
]) name: row[1]
.execute(); }))
return new Response(JSON.stringify(rows), {headers}); return new Response(JSON.stringify(body), {headers});
}, },
} }

View File

@ -25,18 +25,16 @@ 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});
}, },
}; };

View File

@ -1,21 +1,25 @@
import { Head } from "$fresh/runtime.ts"; import { Head } from "$fresh/runtime.ts";
import { get_pages_meta, PageDescription } from "../pages.ts"; import { useSignal } from "@preact/signals";
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] min-h-screen"> <div class="px-4 py-8 mx-auto bg-[#86efac]">
<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"
@ -27,16 +31,14 @@ 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) => ( {
<li class="my-2"> data.map(x=><li class="my-2">
<a <a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${encodeURIComponent(x.name)}`}>
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>

View File

@ -1,26 +1,14 @@
import { PageProps, Handlers } from "$fresh/server.ts"; import { PageProps } 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<PageDescription>) { export default function Pages(props: PageProps) {
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] min-h-screen"> <div class="px-4 py-8 mx-auto bg-[#86efac]">
<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"
@ -30,7 +18,6 @@ export default function Pages(props: PageProps<PageDescription>) {
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>

60
templates/Lists.html Normal file
View File

@ -0,0 +1,60 @@
<!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>

40
templates/index.html Normal file
View File

@ -0,0 +1,40 @@
<!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>

View File

@ -1,10 +1,5 @@
import { defineConfig, Preset } from "https://esm.sh/@twind/core@1.1.3"; import { Options } from "$fresh/plugins/twind.ts";
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;

View File

@ -1,38 +0,0 @@
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();
}

View File

@ -1,35 +0,0 @@
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;
}